防火墙¶
主要作者
本文编写中
Linux 内核网络栈中的防火墙模块称为 Netfilter,负责对进出主机的数据包进行过滤和修改。Netfilter 提供了一套强大的工具,用于实现各种防火墙功能,如包过滤、网络地址转换(NAT)和连接跟踪等。
Netfilter¶
Netfilter 阶段¶
Netfilter 将数据包的处理过程划分为 5 个阶段,并在每个阶段提供 hook 点,允许用户定义规则来控制数据包的流动。这些阶段包括:
- PREROUTING /
NF_INET_PRE_ROUTING -
数据包由网络接口(网卡)接收后,首先进入 PREROUTING 阶段。这个阶段通常用于更改目的地址(DNAT)。
- INPUT /
NF_INET_LOCAL_IN -
数据包在经过路由决策后,如果目标是本地主机,则进入 INPUT 阶段。这个阶段通常用于过滤,管控对本地服务的访问,以及(如有必要)更改源地址(SNAT)。
- FORWARD /
NF_INET_FORWARD -
如果由外部网络进入的数据包的目标不是本地主机,而是需要由本机转发到其他主机,则进入 FORWARD 阶段。这个阶段通常用于过滤。
- OUTPUT /
NF_INET_LOCAL_OUT -
由本地主机发出的数据包首先进入 OUTPUT 阶段。这个阶段通常用于过滤,管控本地应用程序对外的网络访问,以及(如有必要)更改目的地址(DNAT)。
- POSTROUTING /
NF_INET_POST_ROUTING -
数据包在离开主机由网卡发出之前,进入 POSTROUTING 阶段。这个阶段通常用于更改源地址(SNAT)。
这些阶段对应 iptables 的内置链(chains)或 nftables 的 hook 点。
从主机的视角来看,数据包经过 Netfilter 的各个阶段的路径如下图所示:
从主机视角看 Netfilter 的各个阶段
在上图中,ROUTE 指路由决策。
特别地,由本机发往本机(回环接口,即 lo)的数据包会依次经过 OUTPUT 和 POSTROUTING 阶段,由 lo 接口发出的同时也由 lo 接口收到,并再次经过 PREROUTING 和 INPUT 阶段后到达接收端 socket。
该路径的典型场景是使用 localhost、127.0.0.1 或 ::1 等地址访问本机服务,但不包括 Unix socket1。
路由决策与 Reroute check 是什么关系?
可能有部分读者见过 Wikipedia 的这张著名的 Netfilter packet flow:
它与本文的图示有一处微妙的区别:路由决策位于 OUTPUT 阶段之前,而 OUTPUT 阶段后另有一个 Reroute check2。 事实上此图是更加准确的,但在大多数情况下,将路由决策视作位于 OUTPUT 之后更容易理解,可以从以下两点看出:
- 保持 OUTPUT 阶段与 PREROUTING 阶段的相似性:两个阶段均发生在路由决策之前,且 NAT 模式为仅可更改目的地址(DNAT)。
- 路由结果的准确性:数据包最终的路由结果基于经过 OUTPUT 或 PREROUTING 阶段修改后的元信息,如目的地址和防火墙标记等。
本文在介绍 iptables 的表时绘制了 Netfilter 视角的阶段图,能够更直观地反映出此「相似性」。
Reroute check 的细节
首先需要重复的一点是:数据包最终的路由结果是基于经过 OUTPUT 阶段后、进入 POSTROUTING(或 INPUT)阶段前的状态决定的。 那么既然数据包在 OUTPUT 阶段可能发生 MARK 或 DNAT 等修改,为什么不像外部传入的数据包一样,直接在 OUTPUT 阶段后进行路由决策呢? 作者认为有以下两种可能的原因:
- 只是一个历史遗留问题:早期的内核可能并没有考虑到 OUTPUT 阶段修改信息会导致路由变化,因此在数据包由本机进程发出后,直接进行路由决策。
- 「由本机往网卡发出」的数据包只会经过 OUTPUT 和 POSTROUTING 两个阶段,而仅有 OUTPUT 阶段具有 filter 表。在这种情况下,若要限制本机进程允许发出数据包的网络接口,则 OUTPUT 阶段必须支持
-o参数,即需要在 OUTPUT 阶段前进行一次(初步的)路由决策。
为了兼顾「在 OUTPUT 链中可以使用 -o」和最终路由决策的正确性,内核采用了 Reroute 机制3,即针对 OUTPUT 阶段:
- 数据包由本机进程
send()到 Netfilter 之后,首先进行一次路由决策,确定初步的输出接口。 - 在 mangle 表中,若数据包的源地址、目的地址、防火墙标记或 TOS 字段这 4 个元信息发生了变化4,则重新进行一次路由决策5。
- 在 nat 表中,若数据包的目的地址发生了变化,则还会重新进行一次路由决策6。
需要注意的是,尽管数据包可能在 mangle 表和 nat 表中已经经过了至多两次额外路由决策,但其在 filter 表中时,-o 参数所匹配的输出接口始终是最初的路由决策结果。
这是因为最终生效的路由决策存储在数据包的 skb->_skb_refdst 字段中7,而 Netfilter 在进行匹配时使用的是 nf_hook_state->out 字段8,该字段在数据包进入 OUTPUT 阶段之前就已经确定,并不会随着后续的 reroute check 而改变。
在搞清楚这些细节后,我们就知道为什么以下两种理解方式都是正确的:
- 路由决策位于 OUTPUT 之后:因为数据包最终的路由结果是基于经过 OUTPUT 阶段修改后的状态决定的。
- 路由决策位于 OUTPUT 之前,且 OUTPUT 后另有重新路由:因为 OUTPUT 阶段需要支持
-o匹配方式,该信息依赖于初步的路由决策结果。
Hook 的优先级¶
Netfilter 为 hook 定义了一系列优先级,优先级越高的 hook 越早执行。特别地,iptables 的各个表是注册在对应的优先级上的,因此不同表的处理顺序也由 hook 的优先级决定。
在同一个阶段中,不同 hook 的处理顺序为 raw → (conntrack) → mangle → nat (DNAT) → filter → security → nat (SNAT)。
该顺序定义在 enum nf_ip_hook_priorities 中,数值越小则优先级越高。
conntrack¶
连接跟踪器(Connection Tracker,conntrack,也经常简称为 CT)是 Netfilter 的一个核心组件,用于跟踪网络连接的状态。
连接跟踪¶
Conntrack 表的一个重要作用是支持有状态防火墙,允许 Netfilter 组件获取连接状态,并据此做出过滤决策。 一个典型的例子是,允许已建立连接的数据包通过防火墙,而仅过滤新连接请求。 由于 iptables 和 nftables 的规则链都是按顺序线性执行的,若在规则链开头插入「允许 conntrack 状态为已建立(ESTABLISHED)」的规则,就能减少大量数据包的匹配开销。例如:
在 conntrack 看来,数据包与连接的关系有以下几种:
- NEW
-
该数据包将要建立一个新连接
- ESTABLISHED
-
该数据包属于一个已经建立的连接
- RELATED
-
该数据包将要建立一个新连接,但是由某一项 helper 认为是与一个已有连接相关联的,例如 FTP 的数据连接。
- INVALID
-
该数据包不属于任何连接,conntrack 无法辨认
- UNTRACKED
-
该数据包在 raw 表中已经被标记为 notrack(不要跟踪)
- DNAT、SNAT
-
两个特殊的状态,仅用于匹配数据包(
--ctstate),表示数据包经过了 DNAT / SNAT。
连接标记¶
Conntrack 除了记录连接的五元组(四层协议、源地址、目的地址、源端口、目的端口)外,还可以为连接记录一个「标记」(conntrack mark,CONNMARK)。
该标记可以在 iptables 或 nftables 规则中从数据包保存或恢复到数据包上,实现「数据包标记」与「连接标记」的双向互动,例如:
对于经过 NAT 的连接,CONNMARK 在实现「源进源出」时非常重要,
NAT 支持¶
在 Netfilter 中,用户定义的 NAT 规则只会对新连接生效,而已建立的连接的后续数据包则由 conntrack 负责处理 NAT,确保同一连接内的所有数据包都能被正确处理。
例如,在一个典型的家用路由器上,当内网主机向外网发起连接时,路由器上的 NAT 规则会将数据包的源地址改写为路由器的 WAN 口地址,整个流程如下:
- 内网主机对外建立一个新的连接,发送第一个数据包到路由器
- 第一个数据包经过 POSTROUTING 阶段,被 MASQUERADE 改写源地址
- 此时 conntrack 记录该连接双向的五元组(共 9 个字段,其中协议号只需记录一次),包括正向(original)的四元组和反向(reply)的四元组
- 后续的数据包(不论方向)经过该路由器时,在 conntrack 阶段(
NF_IP_PRI_CONNTRACK)匹配到某一方向的四元组,由 conntrack 改写为另一方向的四元组的反向地址9,使对端主机能够正确接收数据包。
对于经过 NAT 的连接,Linux 要求属于该连接的所有数据包都由相同的网卡发出(区分正反方向),否则会停止记录连接信息并丢掉即将发往不同网卡的数据包10。
conntrack 命令¶
conntrack(8) 可以查看和管理内核中的 conntrack 表,其记录了所有经过主机的数据包的连接状态信息。
最常用的命令是列出当前的连接跟踪条目:
conntrack 输出示例
udp 17 91 src=192.0.2.2 dst=8.8.8.8 sport=39043 dport=53 src=8.8.8.8 dst=198.51.100.1 sport=53 dport=39043 [ASSURED] mark=1 use=1
- 协议:
udp(协议号 17) - 剩余超时时间:91 秒
- 正向四元组 src, dst, sport, dport
-
反向四元组 src, dst, sport, dport
本例中,当前主机为负责进行 NAT 的出口路由器,所以反向的 dst 地址为路由器的 WAN 口地址。
-
连接状态标记,如
[ASSURED] - 连接标记
mark - 引用计数器
use
conntrack 命令支持一些与 iptables 语法相同的匹配条件,可以用来过滤输出的连接条目,例如:
conntrack 命令也可以对 conntrack 表进行修改操作,但相比于查询类操作较为不常用,因此具体用法可以参考 conntrack(8)。
另外,conntrack -E 命令可以实时监控 conntrack 表的变化,适合用于调试和分析网络连接的动向。-E 操作同样支持匹配条件,可以过滤出特定的连接事件,方便查找和分析。
IPset¶
IPset 是 Netfilter 的一个扩展模块,用于管理 IP 地址集合(set),可以高效地匹配大量的 IP 地址或网络段。
IPset 的用户态工具为 ipset(8),可以创建、删除和管理 IPset 集合。
IPset 支持多种集合类型,如 hash:ip(单个 IP 地址的哈希集合)、hash:net(CIDR 网段的哈希集合)和 bitmap:ip(IP 地址的 bitmap 集合)等。
完整的集合类型和用法可以参考 ipset help 的输出,以下给出一些基本的示例:
ipset create blacklist hash:ip
ipset add blacklist 192.0.2.1
ipset del blacklist 192.0.2.1
# 即使地址已存在(add 操作)或不存在(del 操作),也不会报错
ipset add -exist blacklist 192.0.2.1
ipset del -exist blacklist 192.0.2.1
IPset 集合可以在 iptables 或 nftables 规则中使用,实现高效的地址匹配。例如:
其中 --match-set blacklist src 表示从名为 blacklist 的 IPset 集合中匹配数据包的源地址。
相比于在 iptables 规则中逐条列出黑名单地址,使用 IPset 可以显著提高匹配效率,尤其是在黑名单地址数量较多的情况下。
如果集合是多元素类型的话,还可以指定端口号等其他字段进行匹配,例如:
此时 blacklist 集合中的每个元素都包含一个 IP 地址和一个端口号,命令中的 src,dst 表示匹配数据包的源地址和目的端口号。
完整的匹配能力请在 iptables-extensions(8) 手册页中查阅 set 模块的说明。
类似地,IPset 中的元素也可以通过 iptables 增减:
iptables -m my-fantastic-match -j SET --add-set blacklist src [--exist]
iptables -m my-fantastic-match -j SET --del-set blacklist src [--exist]
skbinfo¶
部分集合类型支持一项 skbinfo 扩展,可以为每个条目存储额外的信息(skbmark、skbprio、skbqueue),并在匹配时将这些信息附加到数据包上(--map-mark、--map-prio、--map-queue)。
例如:
ipset create qos hash:ip skbinfo
ipset add qos 192.0.2.1 skbmark 0x1 skbprio 5 skbqueue 2
iptables -t mangle -A PREROUTING -m set --match-set qos src --map-mark --map-prio --map-queue -j ACCEPT
iptables¶
iptables 是 Netfilter 的用户空间工具,用于管理防火墙规则。 iptables 将规则组织成不同的表(table),每个表包含多个链(chain),每个链对应一个 Netfilter 阶段。
操作 IPv4 防火墙规则时使用 iptables 命令,操作 IPv6 防火墙规则时使用 ip6tables 命令。除此之外,两者的用法完全相同。
iptables 命令¶
iptables 使用 GNU getopt_long 风格的命令行参数,即以短横线 - 开头的单字符选项和以双短横线 -- 开头的长选项。
每一条 iptables 命令及其所有参数构成一条规则,规则的基本结构如下:
其中 -t 也是全局选项之一,用于指定要操作的表。若省略 -t,则默认该命令操作 filter 表。
链操作¶
对 iptables 链的操作类似增删改查,常用的操作有以下几种:
iptables -A 链名 ... # 追加规则到链尾
iptables -I 链名 [位置] ... # 插入规则到指定位置(默认位置为链头)
iptables -D 链名 序号 # 删除指定规则
iptables -R 链名 序号 ... # 替换指定规则
iptables -L [链名] # 列出指定链的所有规则
iptables -S [链名] # 以命令行格式列出指定链的所有规则
iptables -P 链名 目标 # 设置链的默认策略
iptables -F [链名] # 清空指定链的所有规则
iptables -Z [链名] # 将指定链的所有规则的计数器归零
iptables -N 链名 # 新增一条用户自定义链
iptables -X [链名] # 删除一条用户自定义链
例如,要将 iptables 的 filter 表恢复到初始状态,可以执行以下命令:
iptables -F
iptables -X
iptables -Z
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
注意到 -L, -S, -F, -X, -Z 这些操作都可以省略链名参数,表示对所有链进行操作。
完整的 iptables 规则语法和选项可以参考 iptables(8) 和 iptables-extensions(8) 手册页。
规则匹配条件¶
在「规则匹配条件」部分,可以使用多种匹配模块来指定数据包的特征,如源地址、目的地址、协议类型、端口号等。 匹配的顺序是按命令行参数的顺序依次进行的,只有当数据包满足所有匹配条件时,才会应用该规则的目标。
常用的匹配规则包括:
-s/--source/-d/--destination-
指定源地址或目的地址,可以是单个 IP 地址、CIDR 块或域名。一个
-s或-d参数可以指定最多 15 个 IP 地址或 CIDR 块,多个地址之间用逗号分隔。 -p/--protocol-
指定 IP 数据包内层的协议,如
tcp、udp、icmp等。 --sport/--dport-
指定源端口号或目的端口号,可以是单个端口号或端口范围(格式为
起始端口-结束端口)。该选项只能与-p tcp或-p udp一起使用。该参数事实上由
-m tcp或-m udp模块提供,而 iptables 会根据-p参数自动加载相应的模块,因此不需要显式指定-m tcp或-m udp。 出于同样的原因,你也无法在一条规则内同时匹配 TCP 和 UDP 协议的端口号,需要分别使用两条规则来实现。此参数只能匹配一个端口号或一个端口范围(包含起止端口号),如果需要匹配多个不连续的端口号,可以使用
-m multiport --sports/--dports模块。 -i/--in-interface/-o/--out-interface-
指定数据包的输入接口或输出接口,可以是接口名称(如
eth0)或前缀通配符(如eth+)。由于 Netfilter 的结构和设计考虑,
-i(输入接口)和-o(输出接口)这两个匹配选项在不同阶段的可用性有所不同:阶段 可以使用 -i可以使用 -oPREROUTING INPUT FORWARD OUTPUT 11 POSTROUTING -m conntrack --ctstate-
调用 conntrack 模块判断当前数据包所属的连接类型,参见上文的 conntrack 段落。
-m set --match-set set_name src-
调用
set模块从 IPset 中匹配数据包的地址和端口,参见上文的 IPset 段落。
动作¶
若一个数据包满足某条规则的所有匹配条件,就会进执行该规则的目标(-j / -g 参数)。目标可以是内置目标(如 ACCEPT、DROP、REJECT 等),也可以是用户自定义链的名称(跳转至自定义链继续处理)。
-j / --jump 与 -g / --goto 的区别是,当跳转到自定义链后,-j 会在自定义链处理完毕后返回到原链继续处理,而 -g 则不会返回原链,视作原链已完成处理。可以类比在 shell 中执行命令时的 source(对应 -j)和 exec(对应 -g)的区别。
其中,内置目标包括:
- ACCEPT:接受数据包,允许其继续传输。
- DROP:丢弃数据包,不发送任何响应。
- REJECT:拒绝数据包,并发送响应给对端(默认响应为 ICMP port unreachable)。
其他常用的、由扩展模块提供的目标包括:
-
DNAT (REDIRECT) / SNAT (MASQUERADE):用于网络地址转换(NAT),分别用于更改数据包的目的地址和源地址。
其中 REDIRECT 和 MASQUERADE 分别是 DNAT 和 SNAT 的特殊形式,用于将数据包的目的地址和源地址改写为对应网卡上的地址。
-
LOG:记录数据包信息到系统日志,通常与其他目标结合使用。
- MARK:为数据包打上防火墙标记(firewall mark),通常与路由策略结合使用。
- CONNMARK:为数据包对应的 conntrack 连接打上标记,或从连接中恢复标记。
每条内置链都有一个默认策略(-P / --policy),当数据包经过该链但未匹配到任何规则时,会由该默认策略处理。默认策略只能是 ACCEPT 或 DROP。
例:科大镜像站上限制 80 / 443 端口并发连接数
iptables -A LIMIT \
-p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN \
-m multiport --dports 80,443 \
-m connlimit --connlimit-above 12 --connlimit-mask 29 --connlimit-saddr \
-j REJECT --reject-with tcp-reset
这条命令的理解方式如下:
-
iptables -A LIMIT:将规则追加(-A)到名为 LIMIT 的链中,该规则只会对 IPv4 数据包生效。IPv6 防火墙规则需要使用ip6tables。LIMIT 是我们自定义的一条链,在 INPUT 阶段调用,负责执行类似的限流规则。
-
-p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN:匹配 TCP 协议的数据包,且匹配仅有 SYN 标志位被设置的数据包(即新连接请求)。- 等价的写法是
-p tcp --syn - 另一个(几乎)等价的写法是
-m conntrack --ctstate NEW,该写法调用了 conntrack 模块,由 conntrack 判断什么样的数据包是新连接请求。
- 等价的写法是
-m multiport --dports 80,443:调用multiport模块,匹配目的端口为 80 或 443 的数据包。-m connlimit --connlimit-above 12 --connlimit-mask 29 --connlimit-saddr:调用connlimit模块,匹配来自同一子网(掩码长度 29,即每 8 个 IP 地址为一个子网)且当前已建立连接数超过 12 的数据包。-j REJECT --reject-with tcp-reset:目标为 REJECT,即主动拒绝匹配的数据包,并发送 TCP RST 响应给对端。
iptables 表¶
iptables 的主要表类型有以下几种:
- filter
-
默认表,用于包过滤,其中 DROP 和 REJECT 目标通常只用在此表中。
- nat
-
用于网络地址转换(NAT),如源地址转换(SNAT)和目的地址转换(DNAT)。其中「伪装」(MASQUERADE)是一种特殊的 SNAT 方式,让内核根据出接口网卡上配置的地址自动决定替换后的源地址,通常用于动态 IP 地址的场景。
需要注意的是,nat 表的 PREROUTING 和 OUTPUT 链只能使用 DNAT 目标,而 INPUT 和 POSTROUTING 链只能使用 SNAT(或 MASQUERADE)目标。 这是因为 iptables 将 Netfilter 的两种 hook 优先级(
NAT_DST和NAT_SRC)都放进了 nat 表,因此尽管四个链都在 nat 表中,它们实际上是分属于两种不同的 Netfilter hook。特别的是,仅有建立新连接的数据包会经过 nat 表,而已经建立连接的数据包不会经过 nat 表,而是由 conntrack 模块处理。 对于用户而言,可以理解为「nat 表自带
--ctstate NEW约束12,之后的数据包都使用已经转换后的地址进行通信」。 - mangle
-
用于修改原始数据包,如更改 TOS(服务类型)或 TTL(生存时间),也包括打上防火墙标记(
-j MARK)。这个表通常用于高级的包处理。 - raw
-
改变 conntrack 对当前数据包的行为,可以标记为不经过 conntrack(不进行跟踪,如
-j CT --notrack),或引入其他 conntrack 帮助模块(如-j CT --helper)。
另有 security 表用于 SELinux 等安全模块的集成,但在大多数系统(尤其是 Debian 系列发行版)中不常用,因此本文不作详细讨论。Security 表与 filter 表适用于相同的阶段(Netfilter hook 点),且运行在 filter 表之后,即能够进入 security 表中的数据包都已由 filter 表标记为接受(ACCEPT)了。
iptables 的每个表都会注册到对应的 Netfilter hook 优先级上,因此同一个阶段(例如 PREROUTING)中,不同表的处理顺序与 hook 的优先级相同。
各个表在各个阶段的可用性如下表所示:
| 阶段 | filter / security | nat | mangle | raw |
|---|---|---|---|---|
| PREROUTING | DNAT only | |||
| INPUT | SNAT only | |||
| FORWARD | ||||
| OUTPUT | DNAT only | |||
| POSTROUTING | SNAT only |
若从 Netfilter 自己的视角,将网卡和本地进程(数据包的来源和接收者)都看作外部元素的话,各个阶段及其可用的表和处理顺序如下图所示:
从 Netfilter 自己的视角看各个阶段,以及每个阶段可用的表
iptables-save¶
nftables¶
-
事实上 Unix socket 是一种 IPC 方式,与网络栈几乎无关,没有「路由」和「防火墙」等组件。 ↩
-
此图的 2021 年的版本仍然有一处错误:Reroute check 发生在 OUTPUT 阶段内部,而 FORWARD 阶段后不经过 reroute check。细节可见
iptable_mangle_hook函数。 ↩ -
IPv6 采用的判断条件有所不同,此处不再赘述。详情请见
ip6t_mangle_out函数。 ↩ -
ipt_mangle_out函数 ↩ -
skb_dst_set函数 ↩ -
ipt_do_table函数 ↩ -
该约束事实上是「NEW 或 RELATED」,具体可参考
nf_nat_inet_fn函数。 ↩