猛士设计了Netfilter,在失眠的时候就有事做了,安息日应守为圣日,否则会激怒神,因此双休日我一般不学习和工作,相反,我会在午夜玩一些自己喜欢的东西。我没有受过洗,不是因为不是笃信者,没有安息夜...

NAT和timestamps问题

这个问题就不多说了,总之,NAT设备将所有数据包的大量不同的源地址都转换为了单一的或者少数几个地址,这个转换动作和TCP服务器的PAWS机制一起工作的时候会导致无法新建连接的问题。

解决办法

简单的讲,解决办法有两个,一个是在客户端禁掉TCP的时间戳机制,另一个是在TCP服务端禁掉TCP的时间戳机制,可是一般而言,这两个地方都不是我们所能控制的,比如,你能禁掉每一个智能手机的TCP时间戳,也不能指望手机用户去做这件事,你同样也不能指望可以顺利地禁掉服务器的TCP时间戳,于是,第三个办法就出炉了,那就是在中间的NAT设备上修改掉TCP数据包,实际上只要修改TCP初始化的SYN包即可,将时间戳选项去掉,只所有这么做是可行的,归功于TCP协议和IP协议的协议头是规则且简单的。

关于TCP协议头的options与NOP

任何协议,如果没有被设计成可扩展性的,那它就不是一个好的协议。一个好的协议,在其基础部分应该取定长的格式,而其扩展的部分,应该是一定范围内的变长格式,不管是IP协议还是TCP协议,协议头都有一个“头长度”这么一个字段,该字段正是为了表示所谓的扩展协议头,否则如果都是定长的,也就不需要该字段了,因此可以说,IP协议和TCP协议都是比较好的协议。

       识别了协议好坏的标准之后,扩展字段如何布局就纯粹成了一个编码问题,一般而言,“类型-长度-值”的方式是首选,它可以很方便的编码任意类型,任意长度的扩展字段,在TCP的协议头扩展中,其名称叫做options,即TCP选项,它便是采用了上述的编码方式,每一个选项我可以称之为一个“块”,整个TCP options由多个块组成,每一个块由下面的结构组成:


wKiom1L0TbiB2FG2AAAonfbOnGc782.jpg


注意,有一种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 
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "compat_xtables.h"


MODULE_AUTHOR("Wanagran ");
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,我自己在测试的时候,抓包结果如下:

新建连接的SYN包:


NAT网关自动去掉TCP syn包的时间戳_第1张图片



ESTABLISHED状态的非SYN包:


NAT网关自动去掉TCP syn包的时间戳_第2张图片


感悟

对于一个在IT领域从业5年以上的人而言,任何问题靠技术手段解决都不是个事,关键是如何彻底地解决,相比于每次重复相同的排障过程,一次性解决会好很多,当然,这也许会减少一些出头露面的机会。这个timestamps+NAT问题是如此简单,以至于百行量级的代码就能搞定,但是为何加入这项功能的设备却如此之少,反之,网上的这方面的文章却是汗牛充栋(当然我也贡献过几篇)。难道是因为有些服务器确实要用到timestamps吗?而正常的理由就是,不能改变TCP的语义!我想骂了,NAT保留IP的语义了吗?它为何后来成了标准的东西,原始的IP语义是希望任意IP主机可以仅依靠IP路由双向互联互通,结果有了状态NAT以后,事情复杂了,于是更加复杂的各种打洞技术被呼唤出来,几年前我曾经遇到过一个人,自诩他实现的打洞技术世界第一,我C-T-M-D,他就是一小丑!