開發與維運

部分報文無法通過自建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發出到公網。

問題分析

關於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數據流全圖。

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

Leave a Reply

Your email address will not be published. Required fields are marked *