天天看點

部分封包無法通過自建SNAT轉發到公網

此文探讨部分封包無法通過SNAT轉換IP位址的場景,探究conntrack/iptables處理封包和連接配接的方式,并分析了相關的源碼。

問題現象

使用ECS自建NAT網關,同VPC内其他ECS都通過此自建NAT網關ECS的SNAT功能通路公網。SNAT功能使用iptables實作,指令如下。

iptables -t nat -A POSTROUTING -j MASQUERADE

用戶端通路外網沒有問題,ping、curl等均正常,但是發現有一些封包,比如fin,reset等用戶端封包到達自建NAT網關後,NAT網關沒有進行NAT轉換,進而無法轉發到公網。

正常時候NAT網關抓包,192.168.100.105 經過SNAT轉換為192.168.100.104

15:33:24.179455 IP (tos 0x0, ttl 64, id 44608, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.105.1836 > 2.2.2.2.80: Flags [S], cksum 0x4b19 (correct), seq 1848868094, win 512, length 0 15:33:24.179478 IP (tos 0x0, ttl 63, id 44608, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.104.1836 > 2.2.2.2.80: Flags [S], cksum 0x4b1a (correct), seq 1848868094, win 512, length 0

異常時候NAT網關抓包,192.168.100.105 沒有經過SNAT轉換為192.168.100.104,而是直接從網卡發出去,發出去後依然會到達VPC網關查找路由,由于預設路由的存在,發現下一跳仍然是192.168.100.104,是以會導緻封包一直在NAT網關和VPC網關之間來回轉發,每轉發一次TTL減1,直到TTL減為0封包轉發停止。

15:30:34.320464 IP (tos 0x0, ttl 64, id 10270, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.105.1914 > 2.2.2.2.80: Flags [F], cksum 0x8c02 (correct), seq 1374184646, win 512, length 0 15:30:34.320490 IP (tos 0x0, ttl 63, id 10270, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.105.1914 > 2.2.2.2.80: Flags [F], cksum 0x8c02 (correct), seq 1374184646, win 512, length 0 15:30:34.320550 IP (tos 0x0, ttl 62, id 10270, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.105.1914 > 2.2.2.2.80: Flags [F], cksum 0x8c02 (correct), seq 1374184646, win 512, length 0 15:30:34.320553 IP (tos 0x0, ttl 61, id 10270, offset 0, flags [none], proto TCP (6), length 40) ...........

對TCP連接配接有了解同學都知道,TCP初始封包都是syn,然後進行三次握手,握手後進行資料互動,然後發送fin/reset來斷開連接配接。 那為什麼始發封包是fin或者reset封包的時候,iptables就無法進行nat轉換?

業務拓撲

VPC路由表裡面自定義路由條目0.0.0.0/0下一跳指向自建NAT網關的ECS 192.168.100.104。 由于192.168.100.105沒有公網IP,當通路公網的時候會走預設路由到192.168.100.104的自建NAT網關,自建NAT網關通過iptables規則将封包源位址轉換為192.168.100.104,然後從自己的EIP發出到公網。

部分封包無法通過自建SNAT轉發到公網

問題分析

關于netfilter和iptables

iptables是工作在使用者空間的程式,netfilter才是真正能夠實作防火牆的架構,netfilter 通過在TCP/IP核心協定棧中設定多個鈎子函數來達到對封包的處理,鈎子函數分别是NF_IP_PRE_ROUTING、NF_IP_LOCAL_IN、NF_IP_FORWARD、NF_IP_POST_ROUTING、NF_IP_LOCAL_OUT,對應的iptables鍊PREROUTING,INPUT,FORWARD,POSTING,OUTPUT。在netfilter官網的一篇名為《

ebtables/iptables interaction on a Linux-based bridge

》文檔中有詳細說明,下面這幅圖也是文章中提到的那幅netfilter資料流全圖。

部分封包無法通過自建SNAT轉發到公網

iptables TRACE跟蹤封包

通過添加Iptables trace檢視封包是否被iptables nat規則處理。

iptables -t raw -A PREROUTING -p tcp -d 2.2.2.2/32 -j TRACE

正常SNAT封包TRACE資訊

Feb 22 19:11:51 i-xxx kernel: TRACE: raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=00:16:3e:00:cd:08:ee:ff:ff:ff:ff:ff:08:00 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=64 I D=64863 PROTO=TCP SPT=1490 DPT=80 SEQ=97754196 ACK=1495478036 WINDOW=512 RES=0x00 SYN URGP=0 Feb 22 19:11:51 i-xxx kernel: TRACE: nat:PREROUTING:policy:1 IN=eth0 OUT= MAC=00:16:3e:00:cd:08:ee:ff:ff:ff:ff:ff:08:00 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=64 I D=64863 PROTO=TCP SPT=1490 DPT=80 SEQ=97754196 ACK=1495478036 WINDOW=512 RES=0x00 SYN URGP=0 Feb 22 19:11:51 i-xxx kernel: TRACE: filter:FORWARD:policy:1 IN=eth0 OUT=eth0 MAC=00:16:3e:00:cd:08:ee:ff:ff:ff:ff:ff:08:00 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL= 63 ID=64863 PROTO=TCP SPT=1490 DPT=80 SEQ=97754196 ACK=1495478036 WINDOW=512 RES=0x00 SYN URGP=0 Feb 22 19:11:51 i-xxx kernel: TRACE: nat:POSTROUTING:rule:1 IN= OUT=eth0 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=63 ID=64863 PROTO=TCP SPT=1490 DPT=80 SEQ=97754196 ACK=1495478036 WINDOW=512 RES=0x00 SYN URGP=0

未SNAT封包TRACE資訊

Feb 22 19:13:45 i-xxx kernel: TRACE: raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=00:16:3e:00:cd:08:ee:ff:ff:ff:ff:ff:08:00 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=59895 PROTO=TCP SPT=2652 DPT=80 SEQ=290944766 ACK=427016313 WINDOW=512 RES=0x00 FIN URGP=0 Feb 22 19:13:45 i-xxx kernel: TRACE: filter:FORWARD:policy:1 IN=eth0 OUT=eth0 MAC=00:16:3e:00:cd:08:ee:ff:ff:ff:ff:ff:08:00 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=63 ID=59895 PROTO=TCP SPT=2652 DPT=80 SEQ=290944766 ACK=427016313 WINDOW=512 RES=0x00 FIN URGP=0 Feb 22 19:13:45 i-xxx kernel: TRACE: raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=00:16:3e:00:cd:08:ee:ff:ff:ff:ff:ff:08:00 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=62 ID=59895 PROTO=TCP SPT=2652 DPT=80 SEQ=290944766 ACK=427016313 WINDOW=512 RES=0x00 FIN URGP=0 Feb 22 19:13:45 i-xxx kernel: TRACE: filter:FORWARD:policy:1 IN=eth0 OUT=eth0 MAC=00:16:3e:00:cd:08:ee:ff:ff:ff:ff:ff:08:00 SRC=192.168.100.105 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=61 ID=59895 PROTO=TCP SPT=2652 DPT=80 SEQ=290944766 ACK=427016313 WINDOW=512 RES=0x00 FIN URGP=0

通過對比正常和異常時候的iptables trace資訊結合上文提到的netfilter資料處理流程可以得出以下結論

1. 正常的封包經過POSTROUTING時候是經過NAT處理的,是以可以正确SNAT

2. 異常的時候封包沒有經過POSTROUTING的NAT規則處理,是以封包從網卡直接發了出去,TTL經過FORWARD鍊時候減一,然後經過VPC網關又發回來,直到TTL減到0終止轉發。

檢查conntrack狀态

通過netfilter處理流程圖可以看到,首先會經過PREROUTING的raw表處理,然後會被conntrack子產品記錄連接配接狀态,可以對比正常和異常時候封包的conntrack狀态看是否有線索。

正常SNAT封包conntrack狀态

tcp 6 108 SYN_SENT src=192.168.100.105 dst=2.2.2.2 sport=2685 dport=80 [UNREPLIED] src=2.2.2.2 dst=192.168.100.104 sport=80 dport=2685 mark=0 use=1

此時發現,異常時候是沒有任何fin封包的conntrack連接配接記錄的。

根因

由于iptables 的NAT功能是強依賴與conntrack的連接配接狀态,如果conntrack裡面沒有對應封包的連接配接記錄是無法進行所有NAT功能的,這就是為什麼fin/reset封包無法進行SNAT位址轉換,iptables trace資訊也無法看到封包進行nat規則處理。

驗證

如果一個封包經過conntrack處理後不産生conntrack記錄就無法進行NAT位址轉換,如果将正常連接配接的syn封包标記為NOTRACK,封包是否無法進行NAT位址轉換?

iptables添加指令

iptables -t raw -A PREROUTING -p tcp -d 2.2.2.2/32 -j NOTRACK

此時用戶端正常發起syn連接配接,也無法進行NAT轉換,conntrack表裡沒有對應的連接配接狀态。說明隻要conntrack裡面沒有對應的連接配接記錄是無法命中iptables nat規則的。

20:45:16.125969 IP (tos 0x0, ttl 64, id 41018, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.105.2221 > 2.2.2.2.80: Flags [S], cksum 0xc87a (correct), seq 1794239821, win 512, length 0 20:45:16.126036 IP (tos 0x0, ttl 63, id 41018, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.105.2221 > 2.2.2.2.80: Flags [S], cksum 0xc87a (correct), seq 1794239821, win 512, length 0 20:45:16.126102 IP (tos 0x0, ttl 62, id 41018, offset 0, flags [none], proto TCP (6), length 40) 192.168.100.105.2221 > 2.2.2.2.80: Flags [S], cksum 0xc87a (correct), seq 1794239821, win 512, length 0 .....

為什麼fin/reset封包無法記錄到對應的conntrack資訊?

conntrack中的幾種狀态

conntrack中對封包定義有4種,NEW,ESTABLISHED,RELATED,INVALID

  • NEW:一個連接配接的初始狀态(例如:TCP連接配接中,一個SYN包的到來),或者防火牆隻收到一個方向的流量(例如:防火牆在沒有收到回複包之前)。
  • ESTABLISHED:連接配接已經建立完成,換句話說防火牆已經看到了這條連接配接的雙向通信。
  • RELATED:這是一個關聯連接配接,是一個主連結的子連接配接,例如ftp的資料通道的連接配接。
  • INVALID:這是一個特殊的狀态,用于記錄那些沒有按照預期行為進行的連接配接

顯然,如果一個fin/reset封包到來後,肯定是屬于INVALID狀态的封包,這種狀态的封包經過conntrack之後并不會被丢棄,但也不會被conntrack記錄任何連接配接狀态。

代碼邏輯

封包經過nf_conntrack子產品處理時是從nf_conntrack_in function函數開始處理,以下是關于一個不存在的連接配接的第一個封包到達conntrack之後的處理邏輯

nf_conntrack_in @net/netfilter/nf_conntrack_core.c
    |--> resolve_normal_ct @net/netfilter/nf_conntrack_core.c // 利用__nf_conntrack_find_get查找對應的連接配接跟蹤表項,沒找到則init新的conntrack表項
        |--> init_conntrack @net/netfilter/nf_conntrack_core.c // 初始化conntrack表項
            |--> tcp_new @net/netfilter/nf_conntrack_proto_tcp.c // 到TCP協定的處理邏輯,called when a new connection for this protocol found。在這裡根據tcp_conntracks數組決定狀态。               

reslove_normal_ct

在reslove_normal_ct處理邏輯中,先使用__nf_conntrack_find_get檢視封包是否已經存在的連接配接狀态,如果新到的封包不存在連接配接狀态就使用init_conntrack來初始化新的連接配接記錄

/* look for tuple match */
  hash = hash_conntrack_raw(&tuple, zone);
  h = __nf_conntrack_find_get(net, zone, &tuple, hash);
  if (!h) {
    h = init_conntrack(net, tmpl, &tuple, l3proto, l4proto,
           skb, dataoff, hash);
    if (!h)
      return NULL;
    if (IS_ERR(h))
      return (void *)h;
  }               

init_conntrack

當新的封包到來之後,如果檢測沒有已存在的連接配接,那就會調用tcp_new來檢測。

if (!l4proto->new(ct, skb, dataoff, timeouts)) {
        nf_conntrack_free(ct);
        pr_debug("init conntrack: can't track with proto module\n");
        return NULL;
    }           

tcp_new

下面tcp_new處理代碼中,關鍵是擷取new_state的值,如果new_state的值大于或等于TCP_CONNTRACK_MAX,代碼邏輯會傳回false然後退出。對于FIN封包來說,new_state的值就是sIV。當代碼邏輯退出後就不會有對應的任何conntrack連接配接記錄産生。

/* Called when a new connection for this protocol found. */
static bool tcp_new(struct nf_conn *ct, const struct sk_buff *skb,
            unsigned int dataoff, unsigned int *timeouts)
{
    enum tcp_conntrack new_state;
    const struct tcphdr *th;
    struct tcphdr _tcph;
    struct net *net = nf_ct_net(ct);
    struct nf_tcp_net *tn = tcp_pernet(net);
    const struct ip_ct_tcp_state *sender = &ct->proto.tcp.seen[0];
    const struct ip_ct_tcp_state *receiver = &ct->proto.tcp.seen[1];

    th = skb_header_pointer(skb, dataoff, sizeof(_tcph), &_tcph);
    BUG_ON(th == NULL);

    /* Don't need lock here: this conntrack not in circulation yet */
    // 這裡get_conntrack_index拿到的是TCP_FIN_SET,是枚舉類型tcp_bit_set的值
    new_state = tcp_conntracks[0][get_conntrack_index(th)][TCP_CONNTRACK_NONE];

    /* Invalid: delete conntrack */
    if (new_state >= TCP_CONNTRACK_MAX) {
        pr_debug("nf_ct_tcp: invalid new deleting.\n");
        return false;
    }
......
}           

tcp_conntracks 是一個三維數組,存儲在TCP狀态轉換表裡。

  •  數組第一位為0,表示這個封包是始發封包,如果是響應封包則是1
  •  數組第二位Get_conntrack_index(th),Get_conntrack_index(th)是從tcp_bit_set 枚舉數組裡面擷取的值,如果是FIN封包則擷取的是TCP_FIN_SET為2
  • 數組第三位是TCP_CONNTRACK_NONE,這個值在枚舉數組tcp_conntrack裡面定義是0
/* What TCP flags are set from RST/SYN/FIN/ACK. */
enum tcp_bit_set {
TCP_SYN_SET,
TCP_SYNACK_SET,
TCP_FIN_SET,
TCP_ACK_SET,
TCP_RST_SET,
TCP_NON
}

enum tcp_conntrack {
	TCP_CONNTRACK_NONE, //0
	TCP_CONNTRACK_SYN_SENT,
	TCP_CONNTRACK_SYN_RECV,
	TCP_CONNTRACK_ESTABLISHED,
	TCP_CONNTRACK_FIN_WAIT,
	TCP_CONNTRACK_CLOSE_WAIT,
	TCP_CONNTRACK_LAST_ACK,
	TCP_CONNTRACK_TIME_WAIT,
	TCP_CONNTRACK_CLOSE,
	TCP_CONNTRACK_LISTEN,	/* obsolete */
#define TCP_CONNTRACK_SYN_SENT2	TCP_CONNTRACK_LISTEN
	TCP_CONNTRACK_MAX,//10
	TCP_CONNTRACK_IGNORE,
	TCP_CONNTRACK_RETRANS,
	TCP_CONNTRACK_UNACK,
	TCP_CONNTRACK_TIMEOUT_MAX
};           

tcp_conntracks Array

一個不存在連接配接的fin封包對應的new_state為

new_state = tcp_conntracks[0][get_conntrack_index(th)][TCP_CONNTRACK_NONE]=tcp_conntracks[0][2][0]=sIV

static const u8 tcp_conntracks[2][6][TCP_CONNTRACK_MAX] = {
    {
/* ORIGINAL */
/*syn*/       { sSS, sSS, sIG, sIG, sIG, sIG, sIG, sSS, sSS, sS2 },
/*synack*/ { sIV, sIV, sSR, sIV, sIV, sIV, sIV, sIV, sIV, sSR },
/*fin*/    { sIV, sIV, sFW, sFW, sLA, sLA, sLA, sTW, sCL, sIV },
/*ack*/       { sES, sIV, sES, sES, sCW, sCW, sTW, sTW, sCL, sIV },
/*rst*/    { sIV, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL },
/*none*/   { sIV, sIV, sIV, sIV, sIV, sIV, sIV, sIV, sIV, sIV }
    },
    {
/* REPLY */
/*syn*/       { sIV, sS2, sIV, sIV, sIV, sIV, sIV, sIV, sIV, sS2 },
/*synack*/ { sIV, sSR, sIG, sIG, sIG, sIG, sIG, sIG, sIG, sSR },
/*fin*/    { sIV, sIV, sFW, sFW, sLA, sLA, sLA, sTW, sCL, sIV },
/*ack*/       { sIV, sIG, sSR, sES, sCW, sCW, sTW, sTW, sCL, sIG },
/*rst*/    { sIV, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL },
/*none*/   { sIV, sIV, sIV, sIV, sIV, sIV, sIV, sIV, sIV, sIV }
    }
};           

在宏定義裡面定義了sIV 和TCP_CONNTRACK_MAX相等。

#define sIV TCP_CONNTRACK_MAX

在沒有任何連接配接狀态存在的情況下,當conntrack收到如下封包會被認為是invalid。

  • TCP狀态标志位包含FIN,tcp_conntracks[0][get_conntrack_index(th)][TCP_CONNTRACK_NONE]=tcp_conntracks[0][2][0]=sIV
  • TCP狀态标志位包含RST,tcp_conntracks[0][get_conntrack_index(th)][TCP_CONNTRACK_NONE]=tcp_conntracks[0][4][0]=sIV
  • TCP狀态标志位包含SYNACK,tcp_conntracks[0][get_conntrack_index(th)][TCP_CONNTRACK_NONE]=tcp_conntracks[0][1][0]=sIV
  • TCP狀态标志位不包含标志,tcp_conntracks[0][get_conntrack_index(th)][TCP_CONNTRACK_NONE]=tcp_conntracks[0][5][0]=sIV

iptables NAT 處理

net/ipv4/netfilter/iptable_nat.c 代碼nf_nat_ipv4_fn在進行NAT處理之前先判斷是否有對應的conntrack記錄資訊,如果沒有對應的記錄則直接傳回

nf_nat_ipv4_fn(unsigned int hooknum,

struct sk_buff *skb,

const struct net_device *in,

const struct net_device *out,

int (*okfn)(struct sk_buff *))

{

struct nf_conn *ct;

enum ip_conntrack_info ctinfo;

struct nf_conn_nat *nat;

/* maniptype == SRC for postrouting. */

enum nf_nat_manip_type maniptype = HOOK2MANIP(hooknum);

NF_CT_ASSERT(!ip_is_fragment(ip_hdr(skb)));

ct = nf_ct_get(skb, &ctinfo);

if (!ct)//如果沒有conntrack記錄,不進行NAT轉換直接傳回。

return NF_ACCEPT;

結論

當系統裡面啟用iptables之後,FIN/RST等封包到達系統後,conntrack會把這些封包标記為INVALID狀态,且不會建立任何conntrack連接配接記錄,由于沒有對應的連接配接記錄,是以也就無法進行任何iptables nat規則調用。

相關參考

https://www.alibabacloud.com/blog/tcp-connection-analysis-why-the-socket-remains-in-the-fin-wait-1-state-post-killing-the-process_595798 http://ebtables.netfilter.org/br_fw_ia/br_fw_ia.html https://elixir.bootlin.com/linux/v3.10/source/net/netfilter/nf_conntrack_proto_tcp.c#L96 http://people.netfilter.org/pablo/docs/login.pdf https://wiki.aalto.fi/download/attachments/69901948/netfilter-paper.pdf http://arthurchiao.art/blog/conntrack-design-and-implementation/#151-network-address-translation-nat

繼續閱讀