此文探讨部分报文无法通过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规则调用。
相关参考
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