部分报文无法通过自建SNAT转发到公网-阿里云开发者社区

开发者社区> 云计算> 正文
登录阅读全文

部分报文无法通过自建SNAT转发到公网

简介: 此文探讨部分报文无法通过SNAT转换IP地址的场景,探究conntrack/iptables处理报文和连接的方式,并分析了相关的源码。 问题现象 使用ECS自建NAT网关,同VPC内其他ECS都通过此自建NAT网关ECS的SNAT功能访问公网。SNAT功能使用iptables实现,命令如下。 iptables -t nat -A POSTROUTING -j MASQUERADE 客户端访

此文探讨部分报文无法通过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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
云计算
使用钉钉扫一扫加入圈子
+ 订阅

时时分享云计算技术内容,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。

其他文章