先别急着敲命令,我必须说一句大实话:iptables的NAT配置,核心难点从来不在命令本身,而在于你脑子里有没有一张完整的数据包流转图。SNAT和DNAT这两条规则看似就一行,但如果你没搞懂它们分别在哪个链上生效、改了包头之后数据包怎么回来,那线上出问题的时候,你大概率要抓瞎。这篇是《iptables防火墙规则》的第二篇,专门把SNAT、DNAT的原理和完整配置流程掰开揉碎讲清楚,从内核参数到模块加载,从规则写法到回程路由,最后附上我实际运维中踩过的坑和排查套路。无论你是刚接手公司网关的新手,还是想系统补一下NAT知识的老手,这篇都能给你一份能直接照着干的参考。
- 项目标题: "iptables防火墙规则(二):SNAT、DNAT策略原理及配置全流程"
- 相关热搜词: iptables, 防火墙规则, SNAT, DNAT
- 最新网络热词: iptables v1.8.9 (legacy): can't initialize iptables table `nat': table does not exist
1. 先搞清楚SNAT和DNAT到底在做什么
1.1 NAT的本质:改地址,而不是拦数据
很多人一想到iptables就觉得是"封IP、封端口",这其实只是它的filter表功能。防火墙的另一半江山——NAT(网络地址转换),干的事情完全不一样。
NAT的核心就一个字:改。数据包从你的网卡进来,路过Linux内核,iptables在特定的钩子点上把源IP或者目的IP改掉,再扔出去。为什么要改?因为IP地址不够用,也因为内外网隔离的安全需求。内网机器用的是192.168.x.x这种私网地址,这种地址在公网上是没法路由的,你家的路由器之所以能让手机、电脑同时上网,靠的就是NAT把私网地址换成了路由器公网口的那个IP。
理解NAT的关键在于:你改变的是数据包的"身份信息",而不是"内容"。数据包到了对端服务器,对端看到的源IP是转换后的公网IP,它回包的时候自然就往这个公网IP发。这时候你的Linux网关需要再次做逆向转换,把回包的源IP换成原来的私网IP,内网机器才能认出"这是刚才我请求的回复"。
整个NAT分为两种方向:一种是把内网机器发出的包,源地址改成公网IP,这叫SNAT;另一种是把外部发来的包,目的地址从公网IP改成内网机器的私网IP,这叫DNAT。一个管出,一个管进,两条方向刚好把内外网打通。
有人会问,那为啥不直接用公网IP给每台内网机器?IPv4地址就那43亿个,早就分完了,而且直接暴露内网机器到公网上,安全风险也大。NAT就是在这种现实约束下最实用的折中方案。后面所有配置,都围绕这两个方向展开。
1.2 SNAT:改变"我是谁"
SNAT全称是Source Network Address Translation,源地址转换。它解决的核心问题是:内网机器出去时,源IP是私网地址,回包找不到路,所以要把源IP替换成网关公网口IP。
我举个最常见的场景。公司内网是192.168.10.0/24网段,出口路由器(或者Linux网关)的eth0接公网IP 203.0.113.5,eth1接内网192.168.10.1。内网一台机器192.168.10.100要访问公网上的1.1.1.1,数据包到了Linux网关,如果不做任何处理,网关把包转发出去之后,1.1.1.1看到源IP是192.168.10.100,回包的目标地址也是192.168.10.100。可问题是,公网上的路由器根本不认192.168.10.0/24这个私网网段,回包在某个公网路由器上就被丢弃了,内网机器永远等不到回应。
SNAT就是解决这个问题的。数据包从内网进来,在POSTROUTING链上,iptables把源IP从192.168.10.100改成203.0.113.5,然后包才从eth0发出去。1.1.1.1回包时目标IP是203.0.113.5,包回到Linux网关后,网关查连接跟踪表,发现这个回包对应之前那个被SNAT的原始连接,于是把目的IP从203.0.113.5改回192.168.10.100,再通过eth1送回去。
注意,这里有个关键点:SNAT通常只在POSTROUTING链上做。因为POSTROUTING是路由决策之后、数据包即将离开本机的最后一个钩子点,此时内核已经决定了这个包要从哪个网卡出去,也已经查过路由表了。在POSTROUTING改源IP,能保证出网卡前是最终形态。如果你在PREROUTING或者INPUT链上做SNAT,要么包还没路由、改了也没意义,要么包是发给本机的、根本不该改。
1.3 DNAT:改变"我去哪"
DNAT全称是Destination Network Address Translation,目的地址转换。它解决的核心问题是:外部用户访问你公网IP上的某个服务端口时,把请求转发给内网的某台服务器。
还是举例子。公司有一台公网服务器203.0.113.5,上面跑着iptables,内网有一台Web服务器192.168.10.100,跑着Nginx监听80端口。外部用户想访问公司官网,他访问的是http://203.0.113.5。数据包到达Linux网关,如果不做DNAT,内核看这个包的目的IP是203.0.113.5,恰好是本机地址,于是把包交给INPUT链,交给本机的用户态进程——但本机没有跑Nginx,请求就失败了。
DNAT的做法是:数据包从公网口进来,在PREROUTING链上,iptables把目的IP从203.0.113.5改成192.168.10.100,目的端口改成80。改完之后内核重新做路由决策,发现目标192.168.10.100不在本网段,于是走FORWARD链转发到内网。内网Web服务器处理完请求后回包,源IP是192.168.10.100,目的IP是外部用户地址。这个包经过网关时,网关根据连接跟踪表做逆转换,把源IP改回203.0.113.5,用户那边看到的就是"203.0.113.5给我回包了",整个过程完全是透明的。
跟SNAT相反,DNAT必须在PREROUTING链上做。因为PREROUTING是数据包进入本机后的第一个钩子点,此时还没做路由决策,你还有机会改目的地址来影响路由选择。如果你在POSTROUTING做DNAT,路由早就定完了,改了目的地址也无济于事,包都不知道往哪送。
这里顺便说一个新手最容易混的点:DNAT只改数据包的"去程"——外到内的方向。至于内网服务器的回包怎么回到外部用户,那是系统根据连接跟踪记录自动完成的,不需要你在规则里再写一条"反向SNAT"。这是iptables的conntrack机制自动处理的,你把这一点理解透了,后面配置就不会晕。
2. 动手前的准备:内核参数、模块、表和链的关系
2.1 网络拓扑与需求梳理
配置NAT之前,先别急着写规则,把你的网络拓扑画清楚。我见过太多人上来就抄命令,结果把内网口和外网口搞反了,规则一加,整个网络直接断掉。
咱们用一个最典型的双网卡Linux网关拓扑来走完全程:
- eth0:公网口,IP为203.0.113.5/24,连接运营商
- eth1:内网口,IP为192.168.10.1/24,连接公司内网交换机
- 内网服务器A:192.168.10.100/24,跑Nginx 80端口,网关指向192.168.10.1
- 内网服务器B:192.168.10.200/24,跑MySQL 3306端口,网关指向192.168.10.1
- 内网普通PC:192.168.10.50/24,需要正常上外网
需求有两个: 第一,内网所有机器(服务器A、B和PC)都能访问外网,这需要SNAT; 第二,外部用户能访问内网服务器A的80端口,以及内网服务器B的3306端口,这需要DNAT。
为什么强调先把拓扑画出来?因为SNAT和DNAT的规则都跟接口强相关,你必须清楚哪个网卡是"进来的方向",哪个网卡是"出去的方向"。拿这个拓扑来说,内网流量从eth1进来,从eth0出去,所以SNAT要看eth0的IP,DNAT规则要挂在eth0的入方向。
画完拓扑,还要理清一个概念:NAT规则和你公司里有没有"默认拒绝"的filter规则是两码事。NAT表只管改地址,filter表才管放行还是丢弃。如果你在filter表的FORWARD链上默认DROP了,那就算NAT规则配得再对,数据包也过不去。后面我会专门讲这个联动问题。
2.2 开启内核转发:NAT生效的前提
Linux默认情况下,一个网卡收到的数据包如果目的地址不是本机,内核会直接丢弃,不会把它从另一个网卡转发出去。这背后是内核参数net.ipv4.ip_forward在控制。
你要做的第一件事就是打开转发。临时生效的做法:
sysctl -w net.ipv4.ip_forward=1永久生效的做法,是把配置写进/etc/sysctl.conf,然后执行sysctl -p加载:
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf sysctl -p确认是否生效:
sysctl net.ipv4.ip_forward输出net.ipv4.ip_forward = 1就说明OK了。
顺带提一嘴,如果你用Docker或者K8s,它们其实也依赖这个转发开关。有时候你发现容器跨主机通信不通,查了半天iptables规则没问题,结果发现ip_forward被关了,这种情况我碰到过好多次。
为什么必须开转发?因为NAT的本质是"中间人",数据包不是给网关自己的,而是要穿过去。转发开关就是Linux能不能当路由器的总闸门。SNAT和DNAT本质上都是为转发服务的,你总不能让内网包到了网关就停住吧。
2.3 检查nat表和内核模块:先解决"table does not exist"
这里必须重点讲一下热词里提到的那个报错:iptables v1.8.9 (legacy): can't initialize iptables table 'nat': table does not exist。
这个报错是什么意思?字面意思是找不到nat表。但我明确告诉你,这不是iptables命令的问题,而是内核里压根没有加载nat相关的模块,或者当前内核不支持。
正常情况下,iptables有5张表:raw、mangle、nat、filter、security。其中filter表是默认就有的,只要内核编译了iptables支持就能用。但nat表不一样,它依赖独立的模块,最常见的是iptable_nat和nf_nat。
在较老的内核版本上,你直接跑iptables -t nat -L,可能就会遇到这个报错。解决办法就是先把模块加载进去:
modprobe iptable_nat modprobe nf_nat如果模块存在,加载之后就不会报错了。但问题往往没这么简单。还有几种常见情况:
第一,模块压根没编译进内核。你可以在Linux内核源码配置里搜CONFIG_IP_NF_NAT,凡是看到这个配置项没选上,或者编译成了模块但没有安装,就会出现nat表不存在的报错。
第二,iptables的版本和内核不配套。比如你系统里装的是iptables v1.8.9的legacy版本,它走的是旧的getsockopt接口跟内核通信,如果内核不支持,就会报这个错。这时候你可能要考虑用nftables后端,或者更新内核。
第三,容器环境。你在Docker容器里跑iptables命令,如果容器没有加载宿主机的iptables模块,或者没有相关的内核权限,也会报类似错误。这种情况一般在宿主机上操作,而不是在容器里。
我实际操作中,最省事的判断方法是先看模块:
lsmod | grep nat如果有输出,说明模块已经加载了。如果没有,就执行modprobe。如果加载报错,那就去查内核版本和模块路径,看是不是模块文件不存在。
其次可以检查内核是否支持netfilter的nat功能:
dmesg | grep -i nat有时候报错信息会直接告诉你哪个模块没找到。
在这里提醒你一个细节:如果你的机器是云服务器,比如阿里云、腾讯云,你买的机器默认就是带公网映射的,你再用iptables做SNAT,可能会跟云平台的NAT网关冲突,甚至产生一些奇怪的现象。云服务器上配置NAT之前,先确认你用的是"经典网络"还是"VPC",VPC环境下,云厂商一般不建议你自己在实例里做SNAT,因为外网流量已经经过了一层映射。
2.4 四表五链:一张图理清数据包的完整旅程
NAT配置之前,必须把iptables的整体框架捋清楚,尤其是跟NAT相关的链。
iptables的hook点(也就是链)一共有5个:PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING。数据包每经过一个hook点,就会去查对应表里挂在这个链上的规则。
跟NAT有关的主要用途如下:
- PREROUTING:数据包进入本机,路由决策之前。这个位置主要做DNAT。
- POSTROUTING:数据包即将离开本机,路由决策之后。这个位置主要做SNAT和MASQUERADE。
- OUTPUT:本机自己产生的数据包出去时经过。本机进程访问外部时也可以做NAT,但日常用得少。
那么数据包穿越这台Linux网关的完整旅程是这样的:
外部用户访问203.0.113.5:80的包进来,到达eth0,进入PREROUTING链,查到DNAT规则,目的IP被改成192.168.10.100。然后内核做路由决策,发现目标在内网网段,包被送到FORWARD链——注意,这里要经过filter表的检查,如果FORWARD链拒绝,包就没了。通过检查后,包到达POSTROUTING链,出eth1,进入内网。
反过来,内网机器访问外网的包到达eth1,进入PREROUTING,这里一般没DNAT规则,所以不处理。路由决策后发现目标在公网,包走FORWARD链,再到POSTROUTING链,在这里做SNAT,源IP被改成203.0.113.5,然后出eth0。
以上两次旅程,你只要记住一句话:去程在PREROUTING改目的的是DNAT,去程在POSTROUTING改源的是SNAT,回程全靠conntrack自动逆转换。
有些人配置防火墙时喜欢把规则写在INPUT链上,对于DNAT场景这是不对的。外部包要转发到内网服务器,它不该进INPUT链,INPUT链是给"发往本机"的包用的。但凡你发现DNAT之后包过不去,先用这条思路查一下包的走向,八成能定位问题。
3. SNAT配置全流程:内网出网的两种姿势
3.1 静态公网IP场景:用SNAT精确指定出口IP
当你的公网口IP是固定的静态IP时,用SNAT目标最合适。SNAT的写法需要明确指定转换后的源IP是什么。
基本语法:
iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o eth0 -j SNAT --to-source 203.0.113.5这条规则的意思:凡是源地址属于192.168.10.0/24网段的、且从eth0出去的包,在POSTROUTING链上把源IP改成203.0.113.5。
拆开讲解各个参数:
-t nat:指定操作nat表。-A POSTROUTING:追加规则到POSTROUTING链。-s 192.168.10.0/24:匹配源IP网段。-o eth0:匹配出接口为eth0。-j SNAT:动作是源地址转换。--to-source 203.0.113.5:把源IP改成的目标地址。
这里我强调一下匹配条件的设计逻辑。-s和-o是最常见的条件组合,目的是只转换"从内网来、往公网去"的流量,不要误伤其他流量。如果你不加-s,那么从eth0收到的、又从eth0发出去的包也会被SNAT,这显然不对。如果你不加-o,那内网口出去的包也可能被转换,也会出问题。
有人会问,为什么不用-i eth1来匹配入接口?其实也行,但对于SNAT来说,POSTROUTING链是路由之后的环节,用出接口-o判断更加准确。比如有一台内网机器同时连着两个网段,数据包从哪个口进来不代表它一定从哪个口出去,出口才是最终决定源IP的要素。
配置完成后,验证一下规则:
iptables -t nat -L POSTROUTING -n -v输出类似:
Chain POSTROUTING (policy ACCEPT 4 packets, 336 bytes) pkts bytes target prot opt in out source destination 0 0 SNAT all -- * eth0 192.168.10.0/24 0.0.0.0/0 to:203.0.113.5pkts那一列如果一直是0,那就说明规则没匹配到流量,需要去查包是不是没走到这条链。
再来个实际验证方法:内网机器访问外网,然后在网关上看连接跟踪:
conntrack -L | grep 192.168.10你会看到类似这样的记录:
tcp 6 431999 ESTABLISHED src=192.168.10.50 dst=1.1.1.1 sport=52340 dport=80 src=203.0.113.5 dst=1.1.1.1 sport=52340 dport=80 [ASSURED] mark=0 use=1注意看后半段,src=203.0.113.5,这就是SNAT之后的源地址。能看到这行说明转换生效了。
3.2 动态IP或拨号场景:用MASQUERADE自动适配出口IP
如果你的公网IP不是固定的,比如ADSL拨号上网、或者云服务器的弹性IP绑在别的设备上、或者你的出口通过DHCP动态获取IP,那SNAT就不太合适了。因为你每次拨号或者IP变更后,规则里写死的那条--to-source就失效了。
MASQUERADE就是为了这种场景设计的。它的写法:
iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o eth0 -j MASQUERADE注意到区别了吗?MASQUERADE后面没有--to-source参数。它的原理是:自动取eth0网卡当前配置的IP作为转换源地址。也就是说,不管eth0的IP怎么变,只要网卡上有这个IP,MASQUERADE就会自动用它。
这里我分享一个经验:十年前我维护过一个ADSL拨号上网的小型办公室网关,当时用的就是MASQUERADE。后来运营商更换线路,公网IP从113.x.x.x变成了222.x.x.x,我什么都没改,网络照样正常。如果当时用的是SNAT写死了旧IP,那整个办公室就断网了。
不过MASQUERADE也有代价:每次处理包都要动态获取网卡IP,性能比SNAT稍差一点。对于小型网络完全不是问题,但对于每秒处理几十万连接的大型网关,性能差距就要纳入考虑了。所以能用固定IP的时候,优先用SNAT;动态IP的时候才用MASQUERADE,这是最佳实践。
还有一点,MASQUERADE天然支持多IP的出口。比如eth0上配置了多个IP地址,MASQUERADE会自动选择合适的一个作为源IP,而SNAT需要手动管理IP池。
3.3 回程包怎么处理?conntrack的自动逆向转换
配置SNAT时,新手最担心的一个问题:“我只改了出去的包源IP,回来的包怎么改回去?”
答案是:你不用管,Linux内核的连接跟踪系统(conntrack)自动处理。
当第一个内网包经过SNAT出去时,iptables会在连接跟踪表里记下一条记录,内容包括:原始连接的源IP、目的IP、端口,以及转换后的源IP。后续这个TCP连接的所有回包,内核都会根据这条记录做逆转换。
你可以做个实验。在网关上看:
cat /proc/net/nf_conntrack或者用conntrack工具:
conntrack -L你会发现,每条连接都记录着双向的地址映射。这就是NAT能透明工作的核心。
不过这里有一个需要特别注意的场景:如果内网机器先访问了网关自己的公网IP上的某个服务,比如你已经配置了DNAT让外部访问203.0.113.5:8080转发到内网Web服务器,然后内网一台机器也想通过公网IP访问这个Web服务。这种情况下,数据包从内网进来,DNAT会把目的IP改成内网服务器的IP,同时如果配置了SNAT,还需要把源IP改成网关内网口的IP。很多老手管这个叫"hairpin NAT"或"NAT回环",配置起来稍微有点绕,但原理还是那套:去程在PREROUTING改目的、在POSTROUTING改源,回程自动逆转换。
如果遇到内网访问自己的DNAT映射不生效,不要慌,大概率是没给回包做SNAT导致的。解决方法是在POSTROUTING加一条针对内网网段的SNAT规则,或者配置特殊的hairpin规则。
3.4 SNAT常见错误:源地址网段写错、出口选错、filter链拦截
配置SNAT的过程中,有三个坑我反复踩过,这里集中说一下。
第一个坑:源IP网段写错或漏写。如果你漏了-s参数,SNAT会对所有从eth0出去的包生效,包括从公网进来的、再公网出去的转发流量。这样会导致外部正常访问你公网IP时,源IP被改掉,连接全部异常。我见过有人把整台网关搞到连ping都ping不通,排查半天发现是SNAT把进来的ICMP都改了源地址。
第二个坑:出口接口选错。SNAT的-o指定的是"数据包将从哪个接口出去"。如果你把-o eth0写成了-o eth1,而eth1是内网口,那内网流量不满足出接口条件,SNAT根本不生效。
第三个坑:filter表的FORWARD链拦截。SNAT做完地址转换后,数据包还是要走FORWARD链的。如果你的FORWARD链策略是DROP,内网流量根本到不了POSTROUTING,SNAT自然没机会工作。解决方案是保证FORWARD链上有合适的放行规则,比如:
iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT这两条规则放行了内网到公网的新连接,以及外部回包的相关连接。实际生产中,你还要在上述规则基础上,结合自身需求增加更细粒度的访问控制,不能全部无条件放行。
4. DNAT配置全流程:将外部流量精准导入内网
4.1 端口映射的最简DNAT规则
外部访问内网服务器,最常用的就是端口映射,也就是把公网IP的某个端口映射到内网某台服务器的某个端口。
以刚才拓扑里的需求为例,我们需要把203.0.113.5的80端口映射到192.168.10.100的80端口:
iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p tcp --dport 80 -j DNAT --to-destination 192.168.10.100:80规则拆解:
-t nat:指定nat表。-A PREROUTING:追加到PREROUTING链。-i eth0:匹配入接口为eth0,也就是公网口。-d 203.0.113.5:匹配目的IP为公网IP。-p tcp --dport 80:匹配TCP协议且目的端口为80。-j DNAT:动作是目的地址转换。--to-destination 192.168.10.100:80:把目的地址改成内网IP和端口。
端口不同的情况也很常见。比如公网IP的8080端口要映射到内网的80端口:
iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p tcp --dport 8080 -j DNAT --to-destination 192.168.10.100:80端口映射的原理非常好理解,就是改目的端口。实际运维中,DNS、邮件、Web是最常见的映射类型,DNS需要同时映射UDP和TCP:
iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p udp --dport 53 -j DNAT --to-destination 192.168.10.200:53 iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p tcp --dport 53 -j DNAT --to-destination 192.168.10.200:534.2 DNAT配置的完整流程和验证方法
只配置DNAT规则还远远不够。你需要在filter表的FORWARD链上放行对应的流量。
为什么?因为PREROUTING改完目的地址后,路由决策发现目标在内网,包要经过FORWARD链。如果FORWARD链默认ACCEPT,那没问题;如果默认DROP,包就死在半路了。
所以完整配置应该是这样:
# 1. 添加DNAT规则 iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p tcp --dport 80 -j DNAT --to-destination 192.168.10.100:80 # 2. 放行转发流量 iptables -A FORWARD -i eth0 -o eth1 -d 192.168.10.100 -p tcp --dport 80 -j ACCEPT iptables -A FORWARD -i eth1 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT第二条FORWARD规则是什么意思?它放行了从公网口进来、去往内网Web服务器的TCP 80流量。第三条放行了回程流量(已经建立的连接和相关的连接)。
我见过很多初学者只配了DNAT忘了配FORWARD,结果外网怎么都访问不了,内网自己访问却正常,一查才发现是FORWARD链把包丢了。记住这个顺序:NAT规则负责改地址,FORWARD规则负责放行,两者缺一不可。
验证方法也很直观。在外部电脑上访问http://203.0.113.5,如果页面正常打开,说明映射成功。如果打不开,按以下顺序排查:
- 在网关上看DNAT规则是否匹配到包:
iptables -t nat -L PREROUTING -n -v - 看FORWARD链是否匹配到包:
iptables -L FORWARD -n -v - 看连接跟踪:
conntrack -L | grep 203.0.113.5 - 抓包确认:
tcpdump -i eth1 port 80
tcpdump这一步很关键,如果eth1上能看到去往192.168.10.100的流量,说明DNAT和转发都正常;如果只有eth0上有流量而eth1上没有,那就是路由或FORWARD的问题。
4.3 进来容易回去难:反向SNAT是怎么自动完成的
DNAT引发的最常见困惑是:“DNAT只改了来包的目的地址,那内网服务器回包的时候,源地址是192.168.10.100,外部用户怎么认得出这是203.0.113.5回给它的?”
答案是:内核的conntrack会在连接跟踪表里记录这一条映射关系。当内网服务器回包经过网关时,内核自动查询对应的连接跟踪条目,把包的源IP从192.168.10.100改回203.0.113.5,然后才从eth0发出去。
整个过程,内网服务器完全感知不到自己是被映射的,它只知道有客户端在访问它的80端口。外部用户也感知不到内网服务器的存在,他只看到自己在和203.0.113.5通信。
但是,这里有一个非常容易踩的坑:如果你的内网服务器有多个网卡,或者它的路由设置有问题,回包没有原路返回——也就是说,内网服务器不是把回包发给网关(192.168.10.1),而是直接发给了别的网关——那么conntrack的逆转换就无从生效。这在物理网络的复杂拓扑里有可能会碰到。解决办法是在内网服务器上加一条默认路由指向网关,或者在内网服务器上配置策略路由。
我在实际项目里还遇到过一个更隐蔽的问题:内网Web服务器返回的数据包比较大,TCP分段后,某些分片在回程时没能原路回到网关,导致外部用户连接非常慢。最终查到是内网交换机开启了某种流量负载均衡策略,导致回包路径不稳定。这个问题排查起来很费劲,但从原理上理解,回包必须走同一台网关,这是DNAT能正常工作的前提。
4.4 实战扩展:一对一映射、多端口映射和端口段转发
DNAT不只支持IP加端口的映射,还可以做很多变体。
一对一映射,就是把整个公网IP映射到一台内网服务器,所有端口都转发。写法:
iptables -t nat -A PREROUTING -d 203.0.113.5 -j DNAT --to-destination 192.168.10.100注意这条规则没有指定协议和端口,这个公网IP上所有流量都会被转到192.168.10.100。这种场景一般用在需要对外提供多个服务的服务器上,公网IP正好比较充裕。
多端口映射,可以用--dport加逗号分隔端口列表:
iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p tcp -m multiport --dports 80,443,8080 -j DNAT --to-destination 192.168.10.100端口段转发,可以用冒号表示范围:
iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p tcp --dport 1000:2000 -j DNAT --to-destination 192.168.10.100这条规则会把1000到2000的所有TCP端口都转发到192.168.10.100的相同端口。
还有更灵活的玩法,把公网IP的不同端口映射到内网不同端口:
iptables -t nat -A PREROUTING -d 203.0.113.5 -p tcp --dport 8080 -j DNAT --to-destination 192.168.10.100:80 iptables -t nat -A PREROUTING -d 203.0.113.5 -p tcp --dport 8443 -j DNAT --to-destination 192.168.10.200:443上面这种配置在企业里很常见,用有限的公网IP开多个服务,分别转发到内网不同服务器上。不过要注意,做这种映射时,内网服务器的防火墙也要放行对应端口,否则即使网关转发成功了,内网服务器的本地防火墙也会把包拦掉。
还有一种场景是内外网端口一致但IP不同,比如把203.0.113.5:22映射到192.168.10.100:22,实现SSH跳板。这个我就不展开写了,上面的原理完全覆盖。
5. 结果持久化:重启不丢,这才是完整流程
每次手动执行iptables命令添加的规则,重启后都会全部消失。大部分生产环境都有一个要求:重启之后防火墙规则自动恢复。
把iptables规则保存下来的方法,在主流发行版上不太一样。
CentOS/RHEL 6及以前,用:
service iptables save它会把规则保存到/etc/sysconfig/iptables,重启后自动加载。
CentOS/RHEL 7及以后,默认用firewalld了。如果你还是坚持用iptables,需要先安装iptables-services,然后:
systemctl enable iptables systemctl start iptables iptables-save > /etc/sysconfig/iptablesUbuntu/Debian系统,用:
iptables-save > /etc/iptables/rules.v4 ip6tables-save > /etc/iptables/rules.v6然后再装一个iptables-persistent包,重启时就会自动加载。安装的时候它会提示你是否保存当前规则,选择是就可以了。
我个人的习惯是:不管用哪种系统,都倾向于自己写一个shell脚本,把防火墙规则全部整理进去,然后放到开机启动项里。好处是规则清晰、有注释、可审计、可版本控制。坏处是需要多写点代码。举个例子,就算你重启了系统,顺手执行一下/root/firewall.sh就能恢复全部规则,这比依赖系统的服务管理更可控。
这里我必须强调规则顺序。iptables规则是顺序匹配的,先匹配到的规则生效,后面相同的匹配不会再执行。因此保存规则时,要注意把“放行已建立连接”这类规则放在前面,把“默认拒绝”放在最后,避免放行规则被拒绝规则挡在后面无法生效。
6. 常见问题与排查技巧实录
6.1 nat表不存在,怎么处理
回到开头那个热词报错:iptables v1.8.9 (legacy): can't initialize iptables table 'nat': table does not exist。
这个问题在较老的系统或最小化安装的容器里特别常见。排查步骤我按顺序列一下:
# 1. 检查内核是否支持nat grep -i nat /boot/config-$(uname -r) | grep -i module # 2. 加载相关模块 modprobe iptable_nat modprobe nf_nat modprobe nf_conntrack modprobe nf_conntrack_ipv4有些旧内核还需要加载nf_nat_ipv4之类的模块。加载之后再次执行:
iptables -t nat -L -n如果还是报错,试着看看当前使用的iptables后端:
iptables --version如果显示legacy,而系统内核较新,可以尝试改用nftables后端。很多发行版已经把iptables作为nftables的兼容层实现了,命令不变但底层不同。
如果是容器环境里报这个错,那就是容器缺模块或权限问题,基本只能回到宿主机上操作或者给容器加特权。
6.2 SNAT不生效,内网机器上不了网
内网机器上不了网,排查思路要按数据包的流向一层层来。
先在网关上看规则有没有命中:
iptables -t nat -L POSTROUTING -n -v如果pkts计数为0,说明包根本没走到这条链。这时候多半是路由或FORWARD的问题。再看:
iptables -L FORWARD -n -v如果FORWARD也没有计数,那问题就在前面的环节,比如内网机器根本没把网关当作默认路由。这一点很容易忽略,查一下内网机器的路由表:
ip route确认默认网关是不是192.168.10.1。
如果FORWARD有计数但SNAT没有,那就是SNAT规则的条件没匹配上,常见原因就是-s网段写错,或者-o接口选错。再核查一下内网网段是不是192.168.10.0/24,出口是不是eth0。
如果SNAT有计数但内网还是上不了网,那就要抓包看回包了。在网关执行:
tcpdump -i eth0 icmp然后让内网机器ping一个公网IP。如果看到请求包出去但没有回包,说明公网回包没到达网关,可能是NAT后的源IP被运营商封了,或者回包路由异常。如果看到回包了但内网机器ping不到,那就是conntrack或逆向转换的问题,重启conntrack工具或者查看连接跟踪是否异常。
6.3 DNAT生效但外部访问不了
DNAT不生效,常见原因有四个,我按优先级排序:
第一,FORWARD链拦截。这是最高频的原因。检查:
iptables -L FORWARD -n -v如果看到DNAT后的目标IP的包有计数但下一跳是DROP,那就需要调整FORWARD策略。
第二,目标服务器自身的防火墙拦截。特别是云服务器或者带ufw的系统,本地防火墙会挡住转发进来的流量。检查目标服务器:
ufw status iptables -L INPUT -n -v第三,DNAT规则的条件写得太严格。比如-d 203.0.113.5写成了别的IP,或者--dport端口与实际访问端口不一致。仔细回顾规则条件。
第四,回程路由异常。内网服务器回包没走网关,直接丢包。检查内网服务器路由表,看默认路由是否是网关IP。
我列一张速查表方便你排查:
| 现象 | 排查点 | 常见解决方案 |
|---|---|---|
| PREROUTING pkts为0 | 外部包没到eth0、目的IP不匹配 | 检查防火墙前端设备、确认公网IP |
| FORWARD pkts为0 | filter表未放行 | 添加FORWARD放行规则 |
| FORWARD pkts有但DNAT未计数 | 路由决策走了INPUT | 确认目的IP不是本机、DNAT挂在PREROUTING |
| 内网服务器收不到包 | 回程/目标路由异常 | 检查服务器路由、目标服务器防火墙 |
6.4 规则顺序和连接跟踪的坑
iptables N#AT还有一个隐性坑,那就是连接跟踪表溢出。当网关同时承载大量NAT连接时,/proc/sys/net/netfilter/nf_conntrack_max如果设置得不够大,会出现丢包,表现为内网机器偶尔能上网、偶尔完全断网,非常诡异。
检查连接跟踪表使用率:
sysctl net.netfilter.nf_conntrack_count sysctl net.netfilter.nf_conntrack_max如果count接近max,就加大上限:
sysctl -w net.netfilter.nf_conntrack_max=655350这个参数要根据你的网关内存大小来调整,一条连接跟踪记录大约占几百字节内存,别调太大。
另外一个坑是规则顺序。举个例子,如果你有一条默认拒绝的FORWARD策略,又加了一条放行规则,但放行规则在拒绝规则后面,那放行规则永远不会生效。iptables是顺序执行的,先匹配先执行,匹配了就不再往下走。所以放行规则要尽量靠前,默认拒绝的规则要靠后。
还有,NAT规则和filter规则的顺序不要混为一谈。NAT表只在特定的hook点被查询,而filter表在INPUT/FORWARD/OUTPUT链被查询,它们是独立的表,顺序上不存在干扰。你只需要保证各自表内的规则顺序符合预期即可。
7. 写规则时最容易忽略的几个细节
配置NAT规则看起来就是一条命令的事,但真正在生产环境里跑起来,有几个细节会让你的规则更健壮。我挑几个平时文档里很少见的点说一下。
第一,规则一定要加注释。这是好习惯,但对团队协作特别重要。iptables命令本身支持-m comment --comment "xxx":
iptables -t nat -A PREROUTING -i eth0 -d 203.0.113.5 -p tcp --dport 80 -j DNAT --to-destination 192.168.10.100:80 -m comment --comment "web映射"当你几个月后再看规则列表,注释能帮你节省大量回忆时间。
第二,清空规则时要精确。很多人在调试时直接执行iptables -t nat -F,这个命令会把nat表所有链的规则全部清空。如果你的机器正在生产中,这会瞬间断掉所有NAT。更安全的做法是只清空某条链,或者删除指定的规则。比如删除一条规则:
iptables -t nat -D POSTROUTING -s 192.168.10.0/24 -o eth0 -j SNAT --to-source 203.0.113.5第三,IPv6别忘了。如果你的机器开启了IPv6,而内网又需要IPv6访问,那么NAT规则也要考虑ip6tables。尤其是一些双栈场景,只配置了IPv4的NAT,IPv6流量直接裸奔或者完全不通,都是安全隐患。
第四,保存规则前做语法检查和连通性确认。一个比较稳妥的流程是:
- 先在命令行手动执行规则,确认功能正常
- 再把规则写入保存文件或脚本
- 最后执行一次加载测试,重启或重载规则,确认环境恢复
永远不要在生产环境里,把还没验证过的规则直接写入开机启动项。
8. 我踩过的一个真实案例
最后聊一个我亲身经历的案例,它把SNAT、DNAT、filter这三个要素全串起来了,希望能帮你加深理解。
有一年我负责一个电商小公司的网关,拓扑跟前面讲的差不多:一台Linux网关,eth0接公网,eth1接内网。内网有台Web服务器和一台数据库服务器。某天下午,客服反馈官网打不开,外部用户无法访问,但内网办公电脑访问官网正常。
我的排查过程是这样的:先在外网电脑上ping网关公网IP,能通,说明公网链路没问题。接着在网关上看DNAT规则:
iptables -t nat -L PREROUTING -n -v计数正常,说明外部请求确实到了网关,也匹配了DNAT规则。然后看FORWARD链:
iptables -L FORWARD -n -v发现指向Web服务器的转发规则有计数,但状态有点怪,再看连接跟踪:
conntrack -L | grep 80发现连接状态大量是TIME_WAIT,但缺少ESTABLISHED状态的活动连接。这一步很关键,说明数据包方向不对。
接着tcpdump抓内网口:
tcpdump -i eth1 host 192.168.10.100 and port 80发现只有SYN包进去,没有回包。问题定位到Web服务器方向。登录Web服务器,发现Nginx进程正常,但本机防火墙ufw默认策略是拒绝所有入站流量,而且是后来某次系统更新后自动启用的。我加了一条放行192.168.10.1网关IP的规则,问题解决。
这个案例说明什么?NAT链路是通的,网关转发是通的,但内网服务器自己的防火墙把流量拦住了。所以排查NAT问题,不能只盯网关,整条链路每个环节都要过一遍。前面说的三层排查法——网关NAT规则、网关FORWARD规则、目标服务器本地防火墙——缺一不可。
根据我个人经验,处理这类NAT问题,可以按一个自创的"三段式"定位法快速缩小范围:先确认包有没有进网关(看PREROUTING计数),再确认包有没有过转发(看FORWARD计数),最后确认目标服务器有没有收到包(登录机器看日志或抓包)。这三个节点一查,问题基本就浮出水面了,不用瞎猜。
你把这套SNAT、DNAT的完整流程吃透之后,绝大部分内外网互通的配置和排查需求都能顺手搞定。后面如果还想深入,可以接着研究iptables的mangle表、流量控制、NAT与负载均衡的结合等进阶话题。先花点时间把每一张表每条链的走向在脑子里过一遍,动手写规则时会轻松很多。