NAT網關自動去掉TCP syn包的時間戳

猛士設計了Netfilter,在失眠的時候就有事作了,安息日應守爲聖日,不然會激怒神,所以雙休日我通常不學習和工做,相反,我會在午夜玩一些本身喜歡的東西。我沒有受過洗,不是由於不是篤信者,沒有安息夜...
linux

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 <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,我本身在測試的時候,抓包結果以下:

新建鏈接的SYN包:tcp


wKioL1L0Ta-BGKLMAAvXeKmrggo257.bmp



ESTABLISHED狀態的非SYN包:ide


wKiom1L0TezBKJSTAAjqJPOsh6Q402.bmp


感悟

對於一個在IT領域從業5年以上的人而言,任何問題靠技術手段解決都不是個事,關鍵是如何完全地解決,相比於每次重複相同的排障過程,一次性解決會好不少,固然,這也許會減小一些出頭露面的機會。這個timestamps+NAT問題是如此簡單,以致於百行量級的代碼就能搞定,可是爲什麼加入這項功能的設備卻如此之少,反之,網上的這方面的文章倒是汗牛充棟(固然我也貢獻過幾篇)。難道是由於有些服務器確實要用到timestamps嗎?而正常的理由就是,不能改變TCP的語義!我想罵了,NAT保留IP的語義了嗎?它爲什麼後來成了標準的東西,原始的IP語義是但願任意IP主機能夠僅依靠IP路由雙向互聯互通,結果有了狀態NAT之後,事情複雜了,因而更加複雜的各類打洞技術被呼喚出來,幾年前我曾經遇到過一我的,自誇他實現的打洞技術世界第一,我C-T-M-D,他就是一小丑!
相關文章
相關標籤/搜索