跳转至

防火墙

主要作者

@iBug

本文编写中

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 阶段

从主机视角看 Netfilter 的各个阶段

在上图中,ROUTE 指路由决策

特别地,由本机发往本机(回环接口,即 lo)的数据包会依次经过 OUTPUT 和 POSTROUTING 阶段,由 lo 接口发出的同时也由 lo 接口收到,并再次经过 PREROUTING 和 INPUT 阶段后到达接收端 socket。 该路径的典型场景是使用 localhost127.0.0.1::1 等地址访问本机服务,但不包括 Unix socket1

路由决策与 Reroute check 是什么关系?

可能有部分读者见过 Wikipedia 的这张著名的 Netfilter packet flow

Netfilter flow chart

它与本文的图示有一处微妙的区别:路由决策位于 OUTPUT 阶段之前,而 OUTPUT 阶段后另有一个 Reroute check2。 事实上此图是更加准确的,但在大多数情况下,将路由决策视作位于 OUTPUT 之后更容易理解,可以从以下两点看出:

  1. 保持 OUTPUT 阶段与 PREROUTING 阶段的相似性:两个阶段均发生在路由决策之前,且 NAT 模式为仅可更改目的地址(DNAT)。
  2. 路由结果的准确性:数据包最终的路由结果基于经过 OUTPUT 或 PREROUTING 阶段修改后的元信息,如目的地址和防火墙标记等。

本文在介绍 iptables 的表时绘制了 Netfilter 视角的阶段图,能够更直观地反映出此「相似性」。

Reroute check 的细节

首先需要重复的一点是:数据包最终的路由结果是基于经过 OUTPUT 阶段后、进入 POSTROUTING(或 INPUT)阶段前的状态决定的。 那么既然数据包在 OUTPUT 阶段可能发生 MARK 或 DNAT 等修改,为什么不像外部传入的数据包一样,直接在 OUTPUT 阶段后进行路由决策呢? 作者认为有以下两种可能的原因:

  1. 只是一个历史遗留问题:早期的内核可能并没有考虑到 OUTPUT 阶段修改信息会导致路由变化,因此在数据包由本机进程发出后,直接进行路由决策。
  2. 「由本机往网卡发出」的数据包只会经过 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 而改变。

在搞清楚这些细节后,我们就知道为什么以下两种理解方式都是正确的:

  1. 路由决策位于 OUTPUT 之后:因为数据包最终的路由结果是基于经过 OUTPUT 阶段修改后的状态决定的。
  2. 路由决策位于 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)」的规则,就能减少大量数据包的匹配开销。例如:

iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
nft add rule ip filter input ct state established,related accept

在 conntrack 看来,数据包与连接的关系有以下几种:

NEW

该数据包将要建立一个新连接

ESTABLISHED

该数据包属于一个已经建立的连接

RELATED

该数据包将要建立一个新连接,但是由某一项 helper 认为是与一个已有连接相关联的,例如 FTP 的数据连接。

INVALID

该数据包不属于任何连接,conntrack 无法辨认

UNTRACKED

该数据包在 raw 表中已经被标记为 notrack(不要跟踪)

DNAT、SNAT

两个特殊的状态,仅用于匹配数据包(--ctstate),表示数据包经过了 DNAT / SNAT。

连接标记

Conntrack 除了记录连接的五元组(四层协议、源地址、目的地址、源端口、目的端口)外,还可以为连接记录一个「标记」(conntrack mark,CONNMARK)。 该标记可以在 iptables 或 nftables 规则中从数据包保存或恢复到数据包上,实现「数据包标记」与「连接标记」的双向互动,例如:

iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
iptables -t mangle -A POSTROUTING -j CONNMARK --save-mark
nft add rule ip mangle prerouting meta mark set ct mark
nft add rule ip mangle postrouting ct mark set mark

对于经过 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 -L
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 -L -p udp --dport 53

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 规则中使用,实现高效的地址匹配。例如:

iptables -A INPUT -m set --match-set blacklist src -j DROP

其中 --match-set blacklist src 表示从名为 blacklist 的 IPset 集合中匹配数据包的源地址。 相比于在 iptables 规则中逐条列出黑名单地址,使用 IPset 可以显著提高匹配效率,尤其是在黑名单地址数量较多的情况下。

如果集合是多元素类型的话,还可以指定端口号等其他字段进行匹配,例如:

ipset create blacklist hash:ip,port
iptables -A INPUT -m set --match-set blacklist src,dst -j DROP

此时 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 命令及其所有参数构成一条规则,规则的基本结构如下:

iptables [全局选项] [-t 表名] 链操作 规则匹配条件 目标

其中 -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 数据包内层的协议,如 tcpudpicmp 等。

--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 可以使用 -o
PREROUTING
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_DSTNAT_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 阶段

从 Netfilter 自己的视角看各个阶段,以及每个阶段可用的表

iptables-save

nftables


  1. 事实上 Unix socket 是一种 IPC 方式,与网络栈几乎无关,没有「路由」和「防火墙」等组件。 

  2. 此图的 2021 年的版本仍然有一处错误:Reroute check 发生在 OUTPUT 阶段内部,而 FORWARD 阶段后不经过 reroute check。细节可见 iptable_mangle_hook 函数。 

  3. ip_route_me_harderip6_route_me_harder 函数 

  4. IPv6 采用的判断条件有所不同,此处不再赘述。详情请见 ip6t_mangle_out 函数。 

  5. ipt_mangle_out 函数 

  6. nf_nat_ipv4_local_fn 函数 

  7. skb_dst_set 函数 

  8. ipt_do_table 函数 

  9. nf_nat_manip_pkt 

  10. nf_nat_inet_fn 

  11. 还记得上文为什么说 Wikipedia 的图更准确吗? 

  12. 该约束事实上是「NEW 或 RELATED」,具体可参考 nf_nat_inet_fn 函数。