在之前的几篇文章中,我们讨论了netfilter与iptables的实现原理与基本用法。在netfilter&iptables的各种使用场景中,nat是最常用也是最复杂的用法之一。许多常用的网络使用模式都是通过nat iptables规则实现的,例如docker默认的bridge网络模式。
本文将具体分析iptables中nat规则的实现方式,介绍报文经过nat规则实现地址转换的过程。并以docker bridge模式使用的nat规则为例,具体了解nat规则的实际用法。
问题
- nat规则有哪几种,分别实现什么功能,用于什么场景?
- nat规则通过哪些iptables匹配函数实现?
- nat规则相关的iptables匹配函数在哪些netfilter hook点上被处理?
- nat规则会对哪些网络报文生效?
- nat规则与conntrack功能有什么关系?两者如何协同完成一个连接上所有报文的地址转换?
- docker是如何通过iptables nat规则来实现bridge模式下容器的网络访问的?配置了哪些规则?
nat规则的用途和种类
nat即网络地址转换(network address translation),主要用于在内部网络地址和外部网络地址间互相转换,从而让使用内部网络地址的设备或软件能够访问外部网络,或者让外部网络可以访问到没有外部网络地址的内部设备。nat一般运行在网关/路由设备上,内部设备访问外网或外部网络访问内部设备的报文都需要经过网关转发,网关在内外网转发报文的过程中执行nat转换。
nat主要分为两类:
snat:源地址转换。用于内网设备访问外网时,将请求报文的源地址从内网设备的地址(例如172.17.0.11:1234)转换为网关的外网地址(例如115.133.54.24:12345)
dnat:目的地址转换。用于从外网访问内网设备时,将请求报文的目的地址从网关的外网地址(例如115.133.54.24:8000)转换为内网设备的地址(例如172.17.0.11:80)。
snat和dnat一般都只是指对建连或第一个请求报文的转换。一个内网向外网的请求被snat转发后,必然会收到外网的响应报文,这个报文的目的地址也需要转换,但是这个转换一般就不会被称作是dnat了。在linux内核中,一个连接只有第一个报文才会被snat/dnat处理,后续的报文都会被连接追踪(conntrack)模块处理,按第一个报文的地址转换方式做相同的转换。
iptables中可以在nat表上定义两类动作,SNAT和DNAT,分别提供源地址和目的地址转换功能。
SNAT
SNAT规则配置命令为:
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -j SNAT --to-source 115.133.54.24
上面的规则将来自172.17.0.0/16地址段的请求报文的源地址转换成了115.133.54.24。源地址(-s)和转换地址(--to-source)也可以指定端口或端口段,这时只有符合端口段的请求才会被转换,转换后的端口在--to-source指定的端口段中。
DNAT
DNAT规则配置命令为:
iptables -t nat -A PREROUTING -p tcp --dport 6789 -j DNAT --to-destination 172.17.0.3:9876
上面的规则将发往6789端口的tcp报文的目的地址转换成172.17.0.3:9876。
虽然SNAT和DNAT常用于内网设备访问外网,或者让内网设备向外暴露端口,但其功能并不局限在这些方面。SNAT和DNAT规则可以用于实现任意的源地址和目的地址转换,例如:
iptables -t nat -A PREROUTING -s 33.44.55.66 -j DNAT --to-destination 172.17.0.3
可以将来自33.44.55.66的报文的目的地址全部转换为172.17.0.3。
MASQUERADE
MASQUERADE规则是SNAT规则的一个特例,也用于转换报文的源地址。与SNAT不同的是MASQUERADE规则不用指定转换后的源地址,MASQUERADE会自动选择可用的源地址用于转换。
MASQUERADE规则配置命令为:
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -j MASQUERADE
上面的规则将来自172.17.0.0/16地址段的请求报文的源地址转换成可用于访问目标的本地IP。
MASQUERADE规则最大的用途是在一些外网IP不固定的网关上,可以自动使用当前IP作为SNAT的转换目标,而不需要随着外网IP的变化去重新配置SNAT规则。
NAT规则的iptables实现
内核的nat iptables在iptable_nat模块中实现,入口在net/ipv4/netfilter/iptable_nat.c中。iptable_nat模块中实现了PRE_ROUTING、POST_ROUTING、LOCAL_OUT、LOCAL_IN这4个hook点的hook函数iptable_nat_do_chain。这个hook函数的实现直接调用了ipt_do_table这个标准规则匹配函数,会根据当前链上的nat规则逐一匹配和处理。
然而在hook注册过程ipt_nat_register_lookups中,上述hook函数并没有被直接注册到netfilter的hook点上,而是被注册到了nf_nat_lookup_hook_priv.entries中。真正被注册到hook点上的是在net/netfilter/nf_nat_proto.c中定义的4个函数:nf_nat_ipv4_in、nf_nat_ipv4_out、nf_nat_ipv4_local_fn、nf_nat_ipv4_fn,分别对应上面的4个hook点。其中nf_nat_ipv4_fn函数中包含了主要的实现逻辑,其余3个hook函数通过调用nf_nat_ipv4_fn->nf_nat_inet_fn实现功能。
nf_nat_inet_fn的实现和conntrack高度相关。nat的netfilter hook函数优先级(-100/100)低于conntrack的hook函数(-200),因此报文skb匹配nat规则时已经经过了conntrack模块的匹配和处理。这个函数的大体逻辑流程如下:
可以看到,一条连接只有第一个报文会真正去匹配NAT规则,匹配后的NAT转换模式会记录到conntrack信息中,后续报文就会直接按这个转换模式去做NAT转换。
其中的NAT规则匹配函数就是iptable_nat中定义的hook函数iptable_nat_do_chain。每个hook点会执行对应的nf_nat_lookup_hook_priv.entries中的函数,理论上也可以注册其他处理函数到这个列表中。
上面流程中比较特别的地方是在报文没有匹配NAT规则时,仍然可能需要对连接地址进行转换。在nf_nat_alloc_null_binding中,会检查当前连接的五元组是否唯一,如果五元组已经被占用,即使没有匹配任何NAT规则,还是需要对连接进行地址转换。这种情况可能出现在连接1在本地绑定端口向外建连,同时连接2通过NAT连接了同一个目标地址,刚好又把本地端口映射到了连接1绑定的端口。这时就只能把连接1的端口做一个转换,否则就会无法建连。因此,没有配置NAT规则的连接也有可能被执行NAT转换,实际收发的报文可能和绑定的本地端口并不一致。当然这种情况发生的概率应该是极低的。
iptables规则匹配函数——ipt_do_table
所有iptables模块的规则匹配功能最终都会调用ipt_do_table这个函数来实现。函数的原型为:unsigned int ipt_do_table(struct sk_buff *skb, const struct nf_hook_state *state, struct xt_table *table)
其中第三个参数是一个xt_table。每个IPv4(IPv6也是类似的逻辑)netns都会维护一系列xt_table,包括iptable_filter、iptable_mangle、iptable_raw、nat_table等,iptable的四类规则表,就都维护在这4个xt_table结构中。
xt_table中的规则保存在table->private->entries中,每条规则构成一个ipt_entry。一个xt_table中保存了多个链的规则,通过private->hook_entry[state->hook]来获取某个链规则在entries中的起始位置。每条ipt_entry规则中保存了多个xt_entry_match,每个xt_entry_match保存了规则的一个匹配特征和相应的匹配函数。举例来说,如果我们在一条NAT规则中配置了IP段和端口范围两个匹配条件,那么这条规则相应的ipt_entry中就会包含两个xt_entry_match,分别用于匹配IP段和端口范围。如果skb匹配了一个ipt_entry中的所有xt_entry_match,那么当前报文就匹配了一条iptables规则。接着就会按ipt_entry规则对应的xt_entry_target中指定的target结果返回,或者按指定的target函数处理。
连接追踪——conntrack
关于连接追踪(conntrack),本身是一个复杂的连接信息处理模块,在linux内核中同样基于netfilter实现,模块名为nf_conntrack。conntrack最常见的用途是和iptable_nat模块共同完成连接地址转换功能。可以参考连接跟踪(conntrack):原理、应用及 Linux 内核实现了解更多conntrack相关信息。
nf_conntrack在PRE_ROUTING、POST_ROUTING、LOCAL_OUT、LOCAL_IN这四个hook点注册了hook函数,优先级都是-200。由于conntrack注册的hook点与NAT完全相同,而且优先级高于NAT,因此保证了报文skb在匹配NAT规则前一定已经经过conntrack的处理,包含conntrack信息,保存在sk_buff._nfct中。
conntrack为每条连接都维护了连接信息,在hook函数中为每个skb查找或新建对应的连接信息,并记录在skb中。之后执行到nat的hook函数时,就能根据skb中的连接信息,来获取对应的nat处理操作。如果是连接的首包,那么会根据nat规则的匹配结果,将nat处理操作记录到连接信息结构nf_conn中。
用一张图来总结一下conntrack与NAT的整体处理流程:
docker的bridge网络模式
NAT iptable的一个重要应用就是docker的bridge网络模式。在bridge模式下,每个docker容器都会被分配一个独立的netns和虚拟网络设备veth。容器网络和主机网络间通过主机上的虚拟bridge设备转发网络报文。一般来说,给容器分配的都是单个主机内部的本地IP,是无法直接与主机外的其他外部IP通信的。这时就需要在主机上为容器网络报文做NAT转换来支持容器与外部设备的访问。
通过在创建容器时配置几条简单的NAT iptable规则,docker就实现了容器网络与主机网络地址的转换和互通。
root@VM-4-17-debian:~# docker run -d -p 54321:12345 ubuntu sleep infinity
59eab71ecbf8e7931580a7873a677bfe2ae3c7945b0ac91e5d6dea95282343ab
root@VM-4-17-debian:~# iptables -t nat -L --line-numbers -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 308 9858 DOCKER all -- any any anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 MASQUERADE all -- any docker0 anywhere anywhere ADDRTYPE match src-type LOCAL
2 5 323 MASQUERADE all -- any !docker0 172.17.0.0/16 anywhere
3 0 0 MASQUERADE tcp -- any any 172.17.0.2 172.17.0.2 tcp dpt:12345
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER all -- any any anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 0 0 DNAT tcp -- any any anywhere anywhere tcp dpt:54321 to:172.17.0.2:12345
上面就是一个docker容器创建后,docker为其自动生成的NAT规则。需要注意的是上述规则是在关闭了docker-proxy后的配置。docker自带了一个docker-proxy服务,启动这个服务后会和iptables共同完成地址转换工作,会造成NAT规则实现更加复杂和难以理解。
容器访问外部网络
docker通过一条通用的MASQUERADE为所有容器提供SNAT转换,也就是上图中POSTROUTING链上的rule2。这条规则把所有来自容器网络地址段(172.17.0.0/16)访问外部IP的报文都进行SNAT转换,从而支持容器的外部网络访问。
外部节点访问容器
外部节点对容器的访问实现相对就要复杂一些了。首先,只有创建时配置了端口映射的容器才能被其他节点主动访问。对于这类容器,需要配置相应的DNAT规则,将访问宿主机端口的报文转换成访问容器地址/端口的报文后,再通过bridge转发到对应容器上。
可以看到,docker将DNAT规则配置在自定义的DOCKER规则链中,而通过PREROUTING和OUTPUT链的报文会匹配DOCKER规则。这样配置的原因是访问容器的报文不一定来自外部节点(POSTROUTING rule1),也可能是来自宿主机或本地其他容器(OUTPUT rule1),在两条链上都需要配置相同DNAT规则,因此docker将规则统一到DOCKER链中。DOCKER链的rule1就是DNAT规则。
可以看到NAT规则表在POSTROUTING链上还有两条规则。这两条规则都用于支持本地容器或宿主机通过映射的主机端口来访问容器的情况。当在宿主机上访问127.0.0.1:54321或者hostip:54321时,除了DNAT外,还需要rule1来将源IP转换成bridge IP。而当容器通过hostip:54321来访问本容器的主机映射端口时,就需要rule3来将源IP转换成bridge IP。
总结一下:通过hostip:54321访问容器时,都会使用PREROUTING rule1/OUTPUT rule1=>DOCKER rule1来进行DNAT转换。同时,如果是在宿主机上访问,还会通过POSTROUTING rule1来进行SNAT转换;如果是容器自身访问,还会通过POSTROUTING rule3来进行SNAT转换。
小结
本文具体介绍了iptables中NAT规则的使用和实现,以及其在docker容器网络中的实际用法。最后来看一下开始时的问题:
- nat规则有哪几种,分别实现什么功能,用于什么场景?
NAT规则有3种,其中SNAT和MASQUERADE规则都用于源地址转换,DNAT规则用于目的地址转换。 - nat规则通过哪些iptables匹配函数实现?
通过iptable_nat_do_chain匹配规则,通过nf_nat_inet_fn进行转换。 - nat规则相关的iptables匹配函数在哪些netfilter hook点上被处理?
在PRE_ROUTING、POST_ROUTING、LOCAL_OUT、LOCAL_IN这4个hook点上会调用nat相关的iptables函数。 - nat规则会对哪些网络报文生效?
只对每条连接的第一个报文匹配NAT规则,后续报文按第一个报文匹配的NAT规则指定的地址转换操作进行转换。 - nat规则与conntrack功能有什么关系?两者如何协同完成一个连接上所有报文的地址转换?
每条连接的第一个报文匹配NAT规则后,转换操作记录在连接的conntrack信息中,后续报文通过对应的conntrack信息完成地址转换。 - docker是如何通过iptables nat规则来实现bridge模式下容器的网络访问的?配置了哪些规则?
docker会配置MASQUERADE规则和DNAT规则来支持容器的对外访问和外部节点访问容器。