Linux 基础体系 · 第 20/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 防火墙与 NAT:Netfilter、nftables、连接跟踪和规则治理

一、先建立整体模型

Linux 防火墙问题通常包含四个不同层次:

  1. 数据包如何进入 Linux 网络栈:网卡、驱动、协议栈、路由和 Socket。
  2. 内核在哪些位置观察或修改数据包:这由 Netfilter 提供。
  3. 如何写出匹配和动作规则:现代 Linux 通常使用 nftables
  4. 如何识别“同一条连接”的后续数据包:这由 连接跟踪(conntrack) 提供,NAT 也依赖它保存转换关系。

因此,nftables 不是一个独立于内核的数据平面防火墙;它是用户空间的规则管理工具和表达方式,规则最终由 Linux 内核中的 Netfilter、连接跟踪以及相关模块执行。

一个简化的数据流如下:

flowchart LR
    NIC[网卡/驱动] --> RX[接收路径]
    RX --> PREROUTING[Netfilter PREROUTING]
    PREROUTING --> ROUTE{路由判断}
    ROUTE -->|目标是本机| INPUT[Netfilter INPUT]
    INPUT --> SOCKET[本机 Socket]
    ROUTE -->|需要转发| FORWARD[Netfilter FORWARD]
    FORWARD --> POSTROUTING[Netfilter POSTROUTING]
    SOCKET --> OUTPUT[Netfilter OUTPUT]
    OUTPUT --> POSTROUTING
    POSTROUTING --> TX[网卡发送]
    PREROUTING -.-> CT[连接跟踪]
    INPUT -.-> CT
    FORWARD -.-> CT
    OUTPUT -.-> CT
    POSTROUTING -.-> CT

图中的顺序是理解防火墙的基础:

  • PREROUTING 发生在路由判断之前;
  • INPUT 只处理最终交给本机 Socket 的包;
  • FORWARD 只处理经过本机转发的包;
  • OUTPUT 处理本机产生的包;
  • POSTROUTING 发生在路由决定之后、实际发出之前。

“访问本机服务”和“经过本机转发”是两条不同路径,不能用同一条规则替代。


二、Netfilter:内核中的数据包处理框架

2.1 Netfilter 不是一张规则表

Netfilter 是 Linux 内核提供的一组网络包处理钩子(hook)和扩展机制。它负责在数据包经过网络栈的关键位置时,允许内核模块执行:

  • 丢弃或接受数据包;
  • 修改源地址、目标地址和端口;
  • 记录日志;
  • 进行连接跟踪;
  • 执行队列、策略路由或其他扩展动作。

Netfilter 本身不规定必须使用某一种命令。历史上有:

  • iptables
  • ip6tables
  • ebtables
  • arptables

现代系统通常使用 nft 命令配置 nftables 规则。某些发行版仍提供 iptables 命令,但它可能是:

  • 旧的 legacy 实现;
  • 基于 nftables 的兼容层,即 iptables-nft

因此,看到 iptables -L 的输出并不能自动说明系统正在使用哪一套实现。应先检查:

iptables --version
nft --version
sudo nft list ruleset

如果输出包含 nf_tables,说明 iptables 命令可能正在通过 nftables 兼容层工作。生产环境中应避免同时让多个工具管理同一套规则,否则会出现“命令显示的配置”和“实际期望的配置”相互覆盖的问题。

2.2 五个主要 Netfilter hook

常用的 IPv4/IPv6 主机和路由器路径可以抽象为:

Hook 典型位置 常见用途
prerouting 路由前 DNAT、早期过滤、标记
input 路由后、本机交付前 保护本机服务
forward 路由后、转发时 保护经过本机的流量
output 本机发包时 限制本机主动连接
postrouting 路由后、发出前 SNAT、Masquerade

这里的“路由后”并不是说数据包已经发送出去,而是内核已经根据当前目标地址选择了本地交付、转发或输出接口。

一个关键因果关系是:

  • DNAT 通常需要在 prerouting 中完成,因为路由判断必须看到转换后的目标地址;
  • SNAT 通常需要在 postrouting 中完成,因为出口接口和最终发送路径已经确定;
  • 过滤规则的位置必须与流量路径匹配,向本机服务开放端口应看 input,向后端转发端口开放应看 forward

2.3 Hook、链和优先级

在 nftables 中,规则放在链(chain)里。链有两种:

  1. 基础链(base chain):绑定到 Netfilter hook,真正接收数据包。
  2. 普通链(regular chain):由其他链跳转进入,用于组织规则。

基础链的基本结构如下:

chain input {
    type filter hook input priority filter;
    policy drop;

    tcp dport 22 accept
}

其中:

  • type filter 表示这是过滤链;
  • hook input 表示绑定到 input hook;
  • priority filter 表示在默认过滤优先级附近执行;
  • policy drop 表示没有规则接受的数据包最终丢弃;
  • tcp dport 22 accept 表示匹配 TCP 目标端口 22 时接受。

优先级不是“规则行号”。同一个 hook 可以挂载多个基础链,数值越小通常越早执行。NAT 链、连接跟踪和过滤链之间存在预定义的处理顺序;自定义优先级时必须理解其影响,否则可能发生:

  • 在 DNAT 前匹配原始目标地址;
  • 在连接跟踪建立前访问依赖 ct 的信息;
  • 在另一个链已经丢弃数据包后,后续链根本没有机会执行。

普通防火墙规则通常不需要自定义复杂优先级。除非确实要控制多个框架或链之间的顺序,否则使用 priority filterpriority dstnatpriority srcnat 等标准值更容易维护。


三、nftables:用规则描述数据包处理

3.1 表、链、规则、集合和映射

nftables 的基本对象是:

  • 表(table):按地址族组织规则,例如 ipip6inetarpbridgenetdev
  • 链(chain):保存规则;
  • 规则(rule):由匹配条件和 verdict 组成;
  • 集合(set):保存多个地址、端口或其他元素;
  • 映射(map):把一个键映射到一个值,例如把不同端口映射到不同后端;
  • 计数器(counter):统计匹配的数据包和字节数。

地址族的选择影响规则能匹配的协议:

  • ip:IPv4;
  • ip6:IPv6;
  • inet:同时处理 IPv4 和 IPv6 的过滤规则;
  • bridge:二层桥接路径;
  • netdev:更靠近网卡接收路径,可用于某些早期过滤场景。

例如,主机过滤规则可以写在 inet 表中,但 NAT 示例常写成独立的 ip 表,以避免不同内核和发行版版本对跨地址族 NAT 支持的差异。

3.2 Verdict:规则的结果

常见 verdict 包括:

  • accept:接受当前处理路径中的数据包;
  • drop:静默丢弃;
  • reject:丢弃并向对端返回错误响应;
  • jump chain_name:进入另一个普通链,返回后继续;
  • goto chain_name:进入另一个链,通常不返回到当前链;
  • return:从当前普通链返回;
  • continue:继续下一条规则。

accept 的作用边界需要特别注意:它表示当前 nftables 链接受该包,但如果系统中还有更早或更晚的其他框架、链或安全策略,最终行为仍需结合整体配置验证。对于单一 nftables 规则集,通常可以把它理解为允许该包继续经过网络栈。

规则的处理顺序是从上到下。比如:

tcp dport 22 drop
ip saddr 192.0.2.10 accept

来自 192.0.2.10 的 TCP/22 会先被丢弃,第二条规则不会挽救它。改成:

ip saddr 192.0.2.10 tcp dport 22 accept
tcp dport 22 drop

才表达“只允许该地址访问 SSH”。

3.3 一个可检查的主机防火墙配置

下面的配置保护本机,而不是充当路由器:

flush ruleset

table inet host_filter {
    chain input {
        type filter hook input priority filter;
        policy drop;

        # 本机回环通信
        iifname "lo" accept

        # 已建立连接和相关连接
        ct state established,related accept

        # 无效连接状态
        ct state invalid drop

        # 允许来自管理网段的 SSH
        ip saddr 192.0.2.0/24 tcp dport 22 accept

        # IPv4 ICMP;实际生产规则应根据网络策略细化
        ip protocol icmp accept

        # IPv6 邻居发现、路径 MTU 等依赖 ICMPv6
        ip6 nexthdr icmpv6 accept
    }

    chain forward {
        type filter hook forward priority filter;
        policy drop;
    }

    chain output {
        type filter hook output priority filter;
        policy accept;
    }
}

检查语法而不加载:

sudo nft -c -f host-filter.nft

若检查成功,通常没有输出,并返回状态码 0。加载配置:

sudo nft -f host-filter.nft

查看内核当前实际规则:

sudo nft list ruleset

这个配置有几个前置条件和风险:

  • 必须确认管理来源确实位于 192.0.2.0/24,否则 SSH 会被拒绝;
  • 如果通过远程 SSH 加载配置,policy drop 可能立刻把自己锁在服务器外;
  • ip6 nexthdr icmpv6 accept 较宽,生产环境可按 ICMPv6 类型细化,但不能简单全部禁止,否则可能破坏邻居发现和路径 MTU;
  • output policy accept 只保护入站,不限制本机程序主动联网;
  • 如果系统由 firewalld、Docker、Kubernetes 或云厂商代理管理规则,直接 flush ruleset 可能删除其他组件需要的规则。

更安全的远程变更流程是:

  1. 在控制台、带外管理或第二个会话中保留恢复路径;
  2. 先用 nft -c 检查;
  3. 通过 nft -f 一次性加载完整事务;
  4. 立即从多个网络位置验证;
  5. 配置自动回滚,例如使用 systemd 定时任务或发行版提供的确认机制。

四、连接跟踪:防火墙如何理解“连接”

4.1 数据包和连接不是同一个概念

IP 层只看到独立的数据包。防火墙的状态匹配却需要知道:

  • 这个包是否属于一个已经存在的流;
  • 它是不是一个新连接;
  • 它是否是已有连接的响应;
  • 它是否与某个连接相关,例如 FTP、ICMP 错误或某些辅助协议。

连接跟踪(conntrack) 在内核中为数据包建立流状态。经典五元组是:

(src_ip, src_port, dst_ip, dst_port, protocol)(src\_ip,\ src\_port,\ dst\_ip,\ dst\_port,\ protocol)

例如:

192.0.2.10:53124 -> 198.51.100.20:443 TCP

但实际实现还要考虑方向、网络命名空间、超时、TCP 标志和 NAT 关联等因素。因此,不能把 conntrack 简化为“只存五元组的哈希表”。

4.2 常见状态

nftables 中常见的 ct state 包括:

  • new:被识别为新流;
  • established:已建立的流;
  • related:与已有连接有协议或语义关联的流;
  • invalid:无法对应有效连接状态;
  • untracked:未经过连接跟踪的流。

对 TCP 来说,连接跟踪通常会观察 SYN、SYN-ACK、ACK、FIN、RST 等标志;对 UDP 来说,因为 UDP 没有握手和关闭过程,状态主要依赖方向、流量和超时。

典型状态规则:

ct state invalid drop
ct state established,related accept

它的推导过程是:

  1. 第一个合法新连接包不匹配 established,继续执行后续服务规则;
  2. 服务规则允许它,例如 tcp dport 443 accept
  3. 连接跟踪记录该流;
  4. 服务端返回包被识别为已有流的反向方向,匹配 established
  5. 后续数据包不需要重新匹配完整的服务访问策略。

如果把 established,related accept 放在服务规则之前,表示“已经允许的连接继续通过”;如果放在服务规则之后,已建立连接也可能先被其他拒绝规则拦截。通常应根据明确的安全目标决定位置,而不是机械套用。

4.3 conntrack 表是有限资源

连接跟踪为每个流保存状态,因此存在容量和内存成本。查看相关内核参数:

sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max

查看具体条目通常需要 conntrack 工具:

sudo conntrack -L

输出可能类似:

tcp  6 431999 ESTABLISHED \
src=192.0.2.10 dst=198.51.100.20 sport=53124 dport=443 \
src=198.51.100.20 dst=192.0.2.10 sport=443 dport=53124 \
mark=0 use=1

其中剩余时间、状态和显示字段会因工具版本而变化。conntrack -L 失败可能是因为:

  • 未安装 conntrack 用户空间工具;
  • 当前用户没有足够权限;
  • 内核模块未加载;
  • 当前网络命名空间中没有相关条目。

连接跟踪表耗尽时,新连接可能无法建立,即使 nftables 规则本身看起来允许流量。高并发环境不能只调大 nf_conntrack_max,还必须评估内存、超时、短连接攻击和应用连接复用策略。


五、NAT:地址转换及其状态关系

5.1 NAT 的形式化描述

NAT(Network Address Translation)把一个方向上的地址或端口映射为另一个值。以源地址转换为例:

(src_ip, src_port, dst_ip, dst_port)(src_ip, src_port, dst_ip, dst_port)(src\_ip,\ src\_port,\ dst\_ip,\ dst\_port) \rightarrow (src\_ip',\ src\_port',\ dst\_ip,\ dst\_port)

变量含义是:

  • src_ipsrc_port:原始源地址和源端口;
  • dst_ipdst_port:原始目标地址和目标端口;
  • src_ip'src_port':转换后的源地址和源端口。

NAT 不是单纯修改一个字段。修改地址后,内核还要:

  1. 更新 IP 头校验和;
  2. 更新 TCP/UDP 等传输层校验和;
  3. 记录原始方向和回复方向之间的对应关系;
  4. 对回复包执行反向转换;
  5. 结合路由和过滤规则继续处理数据包。

因此,Linux NAT 通常依赖 conntrack。

5.2 SNAT、Masquerade 和 DNAT

SNAT 修改源地址,典型用途是内网主机通过公网地址访问外部网络:

内网客户端 -> 网关 -> 公网服务器
10.0.0.10       203.0.113.2

Masquerade 是动态源地址伪装,通常用于出口地址可能变化的接口,例如 DHCP、拨号或云环境弹性地址:

oifname "wan0" ip saddr 10.0.0.0/24 masquerade

它与固定 snat to 203.0.113.2 的区别是:

  • snat 明确指定转换地址,适合固定出口地址;
  • masquerade 根据出口接口当前地址进行转换,适合动态地址;
  • 固定公网地址场景使用 masquerade 不一定错误,但表达不够精确,并可能带来额外地址查询和接口变化语义。

DNAT 修改目标地址,典型用途是端口转发:

公网客户端 -> 网关公网地址:8080 -> 内网服务器:80

DNAT 本身不等于放行。即使目标地址已经改为内网服务器,forward 链仍然可以丢弃这个包。

5.3 NAT 为什么通常只在首包决定

NAT 规则通常只在连接的第一个数据包上选择映射。后续包直接使用连接跟踪中保存的转换关系。

假设:

客户端:10.0.0.10:53124
网关公网地址:203.0.113.2
服务器:198.51.100.20:443

客户端发起:

原始方向:
10.0.0.10:53124 -> 198.51.100.20:443

网关选择源端口 40001,执行 SNAT:

转换后发送:
203.0.113.2:40001 -> 198.51.100.20:443

服务器回复:

198.51.100.20:443 -> 203.0.113.2:40001

conntrack 根据映射识别该回复,并反向转换:

交付客户端:
198.51.100.20:443 -> 10.0.0.10:53124

如果中途删除 conntrack 条目,后续包可能无法被正确识别;如果 NAT 规则在连接开始后发生变化,已有连接通常继续使用旧映射,新连接才使用新规则。这也是“修改 NAT 规则后旧连接表现不变”的常见原因。

5.4 一个路由器 NAT 示例

假设:

  • 内网接口:lan0
  • 外网接口:wan0
  • 内网:10.0.0.0/24
  • 公网出口地址固定为 203.0.113.2

先开启 IPv4 转发:

sudo sysctl -w net.ipv4.ip_forward=1

检查:

sysctl net.ipv4.ip_forward

预期结果:

net.ipv4.ip_forward = 1

持久化方式因发行版不同,常见做法是在 /etc/sysctl.d/99-router.conf 中写入:

net.ipv4.ip_forward = 1

然后执行:

sudo sysctl --system

仅开启转发还不够。需要过滤和 NAT:

flush ruleset

table inet router_filter {
    chain forward {
        type filter hook forward priority filter;
        policy drop;

        ct state invalid drop
        ct state established,related accept

        # 允许内网主动访问外网
        iifname "lan0" oifname "wan0" ip saddr 10.0.0.0/24 accept
    }
}

table ip router_nat {
    chain postrouting {
        type nat hook postrouting priority srcnat;
        policy accept;

        oifname "wan0" ip saddr 10.0.0.0/24 snat to 203.0.113.2
    }
}

加载前检查:

sudo nft -c -f router.nft

加载:

sudo nft -f router.nft

完整数据路径为:

  1. 内网主机把默认网关设置为 Linux 路由器;
  2. 数据包从 lan0 进入;
  3. 路由决定目标在 wan0
  4. 经过 forward 链;
  5. oifname "wan0" ... snat to 203.0.113.2postrouting 选择源地址转换;
  6. 外部服务器回复;
  7. conntrack 识别回复方向并反向转换;
  8. ct state established,related accept 允许返回流量;
  9. 路由器把包发送回 lan0

如果只配置 SNAT 而没有 forward 规则,包仍可能在 forward 链被默认策略丢弃。如果只允许 forward 而没有启用 IP 转发,内核也不会按路由器方式转发。

5.5 端口转发算例

将外部接口 wan0 的 TCP/8080 转发到内网服务器 10.0.0.20:80

table ip port_forward {
    chain prerouting {
        type nat hook prerouting priority dstnat;
        policy accept;

        iifname "wan0" tcp dport 8080 dnat to 10.0.0.20:80
    }
}

table inet forward_filter {
    chain forward {
        type filter hook forward priority filter;
        policy drop;

        ct state invalid drop
        ct state established,related accept

        iifname "wan0" oifname "lan0" \
            ip daddr 10.0.0.20 tcp dport 80 accept
    }
}

这里必须区分“匹配哪个地址”:

  • prerouting 的 DNAT 链中,匹配的是外部访问的原始目标端口 8080
  • DNAT 后,转发过滤路径通常看到目标地址 10.0.0.20 和目标端口 80
  • forward 规则允许的是“被转换后的内网目标”。

不同版本和链优先级下,规则观察到原始字段和当前字段的细节还可通过 ct originalct reply 等表达式区分。排查时不要只凭端口转发的表面配置推测匹配结果,应使用计数器、trace 和抓包验证。


六、路由、Socket 和防火墙的边界

防火墙不能替代路由,也不能替代应用监听。

在本机访问场景中,目标地址经过路由判断后进入 INPUT。即使防火墙允许 TCP/443,以下情况仍然会失败:

ss -lntp
ip route get 198.51.100.20
  • 没有程序监听该端口;
  • 监听地址不是目标地址;
  • 路由把数据包送到了错误接口;
  • 应用拒绝连接;
  • SELinux、AppArmor 或应用自身策略阻止访问。

例如:

LISTEN 0 128 127.0.0.1:8080 0.0.0.0:*

这只表示本机回环地址监听 8080。即使防火墙开放了 eth0 上的 TCP/8080,外部客户端也不能访问 127.0.0.1:8080 对应的 Socket。

iproute2 是诊断这类问题的基础工具集。常用命令:

ip addr
ip route
ip route get 198.51.100.20
ip link
ss -lntup

因果链应按顺序验证:

  1. 网卡是否 UP,是否有正确地址;
  2. 路由是否选择预期接口和下一跳;
  3. Socket 是否监听目标地址和端口;
  4. 数据包是否到达;
  5. 防火墙是否放行;
  6. 返回路径是否可达;
  7. 安全模块和应用策略是否允许。

不要把“端口不通”直接归因于防火墙。


七、IPv6、ICMP 和反向路径检查

7.1 IPv6 不是“把 IPv4 规则复制一份”

IPv6 通常没有 NAT 的普遍必要性,网络设计更多依赖可路由地址、路由策略和防火墙。不能因为 IPv4 使用 SNAT,就默认 IPv6 也需要 NAT。

IPv6 依赖 ICMPv6 完成:

  • 邻居发现;
  • 路由器发现;
  • 地址解析;
  • 路径 MTU 发现;
  • 某些错误反馈。

因此,简单写出“丢弃所有 ICMPv6”的规则可能导致:

  • 邻居无法解析;
  • TCP 连接建立后传输大包失败;
  • IPv6 默认路由或地址配置异常。

应根据环境保留必要 ICMPv6 类型,而不是把 ICMP 当作可有可无的调试协议。

7.2 反向路径检查

Linux 的反向路径检查(rp_filter)用于验证收到数据包的源地址是否能够通过同一接口的路由反向到达。它可以降低源地址欺骗风险,但在以下环境中可能误杀合法流量:

  • 多宿主机;
  • 非对称路由;
  • 策略路由;
  • ECMP;
  • 隧道;
  • 某些复杂 NAT 拓扑。

查看设置:

sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.default.rp_filter

当规则看似允许、抓包却显示入包后消失时,rp_filter 是需要检查的路径之一。修改它应基于明确的路由模型,不应为了“先让流量通”而全局关闭。


八、规则治理:让规则可审计、可验证、可恢复

8.1 规则的生命周期

生产规则至少经历以下状态:

stateDiagram-v2
    [*] --> Draft: 编写配置
    Draft --> Checked: nft -c 语法检查
    Checked --> Staged: 测试环境或备用路径验证
    Staged --> Loaded: nft -f 原子加载
    Loaded --> Observed: 计数器/日志/抓包观察
    Observed --> Revised: 发现误放行或误阻断
    Revised --> Checked
    Loaded --> RolledBack: 连接中断或验证失败
    RolledBack --> Checked

nft -f 会把文件中的规则作为一次配置更新提交,而不是让管理员手工逐条输入。这样可以减少“加载到一半”的中间状态,但不能避免配置本身错误,也不能自动保证远程管理连接不被阻断。

8.2 用集合表达策略,而不是重复规则

重复规则容易产生遗漏和不一致。集合可以把策略数据与匹配逻辑分离:

table inet host_filter {
    set admin_hosts {
        type ipv4_addr
        elements = { 192.0.2.10, 192.0.2.11 }
    }

    set allowed_tcp_ports {
        type inet_service
        elements = { 22, 443 }
    }

    chain input {
        type filter hook input priority filter;
        policy drop;

        iifname "lo" accept
        ct state established,related accept
        ct state invalid drop

        ip saddr @admin_hosts tcp dport 22 accept
        tcp dport @allowed_tcp_ports accept
    }
}

这里:

  • admin_hosts 是 IPv4 地址集合;
  • allowed_tcp_ports 是服务端口集合;
  • @admin_hosts@allowed_tcp_ports 引用集合;
  • 修改允许地址或端口时不必复制多条规则。

但集合也会隐藏风险:一个集合中的元素可能具有不同业务含义。生产治理中应按信任边界拆分集合,例如不要把“监控主机”“办公网”“跳板机”混成一个 trusted 集合。

8.3 计数器是规则是否生效的证据

为关键规则添加计数器:

tcp dport 22 counter accept

查看:

sudo nft -a list chain inet host_filter input

输出会包含命中计数和规则句柄(handle),例如:

tcp dport 22 counter packets 12 bytes 720 accept comment "ssh"

计数器可以回答:

  • 规则是否真的被命中;
  • 命中量是否符合预期;
  • 某条规则是否已经长期没有流量;
  • 修改后流量是否转移到了另一条规则。

但计数器不是永久审计日志。重载规则可能清除计数,具体行为取决于加载方式和配置。需要长期证据时,应配合日志、流量采样或集中审计系统。

8.4 日志要控制速率

错误示例:

log prefix "DROP " drop

互联网边界上的扫描、异常流量或攻击可能让日志本身成为资源消耗源。更合理的诊断规则通常限制速率:

limit rate 5/second burst 20 packets \
    log prefix "nft-input-drop " flags all
drop

日志只能说明规则执行到了某个位置,不能单独证明数据包为什么没有到达该位置。应结合:

sudo journalctl -k -f
sudo tcpdump -ni any host 198.51.100.20

日志前缀应包含方向、接口或策略名称,否则多个链的日志混在一起后难以定位。

8.5 使用注释、版本控制和唯一管理者

规则文件应保存在版本控制中,并在规则中写出业务意图:

ip saddr 192.0.2.0/24 tcp dport 22 \
    accept comment "allow SSH from management network"

治理重点不是规则越多越安全,而是每条允许规则都能回答:

  • 谁被允许;
  • 访问哪个协议和端口;
  • 从哪个接口到哪个接口;
  • 访问的是本机还是转发目标;
  • 为什么需要;
  • 谁负责复核;
  • 何时应删除。

同一台机器最好明确一个规则管理者:

  • firewalld;
  • nftables 原生配置;
  • 容器编排系统;
  • 云安全代理;
  • 配置管理系统。

如果多个系统都直接改 Netfilter,最终状态可能与任何一个配置文件都不一致。查看最终生效状态应以:

sudo nft list ruleset

为准,而不是只看某个源文件。


九、诊断:从路径而不是从猜测开始

9.1 先判定流量类型

先问三个问题:

  1. 目标服务运行在本机吗?
  2. Linux 是路由器或 NAT 网关吗?
  3. 这是 IPv4 还是 IPv6?

对应路径通常是:

  • 本机服务:prerouting -> input -> socket
  • 转发流量:prerouting -> forward -> postrouting
  • 本机发出:output -> postrouting

如果把转发流量的规则写在 input,规则可能永远不会命中。

9.2 用 nft trace 观察规则路径

可以为某个数据包设置临时 trace:

add rule inet host_filter input \
    ip saddr 192.0.2.10 meta nftrace set 1

然后另一个终端执行:

sudo nft monitor trace

发起测试连接后,trace 通常会显示:

  • 数据包进入哪个表和链;
  • 哪条规则被评估;
  • 哪条规则匹配;
  • 最终 verdict 是什么。

完成诊断后删除该规则。先查找句柄:

sudo nft -a list chain inet host_filter input

再按实际输出的句柄删除:

sudo nft delete rule inet host_filter input handle <实际句柄>

nft monitor trace 的输出格式和可见字段会随内核、nft 版本变化。它是定位“规则没有命中”或“命中了错误规则”的工具,不应长期在高流量链上开启全量 trace。

9.3 tcpdump 需要在不同位置抓包

sudo tcpdump -ni lan0 tcp port 80
sudo tcpdump -ni wan0 tcp port 80
sudo tcpdump -ni any host 10.0.0.20

在 NAT 场景中,不同接口上的同一个连接可能显示不同地址:

  • lan0 上看到原始内网地址;
  • wan0 上看到 SNAT 后的公网地址;
  • DNAT 后的转发路径中看到内网目标地址。

如果入接口有包、forward 计数为零,可能是路由、早期 hook、地址族或接口条件不匹配。如果 forward 有计数、出口没有包,则要检查 verdict、路由、邻居解析和出口接口。如果两端都能看到包但应用失败,则还要检查 TCP 状态、MTU、服务监听和安全模块。

9.4 常见失败表现和对应边界

规则加载成功,但连接仍失败

可能原因:

  • 规则匹配的是 input,实际流量经过 forward
  • IPv4 规则没有覆盖 IPv6;
  • DNAT 已执行,但 forward 仍被丢弃;
  • 返回流量不匹配状态规则;
  • 应用没有监听目标地址;
  • 路由或邻居发现失败。

开启 NAT 后只有部分网站或大文件失败

常见方向包括:

  • 路径 MTU 发现被错误过滤;
  • ICMP/ICMPv6 错误消息无法返回;
  • 多路径或非对称路由与 rp_filter 冲突;
  • conntrack 或 NAT 状态超时;
  • 后端服务器返回路径没有经过同一 NAT 网关。

重载规则后新连接失败,旧连接仍然正常

这通常是 conntrack 和 NAT 首包选择共同造成的:已有连接已经建立状态和转换关系,新连接才会重新评估新规则。测试规则变更时必须同时验证新连接和已有连接。

删除规则后流量仍然存在

可能是:

  • 删除的是兼容层中的一部分,而非实际规则;
  • 其他表或链仍然允许;
  • 连接已经处于 established,被状态规则继续放行;
  • 容器或 firewalld 又重新生成了规则;
  • 流量走的是 IPv6、桥接或其他网络命名空间。

十、性能与并发边界

nftables 规则匹配通常在内核中执行,集合和映射可以避免大量线性重复规则。但性能问题不能只归因于“规则条数”:

  • 是否启用了连接跟踪;
  • 是否存在大量短连接;
  • 是否使用复杂表达式;
  • 是否在高流量路径上记录日志;
  • 是否有大规模动态集合更新;
  • 是否经过容器、桥接、隧道或虚拟交换路径;
  • 是否存在多 CPU 并发下的共享状态竞争。

连接跟踪表需要为并发流保存状态。一个简单的并发估算是:

NactiveRnew×TavgN_{\text{active}} \approx R_{\text{new}} \times T_{\text{avg}}

其中:

  • RnewR_{\text{new}} 是单位时间内的新流速率;
  • TavgT_{\text{avg}} 是平均条目存活时间;
  • NactiveN_{\text{active}} 是同时存在的连接跟踪条目数量。

例如,如果系统持续每秒创建 10,000 个新流,平均条目存活 60 秒,则稳态活动条目数量大约是:

10,000×60=600,00010{,}000 \times 60 = 600{,}000

这是估算,不是内核容量保证,因为 TCP、UDP、超时、重传、异常流和 NAT 方向会改变实际占用。实际设计还需观察:

watch -n 1 'cat /proc/sys/net/netfilter/nf_conntrack_count'

规则治理中的性能取舍通常是:

  • 用集合表达大量地址和端口;
  • 只对必要事件记录日志;
  • 不在每个数据包上执行昂贵的复杂匹配;
  • 对高风险入口考虑早期丢弃,但先确认不会绕过必须的状态或协议处理;
  • 将高频、稳定的规则放在容易理解且命中路径合理的位置;
  • 不为追求少量规则而把互不相关的信任边界合并。

如果流量规模已经要求 XDP、硬件卸载或专用 DDoS 防护,不能把 nftables 当成所有性能问题的通用解决方案。那属于不同的数据路径和容量边界。


十一、与主机安全加固的关系

防火墙只是主机安全的一层:

  • 最小化安装减少可被访问的服务;
  • Socket 监听地址控制本地暴露范围;
  • nftables 控制网络路径;
  • SELinux/AppArmor 限制进程对文件、端口和系统资源的访问;
  • 审计记录策略变化和异常行为;
  • 漏洞治理处理服务和内核缺陷。

例如,防火墙允许 TCP/8080,并不保证某个 Web 服务一定能绑定该端口;SELinux 可能拒绝一个不符合安全上下文的服务。反过来,SELinux 允许进程监听,也不代表外部网络可以通过防火墙访问。

安全边界应该分层验证,而不是把所有问题都写成 nftables 规则。一个服务是否暴露,至少需要同时检查:

ss -lntup
sudo nft list ruleset
getenforce 2>/dev/null || true
sudo aa-status 2>/dev/null || true

命令是否存在、输出格式和安全模块启用情况随发行版而异。getenforce 面向 SELinux,aa-status 面向 AppArmor,不应把一个不存在的工具输出解释成“安全模块已关闭”。


十二、生产环境中的明确取舍

12.1 默认拒绝不是无条件正确

policy drop 能减少未知流量,但它要求管理员明确允许:

  • 回环通信;
  • 已建立和相关连接;
  • 必要的 ICMP/ICMPv6;
  • 管理入口;
  • 监控、时间同步、DNS、软件更新等系统依赖;
  • 业务所需端口。

如果没有依赖清单,默认拒绝可能造成系统“安全但不可运维”。策略应来自实际数据流,而不是从一个通用模板复制。

12.2 dropreject 的选择

  • drop 不回复,通常减少信息泄露,但客户端可能等待超时;
  • reject 明确返回拒绝,故障发现更快,但会暴露“这里有一个可达的拒绝者”,并可能被滥用为反射或信息探测辅助。

对管理网络内部服务,reject 有时便于快速诊断;对公网边界,常用 drop,但最终取舍取决于协议、监控和威胁模型。

12.3 NAT 不是访问控制

NAT 能改变可见地址和连接方向,但不能替代:

  • input/forward 过滤;
  • 身份认证;
  • TLS;
  • 应用授权;
  • 服务补丁和漏洞治理。

“内网地址没有直接暴露,所以安全”是错误推论。端口转发、出站连接、被入侵主机的横向访问都可能穿过 NAT。


结语

Netfilter 提供 Linux 内核中的数据包处理位置,nftables 提供规则、集合和状态匹配的配置语言,conntrack 为连接状态和 NAT 反向转换保存上下文,NAT 则在连接路径上改变地址或端口。四者必须放在同一条数据流中理解:

  1. 先判断流量是本机交付、转发还是本机发出;
  2. 再确定它经过哪些 Netfilter hook;
  3. 根据原始或转换后的地址选择匹配位置;
  4. 使用 conntrack 状态处理后续方向;
  5. 用计数器、trace、抓包和路由查询证明实际路径;
  6. 通过原子加载、版本控制、唯一管理者和回滚机制治理规则。

真正可靠的防火墙配置不是“规则数量多”,而是每条规则都能被解释、验证、审计,并且在故障时能够恢复。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。