重温iptables
下面这张图描述了一个L3的IP封包如何通过iptables:
对于此图的说明:
- Iptables和内核路由的关系:执行完PREROUTING链之后,会进行路由表的查询
- 通过lo接口的封包,不走PREROUTING的DNAT表
- 出站封包在OUTPUT链之前就进行了路由处理。但是如果OUTPUT进行了DNAT,则会进行重新选路
- 入站封包,如果使用了隧道,则会经由PREROUTING - INPUT链逐层的解除隧道
- 出站封包,如果使用了隧道,则会经由OUTPUT -POSTROUTING链逐层的进行隧道封装
下面的图示意了更接近实际的流程:
iptables是用户空间命令,通过netlink和内核的netfilter模块进行交互,在L3/L4操控封包。
netfilter是Linux网络安全大厦的基石,从2.4开始引入。它提供了一整套Hook函数机制,IP层的5个钩子点对应了iptables的5个内置链条:
- PREROUTING,在此DNAT
- POSTROUTING,在此SNAT
- INPUT,处理输入给本地进程的封包
- OUTPUT,处理本地进程输出的封包
- FORWARD,处理转发给其他机器、其他网络命名空间的封包
对于从网络接口入站的IP封包,首先进入PREROUTING链,然后进行路由判断:
- 如果封包路由目的地是本机,则进入INPUT链,然后发给本地进程
- 如果封包路由目的地不是本机,并且启用了IP转发,则进入FORWARD链,然后通过POSTROUTING链,最后经过网络接口发走
对于本地进程发往协议栈的封包,则首先通过OUTPUT链,然后通过POSTROUTING链,最后经过网络接口发走
除了链条,iptables还包含一个正交的概念,表是用于分类管理iptables规则的。不同的表,通常用作不同的目的:
- filter表:用于控制到达某条链条上的数据包如何处理,是放行(Accept)、丢弃(Drop)还是拒绝(Reject)。如果不指定-t参数,这是默认操控的表格
- nat表:用于修改源地址,或者目的地址。该表格在某个创建了新连接的封包出现时生效
- mangle表:该表格用于修改封包的内容
- raw表:具有高优先级,可以对接收到的封包在连接跟踪(Connection Tracking,为了支持NAT,iptables会对每个L4连接进行跟踪,也就是维护每个L4连接的状态)前进行处理(也就是取消跟踪),可以应用在那些不需要做NAT的情况下提高性能。例如高访问的Web服务,可以不让iptables做80端口封包的连接跟踪
- security表:用于强制访问控制(Mandatory Access Control),很少使用
表的优先级从高到低为 raw ⇨ mangle ⇨ nat ⇨ filter ⇨ security。用户不能自定义新表。
封包会依次经过相关的链,在每个链中,会根据表的优先级,依次遍历各个表中的规则。任何表中的规则都有机会拒绝封包。
需要注意,不是任何链条上可以挂任何表:
- raw可以挂在PREROUTING、OUTPUT
- mangle可以挂在任何链上
- nat(SNAT)可以挂在POSTROUTING、INPUT
- nat(DNAT)可以挂在PREROUTING、OUTPUT
- filter可以挂在FORWARD、INPUT、OUTPUT
- security可以挂在FORWARD、INPUT、OUTPUT
用于修改封包。某些目标只能用在mangle表中,包括:
- TOS,此目标设置封包的Type Of Servier字段,以便影响封包的处理策略,例如如何进行路由
- TTL,设置封包的Time To Live字段
- MARK,给封包添加一个标记值(数字),iproute2能够识别此mark并且进行特殊的路由处理。此外基于MARK还可以进行带宽限制、基于Class的排队
应当仅仅用于网络地址转换。也就是使用以下目标:
- DNAT:用在你有一个公共地址,别人访问此地址时,你需要将访问重定向到防火墙背后的某个服务时
- SNAT:允许隐藏在防火墙背后的内网机器访问外部网络
- MASQUERADE:类似SNAT,不同之处在于每次都需要动态计算使用什么作为转换后的源地址。用在主机地址不固定的情况下
- REDIRECT:类似于DNAT,但是新的目的地址被锁定为接收封包的那个网卡地址,同时端口改为随机的或指定的值
上面这些目标都会改变IP封包的首部。
用于过滤封包,使用ACCEPT、DROP之类的目标
表中存放的是一个个规则,规则由匹配+目标组成。目标包括:
- ACCEPT,允许封包通过,如果在子链ACCEPT,则相当于在父链也ACCEPT了,不会继续遍历父链后续规则
- RETURN,从当前链返回,行为类似于编程语言函数中的return。如果当前:
- 位于子规则链,则返回到父链调用子链的那个规则的下一条规则处执行
- 不位于子规则链,则执行默认策略
- DROP,直接丢弃数据包,不进行后续处理
- REJECT,返回connection refused或者destination unreachable报文
- QUEUE,将数据包放入用户空间的队列,供用户空间应用程序处理
- JUMP,跳转到用户自定义链继续执行
- LOG 让内核记录匹配的封包
- AUDIT 对封包进行审计
- DNAT 目标地址映射
- SNAT 源地址映射
- MASQUERADE 源地址映射,用于本机地址是动态获取的情况下
通常情况下,一旦匹配了某个规则,就会执行它的目标。同时,不再检查同规则链、同表的后续规则,这意味着规则的顺序非常重要。某些目标是例外,例如LOG、MARK,会继续遍历后面的规则
如果内置规则链执行到结尾,或者内置规则链中的某个目标是RETURN的规则匹配封包,那么规则链策略(Chain Policy)中定义的目标将决定如何处理当前封包。
步骤 | 表 | 链 | 说明 |
1 | 封包在网络上 | ||
2 | 封包进入本机网卡 | ||
3 | mangle | PREROUTING | 在此修改封包,例如改变TOS。连接跟踪发生在这个链上 |
4 | nat | PREROUTING | 主要用于DNAT。避免在此进行封包过滤,某些情况下可能被bypass |
5 | 路由决策:封包目的地是本机,还是需要被转发,转发到哪里 | ||
6 | mangle | INPUT | 路由决策之后,发送到本机进程之前修改封包 |
7 | filter | INPUT | 针对所有目的地是本地的封包进行过滤,不管它来自什么网络接口 |
8 | 封包到达本地进程 |
步骤 | 表 | 链 | 说明 |
1 | 本地进程发送封包 | ||
2 | 路由决策:使用什么源地址,什么出口网卡,收集一些其它信息 | ||
3 | mangle | OUTPUT | 修改封包。不要在此进行封包过滤,因为具有副作用。本地生成的连接跟踪也在此进行 |
4 | nat | OUTPUT | 对出站封包进行NAT |
5 | 再次路由决策,因为mangle或nat可能导致需要重新路由 | ||
6 | filter | OUTPUT | 对本机发出的封包进行过滤 |
7 | mangle | POSTROUTING | 在路由决策之后,发送出网卡之前,修改封包 |
8 | nat | POSTROUTING | 在此进行SNAT。不要在此进行封包过滤,即使默认策略设置为DROP也可能导致某些封包溜走 |
9 | 到达网卡 | ||
10 | 进入网线 |
步骤 | 表 | 链 | 说明 |
1 | 封包在网线上 | ||
2 | 封包到达入口网卡 | ||
3 | mangle | PREROUTING | 修改封包 |
4 | nat | PREROUTING | 进行DNAT |
5 | 路由决策 | ||
6 | mangle | FORWARD | 仅用于特殊场景下,在初始路由决策之后修改封包 |
7 | filter | FORWARD | 对转发封包进行过滤 |
8 | 最终路由决策 | ||
9 | mangle | POSTROUTING | 所有路由决策完成之后,修改封包 |
10 | nat | POSTROUTING | 进行SNAT |
11 | 到达出口网卡 | ||
12 | 进入网线 |
连接跟踪(Connection tracking )的目的是,让Netfilter框架能够知晓每个连接的状态。你可以使用 --state来匹配状态。
注意,为了更加准确的跟踪TCP连接,内核还维护一系列的内部状态。这些内部状态不能用来匹配。
在iptables中,每个封包都可能和一个被跟踪的连接相关联。连接可以处于4种不同的状态:
状态 | 说明 |
NEW |
说明看到的封包是连接的第一个封包。导致此状态的例子:
|
ESTABLISHED |
当双向封包都监测到之后,进入此状态。导致此状态的例子:
|
RELATED |
如果连接和另外一个ESTABLISHED状态的连接相关,则它进入此状态 如果主连接产生一个新连接,这个新连接就是RELATED的。例如FTP数据连接是相关于FTP控制连接的 |
INVALID | 无法识别的状态,可能由于系统内存耗尽、不响应已知连接的ICMP报文导致 |
在内核中,连接跟踪由conntrack模块负责。
除了本地生成的包,在OUTPUT中跟踪之外,所有连接跟踪都在PREROUTING链中进行。例如:
- 本地发起一个连接的初始封包,则在OUTPUT链中,连接变为NEW
- 当接收到上述封包的应答包后,在PREROUTING链中,连接变为ESTABLISHED
如果外部发起初始封包,则NEW状态是在PREROUTING中产生的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
iptables [-t table] {-A|-C|-D} chain rule-specification ip6tables [-t table] {-A|-C|-D} chain rule-specification iptables [-t table] -I chain [rulenum] rule-specification iptables [-t table] -R chain rulenum rule-specification iptables [-t table] -D chain rulenum iptables [-t table] -S [chain [rulenum]] iptables [-t table] {-F|-L|-Z} [chain [rulenum]] [options...] iptables [-t table] -N chain iptables [-t table] -X [chain] iptables [-t table] -P chain target iptables [-t table] -E old-chain-name new-chain-name 其中: rule-specification = [matches...] [target] match = -m matchname [per-match-options] target = -j targetname [per-target-options] |
带-n参数时,0.0.0.0/0表示匹配任何地址(anywhere)
不带-n参数时,所有保留私有地址都会显示为bogon
选项被分为若干组:
-t --table table 指定命令操控的表格的名称,如果内核被配置为自动模块加载方式,并且相应模块不存在,将尝试自动加载适当的模块
该组选项指定期望执行的动作,仅其中一个可以在命令行中指定
-A --append chain rule-specification 附加一个或者多个规则到目标规则链。如果源/目标名称解析到多于一个地址,那么规则将会添加到每个可能的地址组合
-C --check chain rule-specification 检查目标规则链是否具有匹配的规则,该命令与-D的逻辑类似,但是不会修改当前的iptables配置,使用其退出码来指示是否成功
-D --delete chain rule-specification 从目标规则链中删除一个或者多个规则,需要指定规则的匹配规则
-D --delete chain rulenum 从目标规则链中删除一个或者多个规则,需要指定从1开始的个数
-I --insert chain [rulenum] rule-specification 插入一个或者多个规则到目标规则链的指定位置,如果rulenum是1则插到最前面,举例:
-I FORWARD 1
-R --replace chain rulenum rule-specification 替换目标规则链中的一个或者多个规则。如果源/目标名称解析到多个地址,该命令将失败
-L --list [chain] 列出选中规则链中的所有规则,如果不指定规则链,则列出全部规则链,与其他命令不同,该命令仅应用到特定的表格,因此如果想列出NAT表的全部规则,需要使用:iptables -t nat -n -L。该命令一般与-n联用,以避免耗时的DNS查找,指定-Z选项也是合法的
-S --list-rules [chain] 打印选定规则链中的所有规则,该命令仅应用到特定的表格
-F --flush [chain] 刷空选定规则链中的所有规则(如果不指定规则链,则应用到表格中全部的规则链),等价于一条条删除规则
-Z --zero [chain [rulenum]] 将封包、字节计数器置零
-N --new-chain chain 创建一个用户自定义的规则链,名称为chain,chain必须不是任何已经存在的target
-X --delete-chain [chain] 删除可选的、用户定义的规则链
-P --policy chain target 设置某个规则链的策略(没有规则匹配封包时的默认行为)为target。仅内置规则链可以设置策略,策略的目标不得是任何规则链,举例:
1 |
iptables --policy INPUT DROP # 设置INPUT的规则链策略为DROP |
-E --rename-chain old-chain new-chain 重命名用户定义的规则链
以下的参数用来组成一个rule specification
[!] -p, --protocol protocol 用来检查的规则或者封包的协议。支持的值包括:tcp, udp, udplite, icmp, icmpv6,esp, ah, sctp, all。叹号用来反转测试,默认all
[!] -s, --source address[/mask][,...] 封包来源规则,address可以是网络名、主机名、IP地址(附加/掩码)、或者普通IP地址。主机名仅仅会在规则提交到内核前解析一次。叹号用来反转地址规则。指定多个地址是可以的,但是会被分解为多个规则,或者导致多个规则被删除(-D)
[!] -d, --destination address[/mask][,...] 封包目标规则,类似来源规则
-m --match match 指定使用的match,所谓match是用来测试特定属性的扩展模块
-j --jump target 指定规则的目标,即:当规则匹配的时候要做什么。目标可以是用户定义的规则链、一个特殊的内置目标(立即决定封包命运)、或者一个扩展。如果某个规则不设置该选项,那么匹配后将对封包不构成任何影响,但是规则的计数器会增加
-g --goto chain 仅仅用在用户定义的规则链中,指示重定向到另外一个链继续处理。和-j不同,-j后子链完成匹配后会跳转到父链,-g则不会跳转,如果子链没有匹配的规则则按子链的默认策略处理
[!] -i, --in-interface name 仅用于进入INPUT, FORWARD, PREROUTING链的封包。限制接收到封包的网络接口名称,如果名称后面跟着+号,所有以该名称开头的接口都被匹配
[!] -o, --out-interface name 仅用于进入FORWARD, OUTPUT, POSTROUTING链的封包。限制封包将被送出的网络接口名称,如果名称后面跟着+号,所有以该名称开头的接口都被匹配
[!] -f, --fragment 对于分片的IP数据报,该规则将仅仅用于第二个或者以后的的分片。仅用于IPv4
-c --set-counters packets bytes 在INSERT, APPEND, REPLACE规则时,用于初始化封包、字节计数器
-v --verbose 冗长输出,list命令将输出接口名称、规则选项、TOS掩码、包以及字节计数器。对于附加、插入、删除、替换规则的操作,该选项导致规则的详细信息被打印
-w --wait 等待xtables锁被解开,为了防止多个进程同时修改,iptables具有锁定机制
-n --numeric 数字化输出,IP地址和端口将使用数字格式
-x --exact 仅与-L命令有关,显示精确的封包、字节计数,使用1024而不是1000作为倍率
--line-numbers 列出规则时,显示行号
--modprobe=command 当添加、删除规则时,使用命令command加载必要的模块(目标、匹配-m扩展等)
1 2 3 4 5 6 7 8 9 10 11 12 |
# 发送任何目的端口为25的封包给在127.0.0.1:10025上监听的进程 # 并且给封包以标记 1 iptables -t mangle -A PREROUTING -p tcp --dport 25 -j TPROXY \ --tproxy-mark 0x1/0x1 --on-port 10025 --on-ip 127.0.0.1 # PREROUTING链之后,转发之前,检查路由表 # 如果发现封包被标记为 1,那么查找100号表 ip rule add fwmark 1 lookup 100 # 100号路由表,将封包通过lo发出。注意出口为lo的封包不会真正路由,而是交给本地进程处理 ip route add local 0.0.0.0/0 dev lo table 100 |
1 2 3 4 5 6 7 8 9 10 |
# 对于准备从wlan0出去的封包,进行源地址映射 iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE # 允许转发来自eth0、发往wlan0的封包 iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT # 对于来自wlan0、发往wlan0的封包,如果状态为RELATED,ESTABLISHED则允许通过 # ESTABLISHED,封包属于一个已经建立的连接 # RELATED,封包创建了新连接,但是此连接和一个已经存在的连接相关,例如FTP数据传输器、ICMP错误 iptables -A FORWARD -i wlan0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 列出filter表(可用于实现防火墙)内容。注意默认网卡信息不显示,需要加 -v iptables -t filter -L -n # 禁止向目标地址发送网络包 iptables -A OUTPUT -j DROP -d 10.5.39.223 # 撤销 iptables -D OUTPUT -j DROP -d 10.5.39.223 # 禁止PING iptables -I INPUT -p icmp --icmp-type 8 -j DROP # 撤销 iptables -D INPUT -p icmp --icmp-type 8 -j DROP # 禁止向目标网络转发包 iptables -A FORWARD -d 172.27.0.0/16 -j DROP iptables -D FORWARD -d 172.27.0.0/16 -j DROP # 取消Docker的网络隔离 iptables -I DOCKER-USER 1 -j ACCEPT # 或者 两个Docker网络的网段 iptables -t filter -I DOCKER-USER -s 172.17.0.0/16 -j ACCEPT iptables -t filter -I DOCKER-USER -s 172.21.0.0/16 -j ACCEPT |
1 2 |
# 显示Filter表的详细信息,格式化输出 sudo iptables -L -nv -t filter | column -t -s " " |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 作为VPN服务器的主机,启用ipv4转发 iptables -t nat -L -v -n # 输出如下: # ... # Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) # pkts bytes target prot opt in out source destination # 66404 4468K MASQUERADE all -- any any anywhere anywhere # 删除既有的IP掩蔽设置,针对所有封包做了端口映射 iptables -t nat -D POSTROUTING -j MASQUERADE # 这时再连接到gmem.cc的VPN,发现无法上外网了 # 原因是,没有合适的端口映射,gmem.cc无法将封包发回给VPN客户端 # 修改后的IP掩蔽配置,限制了源IP地址的范围、出口网卡 iptables -t nat -A POSTROUTING -m iprange --src-range 172.21.0.100-172.21.0.120 -o eth0 -j MASQUERADE |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# 把针对本机192.168.0.89的7777端口的请求全部转换为对虚拟机172.16.87.132的80端口的请求 sudo iptables -t nat -A PREROUTING # 入站后第一个链 -p tcp --dport 7777 -i eth0 # 如果封包来自eth0且目标端口为7777 -j DNAT --to-destination 172.16.87.132:80 # 那么修改目标地址为虚拟机的80端口 # 此时,针对192.168.0.89:7777的封包,目的地址已经改为172.16.87.132 # 允许对目的地址为172.16.87.132的封包进行转发,可能发生的情况是: # 原始数据报 # 192.168.0.200:23387 => 192.168.0.89:7777 # 入站后,目标地址被映射: # 192.168.0.200:23387 => 172.16.87.132:80 # 转发:由于目的地址不是本机,因此进行转发,ipv4转发功能已经开启过 # 出站前,源地址被映射被映射: # 172.16.87.1:39480 => 172.16.87.132:80 # 这就为本机39480端口和192.168.0.200:23387建立了映射关系 sudo iptables -t nat -A POSTROUTING -p tcp -d 172.16.87.132 --dport 80 -j MASQUERADE # 上面的命令也可以替换为: sudo iptables -t nat -A POSTROUTING -p tcp -d 172.16.87.132 --dport 80 -j SNAT --to-source 172.16.87.1 # 返回的数据报,处理时,自动逆向映射:原先的目标映射用于逆向源映射,原先的源映射用于逆向目标映射 # 原始数据报 # 172.16.87.132:80 => 172.16.87.1:39480 # 入站后,目标地址被映射 # 172.16.87.132:80 => 192.168.0.200:23387 # 出站前,源地址被映射 # 192.168.0.89:7777 => 192.168.0.200:23387 # ✭✭✭ 一个重要的原则是,仅仅去匹配初始发起连接的包,做好NAT后,返回的包自动映射 |
很多iptables命令都要求提供一个规则的规格(rule-specification,后面简称规则)。规则实际上就是一系列iptables命令选项,大部分选项都用来说明如何匹配封包的, -j用于指定目标。
要为规则添加注释,参考:
1 2 |
iptables -m comment --comment "comment here" iptables -A INPUT -i eth1 -m comment --comment "my LAN - " -j DROP |
可以看到,注释以匹配扩展的形式被支持。
注释最多256字符,执行 iptables -L命令时会以 /* */打印在规则尾部。
这些匹配选项的值,前面都可以加 !表示反向匹配。
选项 | 说明 |
-p, --protocol | 匹配协议,可选值 TCP, UDP, ICMP等 |
-s, --src, --source | 匹配源地址。地址形式192.168.0.0/24 |
-d, --dst, --destination | 匹配目的地址。地址形式192.168.0.0/24 |
-i, --in-interface | 匹配入站网络接口,仅用于INPUT, FORWARD, PREROUTING链。eth+表示匹配任何以太网卡 |
-o, --out-interface | 匹配出站网络接口,OUTPUT, FORWARD, POSTROUTING链。eth+表示匹配任何以太网卡 |
-f, --fragment | 用于匹配分片的封包的非首分片。取反需要 ! -f |
选项 | 说明 |
--sport, --source-port | 源端口。支持端口范围: --source-port 22:80 |
--dport, --destination-port | 目的端口。支持端口范围 |
--tcp-flags | 匹配TCP标记,可用的标记SYN, ACK, FIN, RST, URG, PSH,还有两个特殊的值ALL, NONE分别匹配具有任何标记的封包、没有任何标记的封包 |
--syn | 匹配设置了SYN位,但是没有设置ACK,RST位的封包 |
--tcp-option | 基于TCP选项来匹配 |
举例:
1 2 |
# 匹配SYN标记被设置,ACK,FIN,RST没有被设置的封包 iptables -A FORWARD -p tcp --tcp-flags SYN,ACK,FIN,RST SYN |
选项 | 说明 |
--sport, --source-port | 源端口匹配 |
--dport, --destination-port | 目的端口匹配 |
选项 | 说明 |
--icmp-type | 匹配ICMP报文类型 |
所谓显式匹配,是指必须明确通过 -m或 --match选项加载的匹配。
内核中基于netfilter框架实现的连接跟踪模块即conntrack。在DNAT时,conntrack使用状态机来跟踪连接的状态,记住目的地址被从什么改成了什么,这样才能将回程包的源地址改回来
基于连接跟踪的匹配。使用 -m conntrack加载,可以接额外选项:
子选项 | 说明 |
--ctstate |
匹配封包状态,可选值: INVALID 匹配无法识别或没有任何状态的数据报 要匹配多个状态,用逗号分隔: -m conntrack --ctstate ESTABLISHED,RELATED 取反需要在匹配模块名之后: -m conntrack ! --ctstate ESTABLISHED,RELATED |
--ctproto | 匹配封包协议。示例 -m conntrack ! --ctproto TCP |
--ctorigsrc | 匹配封包源地址。示例 -m conntrack --ctorigsrc 192.168.0.0/24 |
--ctorigdst | 匹配封包目的地址 |
使用IP地址范围进行匹配。使用 -m iprange加载,可以接额外选项:
子选项 | 说明 |
--src-range | 源地址范围。示例 -m iprange --src-range 192.168.1.13-192.168.2.19 |
--dst-range | 目的地址范围。示例 -m iprange --dst-range 192.168.1.13-192.168.2.19 |
第三层载荷匹配。该扩展用于根据网络层载荷,即传输层封包的长度进行匹配
用法: -m length [!] --length length[:length] TCP/UDP包的长度
可以用于限制特定规则的日志记录的频度。使用 -m limit加载,可以接额外选项:
子选项 | 说明 |
--limit |
指定平均匹配速率。如果指定单位时间内匹配的封包超过数量,则不再匹配 示例: -m limit --limit 3/hour。每小时最多匹配3次 可用单位:/second /minute /hour /day |
--limit-burst | 指定最大匹配速率 |
根据源地址的MAC地址进行匹配,只能用在PREROUTING, FORWARD,INPUT 规则链。
示例: -m mac --mac-source 00:00:00:00:00:01
根据关联当前封包的netfilter标记进行匹配。mark是一个特殊字段,无符号整数(unsigned int),仅仅在内核中维护(不会嵌入在封包里)。封包在通过主机时,标记可以和它关联。
很多内核例程都会使用mark,从而完成流量塑形(Traffic Shaping)、过滤等工作。
要设置一个mark,可以使用iptables的MARK目标。由于以前在ipchains的FWMARK目标中设置,现在仍然会使用FWMARK这个术语。
示例: -m mark --mark 1986,匹配具有1986标记的封包。你还可以指定标记的掩码,也就是 -m mark --mark 1/1这种形式,这样 / 后面的数字,会与netfilter标记先进行逻辑与,得到的值再和 / 前面的值进行比较 —— 这可以仅仅关注fwmark的某个位
多端口匹配。该扩展只能用于以下协议:tcp, udp, udplite, dccp, sctp。最多指定15个端口
指定端口的语法: port[,port|,port:port]
使用 -m multiport加载,可以接额外选项:
子选项 | 说明 |
--source-port | 根据源端口匹配,示例 -m multiport --source-port 22,53,80,110 |
--destination-port | 根据目的端口匹配 |
--port | 根据源或目的端口匹配 |
根据创建封包的进程的身份来匹配封包,很明显,只能用于本机产生的封包上,你只能在OUTPUT链中使用该匹配。
使用 -m owner加载,可以接额外选项:
子选项 | 说明 |
--uid-owner |
如果封包是由指定的User ID创建,则匹配 可能的应用场景:仅仅允许Apache通过80端口发出数据 |
--gid-owner | 如果封包是由指定的Group ID中的用户创建,则匹配 |
--pid-owner | 如果创建封包的进程具有指定的PID,则匹配 |
--sid-owner | 如果创建封包的进程具有指定的Session,则匹配 |
根据链路层类型匹配。用法 -m pkttype [!] --pkt-type {unicast|broadcast|multicast}
和内核中的连接跟踪代码配合工作。此匹配会从连接跟踪状态机中读取封包的状态。
示例: -m state --state RELATED,ESTABLISHED
根据TCP的Maximum Segment Size来匹配,仅仅针对SYN或SYN/ACK包。
示例: iptables -A INPUT -p tcp --tcp-flags SYN,ACK,RST SYN -m tcpmss --mss 2000:2500
根据Type Of Service来匹配。示例: iptables -A INPUT -p tcp -m tos --tos 0x16
根据Time To Live来匹配。示例: iptables -A OUTPUT -m ttl --ttl 60
通过封包的地址类型进行匹配,可用的地址类型包括:
地址类型 | 说明 |
UNSPEC | 未指定的地址,例如0.0.0.0 |
UNICAST | 单播地址 |
LOCAL | 本地地址(任何分配到任一本地网络接口的地址,包括loopback地址、ipvs虚拟地址) |
BROADCAST | 广播地址 |
ANYCAST | 选播地址 |
MULTICAST | 黑洞地址 |
UNREACHABLE | 不可达地址 |
PROHIBIT | 禁止地址 |
使用 -m addrtype加载,可以接额外选项:
子选项 | 说明 |
[!] --src-type type |
根据源地址类型匹配 地址类型: LOCAL:不是指loopback地址,而是指任何分配给本机某个网络接口的IP地址(包括loopback地址、ipvs虚拟地址) |
[!] --dst-type type | 根据目标地址类型匹配 |
--limit-iface-in | 限制检查的入站网络接口,仅支持PREROUTING, INPUT, FORWARD规则链 |
--limit-iface-out | 限制检查的出站网络接口,仅支持POSTROUTING, OUTPUT, FORWARD规则链 |
根据某个连接到目前为止发送的字节数、封包数,或者每封包平均字节数进行匹配,计数器64位长。
该扩展可以用来检测维持了很长时间的下载,并将其安排到低优先级,以进行带宽控制。
使用 -m connbytes加载,可以接额外选项:
子选项 | 说明 |
[!] --connbytes from[:to] | 指定匹配的封包/字节数范围 |
--connbytes-dir {original|reply|both} | 统计哪种类型的封包 |
--connbytes-mode {packets|bytes|avgpkt} | 统计模式,封包数、字节数、平均每封包的字节数 |
可用于限制一个客户端可以连接到某个服务的并发连接数。可以接额外选项:
子选项 | 说明 |
--connlimit-upto n | 如果连接数低于或者等于n则匹配 |
--connlimit-above n | 如果连接数大于n则匹配 |
--connlimit-mask prefix_length | 统计哪些IP的总和连接数,prefire_length指定分组的IP前缀 |
--connlimit-saddr | 限制源地址组,默认值 |
--connlimit-daddr | 限制目的地址组 |
示例:
1 2 3 4 |
# 允许每个客户端主机开启两个telnet连接 iptables -A INPUT -p tcp --syn --dport 23 -m connlimit --connlimit-above 2 -j REJECT # 限制每个C类地址网段(例如192.168.0.*)最多总计建立16个HTTP连接 iptables -p tcp --syn --dport 80 -m connlimit --connlimit-above 16 --connlimit-mask 24 -j REJECT |
基于关联到连接的netfilter mark字段进行匹配,此mark由CONNMARK目标来设置。
用法: -m connmark [!] --mark value[/mask]
示例:
1 2 3 |
# 匹配所属连接被标记为123的封包 # 将连接的标记拷贝到封包上 iptables -t mangle -I OUTPUT -m connmark --mark 123 -j CONNMARK --restore-mark |
类似于connmark,但是标签是按位的(每个标签占一个bit),全系统支持最多128个标签
匹配条件:
- 针对包进行套接字查找(Socket lookup),如果发现存在打开的TCP/UDP套接字,也就是说SRC_IP:SRC_PORT:DST_IP:DST_PORT作为已建立的(established)套接字存在于本地TCP/IP栈,则匹配
- 存在established的监听套接字,则匹配
- 存在非零绑定的监听套接字(non-zero bound listening socket),甚至在非本地地址上监听,则匹配。所谓zero bound应该就是指绑定到0.0.0.0
简而言之,就是匹配应当由本地TCP/IP栈处理(而非转发)的封包。进行套接字查找时,使用packet元组,或者嵌入在 ICMP/ICPMv6错误报文中的原始TCP/UDP头
可以接额外选项:
子选项 | 说明 |
--transparent |
仅仅匹配透明套接字。透明套接字意味着地址不是本机的任何网络接口上配置的地址 |
--nowildcard |
不忽略绑定到ANY地址(0.0.0.0)的套接字 默认情况下不匹配zero-bound的监听套接字,其目的是让本地服务能够监听请求而不是统统被转发 |
这个匹配扩展+MARK+策略路由,就能够实现全功能的non-locally bound套接字。常常用于配合优化透明代理。通常是匹配后进行MARK,然后具有此MARK的封包路由设置到lo。
从各种文档中看到关于socket match的说明,都不是非常明朗,导致理解起TPROXY拦截模式下Istio的iptables规则有些困难:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# mangle表 target prot opt source destination ISTIO_INBOUND tcp -- 0.0.0.0/0 0.0.0.0/0 Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain POSTROUTING (policy ACCEPT) target prot opt source destination Chain ISTIO_DIVERT (1 references) target prot opt source destination MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK set 0x539 LOG tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 LOG flags 0 level 4 prefix "dst-80-socket-matched: " ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 Chain ISTIO_INBOUND (1 references) target prot opt source destination RETURN tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 RETURN tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:15090 RETURN tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:15020 ISTIO_DIVERT tcp -- 0.0.0.0/0 0.0.0.0/0 socket ISTIO_TPROXY tcp -- 0.0.0.0/0 0.0.0.0/0 Chain ISTIO_TPROXY (1 references) target prot opt source destination LOG tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 LOG flags 0 level 4 prefix "dst-80-before-tproxy: " TPROXY tcp -- 0.0.0.0/0 !127.0.0.1 TPROXY redirect 0.0.0.0:15001 mark 0x539/0xffffffff |
这是一个启用了TPROXY拦截模式的Pod的iptables,LOG目标是我添加用于分析socket match的行为的。此Pod的地址是172.27.121.156。可以看到它在监听:
1 2 3 4 |
# netstat -nlt # Active Internet connections (only servers) # Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN |
现在我们从另一个客户端 172.27.155.65上访问 curl http://172.27.121.156。
这会发起一个TCP连接,它的SYN包,是否匹配socket扩展呢?我们回顾一下它的匹配条件:
- 套接字查找成功,这个不满足。因为是新连接
- 已经建立的监听套接字,这个不满足,监听套接字还没有收到SYN,谈不上建立
- 存在non-zero bound listening socket。这个说的让人费解。但是肯定隐含着在SYN包的目的地址上监听。此SYN包的目的地址是172.27.121.156:80,我们知道Pod在0.0.0.0:80上监听了,那么172.27.121.156作为它的本地地址,自然也在其上监听。那么,这个监听套接字,是不是non-zero bound的呢?网上很难找到相关信息,我的理解,zero bound就是指绑定到0.0.0.0。在这种猜测之下,是不匹配socket的
根据iptables日志来验证一下。客户端curl之后,在Pod的内核看到一下日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 客户端发来的SYN,可以看到不匹配socket,直接走到TPROXY,这证明了上述猜测 dst-80-before-tproxy: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=60 SYN URGP=0 # 客户端发来的ACK,注意反方向报文没有日志 # 这此匹配socket了,因为满足了1,套接字查找成功 dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 ACK URGP=0 MARK=0x539 # 这次应该是客户端发送HTTP请求报文了 dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=130 ACK PSH URGP=0 MARK=0x539 # 这里是Envoy发给80端口进程,首个报文,但是不会TPROXY,因为目的地址不满足要求 dst-80-before-tproxy: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00 SYN URGP=0 # 套接字查找成功,都不再走TPROXY了 dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539 dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=322 TOS=0x00 PREC=0x00 ACK PSH URGP=0 MARK=0x539 dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539 dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539 # 80进程处理完,报文返回Envoy,Envoy又返回给客户端。客户端发起后续报文,包括FIN dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539 dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539 dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK FIN URGP=0 MARK=0x539 dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539 dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK FIN URGP=0 MARK=0x539 |
IPSet扩展,可以匹配一组IP。使用 -m set加载,额外选项:
子选项 | 说明 |
--match-set ipsetname |
匹配的IP Set, [!] --match-set setname flag[,flag]...形式 此选项必须,flag是IPSet的字段 iptables -A FORWARD -m set --match-set test src,dst表示:
|
--return-nomatch | 如果指定此选项并且目标IPSet的类型支持nomatch,则反转匹配结果 |
使用Linux Socket Filter来过滤,需要提供十进制形式的(由nfbpf_compile生成)BPF字节码。
示例:
1 2 3 |
iptables -A OUTPUT -m bpf --bytecode '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0' -j ACCEPT # 等价写法 iptables -A OUTPUT -m bpf --bytecode "`nfbpf_compile RAW 'ip proto 6'`" -j ACCEPT |
根据处理封包的CPU来匹配。示例:
1 2 3 4 |
# 如果是第一个核心处理了封包,则重定位到8080端口 iptables -t nat -A PREROUTING -p tcp --dport 80 -m cpu --cpu 0 -j REDIRECT --to-port 8080 # 如果是第二个核心处理了封包,则重定位到8081端口 iptables -t nat -A PREROUTING -p tcp --dport 80 -m cpu --cpu 1 -j REDIRECT --to-port 8081 |
根据封包到达时间来匹配。
使用选项 -j来指定目标,除了本节要介绍的各种目标外,该选项的值还可以是:
- 自定义链的名称,这导致跳转到自定义的链继续执行
接收封包,不再遍历当前链的其它规则、也不会在遍历当前表中的任何规则。但是,在一个链中被接收的封包,可能仍然需要经过其它表中的链,仍然可能被DROP。
在子链中ACCEPT的效果,等价于在父链中被ACCEPT。
停止遍历当前链:
- 如果当前位于子链,则回到父链,继续执行后面的规则
- 如果当前位于主链,则执行默认策略
静默的丢弃封包,不会在对它进行任何后续处理。
可能会导致对方主机上出现一个僵尸套接字,最好用REJECT代替。
用于发送一个错误报文给匹配封包的发送者,其它方面,该目标的行为与DROP一致:丢弃封包,终止规则遍历。
该目标仅用于INPUT, FORWARD, OUTPUT规则链中,或者从这两规则链调用的用户自定义规则链。
额外选项:
子选项 | 说明 |
--reject-with type | 错误报文的类型: icmp-net-unreachable icmp-host-unreachabl icmp-port-unreachable(默认) icmp-proto-unreachable icmp-net-prohibited icmp-host-prohibite icmp-admin-prohibited |
允许创建命中目标的封包的审计记录,用来记录被接受的、丢弃的、拒绝的封包。
用法: -j AUDIT --type {accept|drop|reject}
示例:
1 2 3 4 5 6 |
# 添加一个规则链到默认表格 iptables -N AUDIT_DROP # 审计丢弃包的动作 iptables -A AUDIT_DROP -j AUDIT --type drop # 丢弃包 iptables -A AUDIT_DROP -j DROP |
这是一个非终止性目标,后续规则总是会被执行。如果你希望LOG后DROP,在此规则后面紧跟着一个相同匹配的目标为DROP的规则。
让内核记录匹配的封包。内核将会打印匹配的封包的信息,包括大部分IP头字段。这些日志可以在dmesg/syslog中看到。
额外选项:
选项 | 说明 |
--log-level level | 日志级别:emerg, alert, crit, error, warning, notice, info, debug |
--log-prefix prefix | 日志前缀,29个字符最多 |
--log-tcp-sequence | 记录TCP序列号 |
--log-tcp-options | 记录TCP选项 |
--log-ip-options | 记录IP选项 |
--log-uid | 记录生成封包的UID |
关于容器中的iptables logging:
- 默认情况下,来自容器(非全局网络命名空间)的iptable logging被忽略,你无法在 dmesg -w中看到
- 要改变此行为,你需要较新版本的内核,并且设置: echo 1 > /proc/sys/net/netfilter/nf_log_all_netns
仅用于nat表的PREROUTING,OUTPUT规则链,或者从这两规则链调用的用户自定义规则链。
该目标指定封包的目的地址应当被修改(且该连接上所有以后的封包应当执行同样的操作),规则应当停止检查。
应用场景:HTTP服务放置在内网主机上,前端有个防火墙配置公网IP,互联网用户通过此公网IP访问内网主机上的HTTP服务,这种情况需要DNAT
额外选项:
子选项 | 说明 |
--to-destination |
--to-destination [ipaddr[-ipaddr]][:port[-port]] 指定单个目的IP地址或者一个地址范围。如果指定了以下协议:tcp, udp, dccp, sctp,则可以指定一个端口范围。如果不指定端口,那么目的端口不会被改写。如果不指定IP地址,则仅仅会修改端口
|
--random | 随机化端口映射 |
--persistent | 对于一个客户端连接,给予相同的映射 |
举例:
1 2 3 4 5 6 7 |
# 如果公网客户端访问本防火墙的80端口 # 那么将其目标地址改写为192.168.0.200,目标端口不变 iptables -t nat -A PREROUTING -d 106.185.46.7 --dport 80 -j DNAT --to-destination 192.168.0.200 |
仅用于nat表的POSTROUTING,INPUT规则链,或者从这两规则链调用的用户自定义规则链。
该目标指定封包的源地址应当被修改(且该连接上所有以后的封包应当执行同样的操作),规则应当停止检查。
应用场景:局域网前端有个防火墙配置公网IP,内网主机通过该IP访问公网,为了内网主机能够访问外网,需要SNAT。
额外选项:
子选项 | 说明 |
--to-source |
--to-source [ipaddr[-ipaddr]][:port[-port]] 指定单个源IP地址或者一个地址范围。如果指定了以下协议:tcp, udp, dccp, sctp,则可以指定一个端口范围。如果不指定端口范围,那么小于512的源端口映射到512以下;512-1023之间的源端口映射到1024以下;其它源端口映射到1024以上。如果可能,则不去修改端口
|
--random | 随机化端口映射 |
--persistent | 对于一个客户端连接,给予相同的映射 |
举例:
1 2 3 4 5 6 7 |
# 对于来自192.168.0.0网段的所有出站请求 # 如果这些封包将通过eth0接口(外网网卡)传出去 # 那么,将封包源地址改写为106.185.46.7(即eth0的IP地址) iptables -t nat -A POSTROUTING -s 192.168.0.0/255.255.255.0 -o eth0 -j SNAT --to-source 106.185.46.7 |
仅用于nat表的POSTROUTING规则链。应当用于动态分配IP的网络连接,静态网络连接应当使用SNAT目标。该目标会自动从网络接口上获取当前IP地址,用来做源地址的SNAT
应用场景:本主机具有动态分配的公网地址,有客户端通过PPP连接(VPN)到本主机,并通过本主机上网。这时需要将客户端封包的源地址(PPP对端地址)转换为公网地址,但是由于本主机的公网地址是动态获取的,因此不能使用SNAT,只能使用MASQUERADE
额外选项:
子选项 | 说明 |
--to-ports port[-port] | 指定一组转换后使用的源端口。仅当协议指定为tcp, udp, dccp, sctp时可用 |
--random | 随机化端口映射 |
举例:
1 2 3 4 |
iptables -t nat -A POSTROUTING -s 192.168.0.0/255.255.255.0 -o eth0 -j MASQUERADE |
仅可用在mangle的PREROUTING链(或者从该链调用的用户自定义链)。此目标能够重定向包到一个本地套接字,但却不修改包头的任何信息。 它还能够改变封包的mark,进而辅助后续的策略路由。
额外选项:
子选项 | 说明 |
--on-port port | 重定向到的本地套接字的目标端口,0表示新的目标端口与原始端口相同 |
--on-ip address | 重定向到的本地套接字的目标IP地址,默认情况下是入口网卡的地址。仅仅当规则指定了-p tcp或-p udp时可用 |
--tproxy-mark value[/mask] | 为封包添加标记,以便选择正确的路由表,这里设置的fwmark可以用于高级路由。透明代理必须要此选项正确设置,否则封包将被转发走 |
可以认为是一种DNAT。
仅用于nat表的PREROUTING,OUTPUT规则链,或者从这两规则链调用的用户自定义规则链。该目标重定向封包到当前主机本身,通过把目的地址改写为入站网卡的主地址(本地生成的封包映射到localhost地址,对于IPv4即127.0.0.1)。
额外选项:
子选项 | 说明 |
--to-ports port[-port] |
重定向到的目的端口,或者目的端口的范围 如果不指定此选项,则目的端口不变 |
--random | 随机化端口映射 |
在连接上设置netfilter标记。标记为32bit。
额外选项:
子选项 | 说明 |
--set-xmark |
--set-xmark value[/mask] 如果指定mask,则将mask提供的bit归零,并和value 异或,得到最终的ctmark |
--save-mark |
--save-mark [--nfmask nfmask] [--ctmask ctmask] 拷贝封包标记(nfmark)为连接标记(ctmark)。算法: ctmark = (ctmark & ~ctmask) ^ (nfmark & nfmask) |
--restore-mark |
--restore-mark [--nfmask nfmask] [--ctmask ctmask] 拷贝连接标记为封包标记。算法: nfmark = (nfmark & ~nfmask) ^ (ctmark & ctmask) 此选项仅能用于mangle表 |
--and-mark bits | 等价于 --set-xmark 0/invbits,invbits是bits取反 |
--or-mark bits | 等价于--set-xmark bits/bits |
--xor-mark bits | 等价于--set-xmark bits/0 |
--set-mark value[/mask] | 仅仅mask中设置的那些bit,对应value中的bit的设置 |
在封包上设置netfilter标记。标记为32bit。
常常和基于fwmark的路由(需要iproute2)联用,这种情况下你需要在PREROUTING链mangle表中使用,以影响路由策略。
额外选项:
子选项 | 说明 |
--set-xmark value[/mask] | 如果指定mask,则将当前nfmark中mask提供的bit归零,并和value 异或,得到最终的nfmark |
--set-mark value[/mask] | 如果指定mask,则将当前nfmark中mask提供的bit归零,并和value或,得到最终的nfmark |
--and-mark bits | 和nfmark进行二进制与 |
--or-mark bits | 和nfmark进行二进制或 |
--xor-mark bits | 和nfmark进行二进制异或 |
只能用于mangle表,可以为封包添加缺失的校验和
可以用于识别某个网络接口已经空闲一定的时间
允许静态的把整个网络的IP地址映射到另外一个网络的IP地址
用于记录匹配包的日志
禁止匹配规则的封包的连接跟踪。你无法通过conntrack命令看到这样的连接
修改TCP的SYN封包的MSS值,用来控制连接的最大单个段的大小,一般限制到出站网络接口MTU-40
该目标用于修改封包的TTL
假设内网客户端机器IP地址为10.0.0.1;NAT网关内网地址10.0.0.2,外网地址10.4.80.14;外网服务器地址为9.135.102.75。
我们可以通过conntrack命令,在NAT网关上查看相关事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
conntrack -E --proto tcp -d 9.135.102.75 [NEW] tcp 6 120 SYN_SENT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 [UNREPLIED] src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [UPDATE] tcp 6 60 SYN_RECV src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [UPDATE] tcp 6 432000 ESTABLISHED src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED] [UPDATE] tcp 6 120 FIN_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED] [UPDATE] tcp 6 60 CLOSE_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED] [UPDATE] tcp 6 30 LAST_ACK src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED] [UPDATE] tcp 6 120 TIME_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED] # 过了2MSL [DESTROY] tcp 6 src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED] |
NAT表的特殊之处在于,仅新连接的第一个封包会经过此表。第一个封包的地址转换结果,会应用到连接的所有后续封包。
NAT网关接收到新连接后,创建conntrack条目:
10.0.0.1:56736->9.135.102.75:36000, 9.135.102.75:36000-> 10.0.0.1:56736
在iptables中,NAT网关的POSTROUTING钩子中的SNAT规则,会修改conntrack的应答目的地址
10.0.0.1:56736->9.135.102.75:36000, 9.135.102.75:36000->10.4.80.14:56736
这时,就到达上面命令输出的[NEW]状态了。请求/应答报文进行地址转换所需的信息已经完备。
iptables规则所做的事情,仅仅是修改conntrack规则而已。所有其它事情,包括请求封包的源地址修改,是由内核中的conntrack模块(nf_conntrack、nf_conntrack_ipv4)以及nat模块(nf_nat、nf_nat_ipv4)等负责的。
当NAT网关接收到应答报文后,会检索conntrack中的条目,并将匹配的条目关联到该封包( skb->_nfct)。这个条目将用于目的地址的还原。
注意以下几点:
- iptables不是netfilter本身,它是netfilter的用户。而conntrack和nat模块是netfilter的一部分
- 无法在POSTROUTING钩子+NAT表看到检查到应答报文,因为NAT表仅仅对新连接调用一次
- 连接跟踪、NAT转换的大部分工作由相应内核模块,而不是iptables完成
在iptables新老版本中,xtables锁的实现不同。在1.4.x中,锁基于abstract unix domain socket实现;1.6.x中则基于普通文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
func grabIptablesLocks(lockfilePath string) (iptablesLocker, error) { var err error var success bool l := &locker{} defer func(l *locker) { // Clean up immediately on failure if !success { l.Close() } }(l) // iptables 1.6.x和1.4.x版本的锁是不一样的 // 1.6.x的锁文件是:/run/xtables.lock // 下面的逻辑是1.6.x xtables_lock() 函数的粗略的重复实现 l.lock16, err = os.OpenFile(lockfilePath, os.O_CREATE, 0600) if err != nil { return nil, fmt.Errorf("failed to open iptables lock %s: %v", lockfilePath, err) } // 尝试加锁(1.6.x版本) if err := wait.PollImmediate(200*time.Millisecond, 2*time.Second, func() (bool, error) { if err := grabIptablesFileLock(l.lock16); err != nil { return false, nil } return true, nil }); err != nil { return nil, fmt.Errorf("failed to acquire new iptables lock: %v", err) } // 下面的逻辑是1.4.x xtables_lock() 函数的粗略的重复实现 if err := wait.PollImmediate(200*time.Millisecond, 2*time.Second, func() (bool, error) { // 1.4.x的锁是抽象Unidx Domain socket // 前缀的@表示套接字位于抽象命名空间(abstract namespace),不体现为文件系统中的文件 l.lock14, err = net.ListenUnix("unix", &net.UnixAddr{Name: "@xtables", Net: "unix"}) if err != nil { return false, nil } return true, nil }); err != nil { return nil, fmt.Errorf("failed to acquire old iptables lock: %v", err) } success = true return l, nil } // 加1.6锁 func grabIptablesFileLock(f *os.File) error { // 独占 非阻塞 return unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) } |
尝试修改iptables时,可能会因为锁被占用导致报错。这种报错没有专门的退出码,因此只能比对stderr输出来判定。
正常情况下,出现锁争用的概率比较低,可以使用下面的命令显式占用xtables锁:
1 2 3 4 5 |
# 1.4.x加锁 socat ABSTRACT-LISTEN:xtables - # 1.6.x加锁 flock -x /run/xtables.lock sleep 365d |
保持运行上述两个命令的Terminal打开,然后插入iptables规则。可以看到报错信息如下:
- v1.4.21版本:
- 不带-w参数:Another app is currently holding the xtables lock. Perhaps you want to use the -w option?
- 带有-w参数:Another app is currently holding the xtables lock; still 21s 0us time ahead to have a chance to grab the lock... Another app is currently holding the xtables lock. Stopped waiting after 30s.
- v1.6.0版本:报错同上
实际上,K8S中就是基于上述输出信息进行判断的:
1 2 3 |
func IsProxyLocked(err error) bool { return strings.Contains(err.Error(), "holding the xtables lock") } |
写的非常棒,很详细,方方面面都覆盖到了。
经常会到这篇文章进行速查。