NAT和timestamps问题
这个问题就不多说了,总之,NAT设备将所有数据包的大量不同的源地址都转换为了单一的或者少数几个地址,这个转换动作和TCP服务器的PAWS机制一起工作的时候会导致无法新建连接的问题。解决办法
简单的讲,解决办法有两个,一个是在客户端禁掉TCP的时间戳机制,另一个是在TCP服务端禁掉TCP的时间戳机制,可是一般而言,这两个地方都不是我们所能控制的,比如,你能禁掉每一个智能手机的TCP时间戳,也不能指望手机用户去做这件事,你同样也不能指望可以顺利地禁掉服务器的TCP时间戳,于是,第三个办法就出炉了,那就是在中间的NAT设备上修改掉TCP数据包,实际上只要修改TCP初始化的SYN包即可,将时间戳选项去掉,只所有这么做是可行的,归功于TCP协议和IP协议的协议头是规则且简单的。关于TCP协议头的options与NOP
任何协议,如果没有被设计成可扩展性的,那它就不是一个好的协议。一个好的协议,在其基础部分应该取定长的格式,而其扩展的部分,应该是一定范围内的变长格式,不管是IP协议还是TCP协议,协议头都有一个“头长度”这么一个字段,该字段正是为了表示所谓的扩展协议头,否则如果都是定长的,也就不需要该字段了,因此可以说,IP协议和TCP协议都是比较好的协议。注意,有一种TCP选项叫做NOP,它是为了确保TCP协议头结束在4字节对齐的位置,我们可以从RFC793的3.1节看出:
Padding: variable
The TCP header padding is used to ensure that the TCP header ends
and data begins on a 32 bit boundary. The padding is composed of
zeros.
在本文中,我心爱的NOP并不是为了填充的,而是为了用其替换时间戳选项。我不知道别的系统怎么处理NOP,反正在Linux中,是直接忽略NOP,而这种忽略正是提供了一种将时间戳选项替换为NOP的可能性。
代码与解释
Netfilter原则上可以将所有的数据包“偷走”,从标准协议栈偷走,这么极端的事它都可以做,还有什么不能做的呢?和往常一样,还是写一个iptables的module,包括两个组件,一个是内核模块,另一个是iptables的用户态模块。我依然将取名字这件事搁置,因此我的这个target姑且确定为YYY,内核模块代码如下:
/*
* xt_yyy - kernel module to drop TCP timestamps
*
* Original author: Wanagran <marywangran@126.com>
*/
#include <linux/module.h>
#include <linux/netfilter/x_tables.h>
#include <linux/netfilter.h>
#include <linux/skbuff.h>
#include <linux/tcp.h>
#include <net/tcp.h>
#include <linux/ip.h>
#include "compat_xtables.h"
MODULE_AUTHOR("Wanagran <marywangran@126.com>");
MODULE_DESCRIPTION("Xtables: yyy match module");
MODULE_LICENSE("GPL");
MODULE_ALIAS("ipt_yyy");
static unsigned int
yyy_tg4(struct sk_buff **skb, const struct xt_action_param *par)
{
const struct iphdr *iph = (struct iphdr *)((*skb)->data);
struct tcphdr *hdr;
unsigned int hdroff = iph->ihl*4;
int datalen = (*skb)->len - hdroff;
int hdrsize = 8; /* TCP connection tracking guarantees this much */
const unsigned char *ptr;
unsigned char buff[(15 * 4) - sizeof(struct tcphdr)];
int length;
int recalc = 0;
if (iph->protocol != IPPROTO_TCP) {
return XT_CONTINUE;
}
hdr = (struct tcphdr *)((*skb)->data + hdroff);
/**
* 以下这个判断不适合在代码中写死,因为:
* iptables完全可以用TCP的flags match来完成这个判断
*
* if (!hdr->syn) {
* return XT_CONTINUE;
* }
*
* 另外,很多人应该觉得检查一下conntrack是否已经加载,如果没有加载就
* 直接CONTINUE,但是我没有这么做,因为NAT的实现并不一定要基于conntrack
*/
if ((*skb)->len >= hdroff + sizeof(struct tcphdr))
hdrsize = sizeof(struct tcphdr);
if (!skb_make_writable(skb, hdroff + hdrsize))
return XT_CONTINUE;
length = (hdr->doff*4) - sizeof(struct tcphdr);
ptr = skb_header_pointer(*skb, sizeof(struct tcphdr) + hdroff,
length, buff);
while (length > 0) {
int opcode=*ptr++;
int opsize;
switch (opcode) {
case TCPOPT_EOL:
returni XT_CONTINUE;
case TCPOPT_NOP: /* Ref: RFC 793 section 3.1 */
length--;
continue;
case TCPOPT_TIMESTAMP:
{
int i = 0;
char *base = ptr-1;
opsize=*ptr;
/**
* 为了减少数据移动以及指针移动,进而减少内存拷贝
* 我只是将时间戳替换成了NOP而已
*/
for (; i < opsize; i++, base++) {
*base = 0x01;
}
recalc = 1;
}
default:
opsize=*ptr++;
length -= opsize;
ptr += opsize - 2;
}
}
/**
* 改变了TCP头后,重新计算校验码是必然的,但是以下的
* 代码太粗糙,因为它没有考虑硬件也有能力计算校验码
* 这么一件事!
*/
if (recalc) {
hdr->check = 0;
hdr->check = tcp_v4_check(datalen,
iph->saddr, iph->daddr,
csum_partial(hdr,
datalen, 0));
}
return XT_CONTINUE;
}
static struct xt_target yyy_tg_reg[] __read_mostly = {
{
.name = "YYY",
.revision = 1,
.family = NFPROTO_IPV4,
.target = yyy_tg4,
.me = THIS_MODULE,
},
};
static int __init xt_yyy_target_init(void)
{
int status = 0;
status = xt_register_targets(yyy_tg_reg, ARRAY_SIZE(yyy_tg_reg));
if (status < 0) {
printk("YYY: register target error\n");
goto err;
}
err:
return status;
}
static void __exit xt_yyy_target_exit(void)
{
return xt_unregister_targets(yyy_tg_reg, ARRAY_SIZE(yyy_tg_reg));
}
module_init(xt_yyy_target_init);
module_exit(xt_yyy_target_exit);
该有的解释都在注释里面了。用户态的模块就不贴出了了,例行公事而已,没有任何逻辑,毕竟目前的版本YYY target不需要任何参数。
YYY模块的使用
只需要简单地在NAT网关添加一个iptables规则:iptables -A FORWARD -p tcp -......-j YYY
接下来的时间里,你将不会再面对NAT设备的TCP timestamps的问题了。值得注意的是,由于TCP服务端仅仅针对新建连接来做检查,因此可以不必对非SYN包来做YYY target,我自己在测试的时候,抓包结果如下: