5.5 iptables/nftables 防火墙实战

预计阅读时间:14 分钟

📖 目录

学习目标

学完本章后,你将能够:

  • 理解 netfilter 框架和 nftables 的表/链/规则层级结构
  • 编写 nftables 基础过滤规则(输入/转发/输出链策略)
  • 配置 NAT(SNAT/DNAT/MASQUERADE)实现共享上网和端口转发
  • 使用 conntrack 工具监控连接跟踪表
  • 配置持久化 nftables 规则集和 ufw 前端

核心知识

  • netfilter——Linux 内核网络栈中的包过滤框架,提供 5 个钩子(hook)点
  • 表(table)——nftables 的顶级容器,按功能分类(filter/nat/route 等)
  • 链(chain)——规则的集合,必须关联到钩子才能生效
  • 规则(rule)——由匹配条件(match)+ 动作(verdict)组成
  • 连接跟踪(conntrack,Connection Tracking 的缩写)——跟踪每条流的状态,让防火墙感知连接语境。类似于"记住每个会话的上下文",使防火墙能区分新连接、已建立连接和关联连接
  • NAT——SNAT(改源 IP)、DNAT(改目标 IP)、MASQUERADE(动态 SNAT)
  • 地址族——inet(IPv4+IPv6)、ip(仅 IPv4)、ip6(仅 IPv6)

知识关联

原理讲解

为什么防火墙要分表/链/规则三层

iptables/nftables 的表-链-规则分层设计源于 Netfilter 项目的架构决策。Linux 内核需要在网络栈的多个位置(PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING)拦截数据包,但不同位置的包处理逻辑差异很大——例如 NAT 转换必须在路由决策之前完成(PREROUTING),而包过滤可以在路由之后。如果把所有规则混在一起,既难以管理也难以优化性能。因此 Netfilter 引入了"表"的概念:filter 表负责包过滤、nat 表负责地址转换、mangle 表负责包修改、raw 表负责连接跟踪前处理。每张表只关心自己职责范围内的操作,内核可以按优先级并行处理不同表的规则,提高了吞吐量。

链(chain)对应 Netfilter 的钩子点,是规则的容器。规则按顺序匹配,一旦命中立即执行动作(accept/drop/return/jump),不再检查后续规则。这种"先匹配先处理"的设计简化了规则编写——你不需要为每条规则指定完整条件链,只需在合适的链中按顺序添加规则即可。

为什么推荐用 nftables 而不是 iptables

iptables 诞生于 1998 年,其底层基于 x_tables 框架,存在几个结构性问题:规则存储在内核内存中,每条规则独立评估,大量规则时性能线性下降;IPv4/IPv6 规则需要分别管理;没有原子化的规则集更新机制(修改规则时可能短暂中断连接)。nftables(2008 年引入)从底层重新设计:使用字节码虚拟机评估规则,支持集合(set)和映射(map)进行 O(1) 查找,规则集作为原子整体加载,且原生支持 IPv4+IPv6 双栈(inet 地址族)。性能方面,nftables 在规则数量超过 100 条时显著优于 iptables;在规则集更新时也不会中断现有连接。

netfilter 钩子与包处理流程

netfilter 在内核网络栈中嵌入 5 个钩子点(hook),每个数据包会依次经过这些钩子:

入站包路径:
  [PREROUTING] → [路由决策] → [INPUT] → 本地进程

转发包路径:
  [PREROUTING] → [路由决策] → [FORWARD] → [POSTROUTING]

出站包路径:
  本地进程 → [OUTPUT] → [路由决策] → [POSTROUTING]

表/链遍历顺序

nftables 在同一钩子上可以有多个表,通过链的 priority 决定遍历顺序(数值越小越先执行):

钩子典型 priority用途
rawprerouting, output-300连接跟踪前处理
filterinput, forward, output0包过滤
natprerouting, output, postrouting-100 / 100NAT 转换

连接跟踪(conntrack)

conntrack 记录每条流的状态,防火墙据此识别新连接、已建立连接和相关连接。状态包括:NEW(新连接请求)、ESTABLISHED(双向可见)、RELATED(关联连接,如 FTP 数据连接)、INVALID(无法识别)。没有 conntrack,防火墙只能对每个包独立决策,无法实现有状态过滤。

NAT 的完整处理流程

NAT 并不是一个独立动作,而是拆成两段协作:DNAT(目的地址转换)发生在 prerouting 钩子——数据包刚进网卡、还没做路由决策时改写目标地址;SNAT/MASQUERADE(源地址转换)发生在 postrouting 钩子——数据包即将发出网卡前改写源地址。中间的路由决策看到的是 DNAT 之后的目标,所以转发链上的过滤规则要匹配转换后的地址。

外网访问内网 Web 服务器(公网 203.0.113.10:8080 → 内网 10.0.1.10:80):
  包进入 eth0
  → prerouting:  tcp dport 8080 dnat to 10.0.1.10:80   # 目标被改写
  → 路由决策:    目标现在是内网地址,走转发路径
  → forward:     ct state new,established accept          # 放行转发
  → postrouting: ip saddr 10.0.0.0/8 masquerade          # 源被改写(回程包才能回来)
  → 发出 eth1 到达内网服务器

一个常见误区:DNAT 只做"单程"改写,回程包需要反向转换。conntrack 会自动为已建立的连接维护反向 NAT 条目,前提是回程包能命中 ct state established,related 的放行规则——所以 forward 链的 established,related 规则是 NAT 能工作的前提。

firewalld 与 ufw 对比

三者都是 netfilter 的前端,但设计哲学不同:nftables 是"直接写规则集"的原生工具;ufw 定位"简单防火墙",隐藏了大部分细节;firewalld 定位"动态防火墙",引入 zone(区域)概念管理不同信任级别的网络。

维度nftablesfirewalldufw
底层引擎直接操作 nftnftables 规则集nftables 规则集
配置模型表/链/规则Zone(区域)+ 服务简单的 allow/deny 命令
热加载需重新加载规则集规则即时生效,不中断连接规则即时追加
默认发行版Arch、Gentoo、手动安装RHEL/CentOS/FedoraUbuntu/Debian
适用人群运维/安全工程师需要 zone 管理的主机防火墙新手或桌面主机

生产环境选型建议:RHEL 系直接用 firewalld(与 NetworkManager 集成良好),Ubuntu 服务器可用 ufw 快速兜底,但需要精细控制(NAT、限速、集合)时应直接使用 nftables——无论哪个前端,最终都要能读懂 nft list ruleset 的输出。

示例代码

1. nftables 基础——表/链/规则

# 查看当前规则集
nft list ruleset
# 输出: table inet filter { ... }

# 创建表
nft add table inet filter

# 创建基础链
nft add chain inet filter input { type filter hook input priority 0\; }
nft add chain inet filter forward { type filter hook forward priority 0\; policy drop\; }
nft add chain inet filter output { type filter hook output priority 0\; policy accept\; }

# 添加规则
nft add rule inet filter input tcp dport 22 accept
nft add rule inet filter input ct state established,related accept

# 插入规则到指定位置(index 从 0 开始)
nft insert rule inet filter input index 0 tcp dport 443 accept

# 删除规则
nft delete rule inet filter input handle 3   # handle 通过 nft -a list 查看

# 清空表
nft flush table inet filter

2. 过滤规则——服务器防火墙模板

#!/usr/sbin/nft -f

flush ruleset

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

        ct state established,related accept
        iif lo accept

        icmp type { echo-request, echo-reply } accept
        icmpv6 type { echo-request, echo-reply, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept

        tcp dport { 22, 80, 443 } accept

        tcp dport 3306 ip saddr 10.0.0.0/8 accept
        tcp dport 6379 ip saddr 10.0.0.0/8 accept

        log prefix "NFT_INPUT_DROP: " counter drop
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
    }

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

3. NAT/SNAT/DNAT 配置

# NAT 表
table inet nat {
    chain prerouting {
        type nat hook prerouting priority -100;

        tcp dport 2222 dnat to 10.0.1.50:22
        tcp dport 8080 dnat to 10.0.1.10:80
    }

    chain postrouting {
        type nat hook postrouting priority 100;

        ip saddr 10.0.0.0/8 oif eth0 masquerade
        ip saddr 172.16.0.0/12 oif eth0 masquerade
        ip saddr 192.168.0.0/16 oif eth0 masquerade
    }
}

# 启用 IP 转发(NAT 必需)
sysctl -w net.ipv4.ip_forward=1

4. 端口转发实战

# 主机 80 端口 → 本地 8080 服务
nft add rule inet nat prerouting tcp dport 80 dnat to 127.0.0.1:8080

# 公网 2222 → 内网 10.0.1.50:22
nft add rule inet nat prerouting tcp dport 2222 dnat to 10.0.1.50:22

# HTTPS → 内部 8443
nft add rule inet nat prerouting tcp dport 443 dnat to 10.0.1.20:8443

nft list table inet nat
# 输出:
# table inet nat {
#     chain prerouting { ... }
#     chain postrouting { ... }
# }

5. 持久化配置

# 保存当前规则集
nft list ruleset > /etc/nftables.conf

# 使用 systemd 自动加载
systemctl enable --now nftables

# 手动加载
nft -f /etc/nftables.conf

# 验证加载
nft list ruleset | head -5
# 输出: table inet filter { ... }

6. ufw——nftables 前端

# ufw 底层生成 nft 规则到 inet ufw-* 表
ufw status verbose

# 常用命令
ufw enable
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw deny 23
ufw allow from 10.0.0.0/8 to any port 3306
ufw status numbered
ufw delete 3
ufw disable

# 应用配置
ufw app list
ufw allow 'Nginx Full'

# 查看 ufw 生成的底层规则(ufw 经 iptables-nft 后端注册,表为 ip/ip6 filter)
nft list table ip filter

7. 命名集合与元素管理

# 命名集合:把允许的 IP 抽出来单独维护
nft add table inet filter
nft add set inet filter allowed_ipv4 { type ipv4_addr\; flags interval\; }
nft add set inet filter blocked_port { type inet_service\; }

# 向集合添加元素
nft add element inet filter allowed_ipv4 { 10.0.0.0/8, 192.168.1.0/24 }
nft add element inet filter blocked_port { 23, 3389 }

# 集合在规则中引用(维护时只需增删元素,不改规则)
nft add rule inet filter input ip saddr @allowed_ipv4 accept
nft add rule inet filter input tcp dport @blocked_port drop

# 查看集合与元素
nft list set inet filter allowed_ipv4
# 输出: table inet filter { set allowed_ipv4 { type ipv4_addr ... elements = { 10.0.0.0/8, 192.168.1.0/24 } } }

# 动态封禁脚本思路(配合 cron 或 fail2ban)
# nft add element inet filter blocked_ip { 203.0.113.66 }

8. 流量限速与日志策略

# 限制 SSH 每分钟新连接数(防暴力破解)
nft add rule inet filter input \
  tcp dport 22 ct state new limit rate 5/minute accept

# 更精细:用 meter 按源 IP 限速(每个 IP 独立计数)
nft add rule inet filter input \
  tcp dport 22 ct state new meter ssh_bruteforce \
  { ip saddr limit rate 5/minute } accept

# 超出限制的进入 log + drop
nft add rule inet filter input \
  tcp dport 22 ct state new meter ssh_bruteforce \
  { ip saddr limit rate 5/minute } \
  log prefix "SSH_RATE_DROP: " counter drop

# 丢弃前记录 ICMP 泛洪(每 10 秒最多 3 条日志)
nft add rule inet filter input icmp type echo-request \
  limit rate 3/second burst 10 \
  log prefix "ICMP_LOG: " accept

9. firewalld 快速上手

# 查看默认区域与已开放服务
firewall-cmd --get-default-zone
firewall-cmd --list-all

# 开放端口/服务(--permanent 持久化)
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --reload

# 指定源区域:仅允许内网访问 3306
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" port port="3306" protocol="tcp" accept'
firewall-cmd --reload

# 查看防火墙底层 nft 规则
nft list table inet firewalld

常见错误

错误表现根因正确做法
SSH 连接被自己加的规则切断nft 命令立即生效,链 policy drop 且未先放行 22 端口先写入文件用 nft -f 加载,或开两个终端确保逃生通道
DNAT 端口转发不生效未启用 net.ipv4.ip_forwardsysctl -w net.ipv4.ip_forward=1 并写入 sysctl.conf
MASQUERADE 后内网无法上网FORWARD 链默认 drop 且未放行转发流量forward 链添加 ct state established,related accept
NAT 规则意外匹配了 DNS 查询DNAT 规则未限制目的 IP,DNS 查询也被转发DNAT 规则增加 ip daddr 限制目标地址
conntrack table full, dropping packet连接跟踪表耗尽,默认 65536 条上限增大 nf_conntrack_max 或缩短超时时间
nft 报 Error: syntax error, unexpected newline命令行中分号未转义分号前加反斜杠:priority 0\;
DNAT 只做了半套(内网能通外网不通)回程包没有匹配 established 规则或源地址未转换forward 链保留 ct state established,related accept;确认 postrouting 的 masquerade 覆盖回程网段
修改集合后规则不生效元素修改后未保存/未重新加载文件交互式改动用 nft add element;文件方式改完执行 nft -f
firewalld 与 nftables 规则互相覆盖两套前端同时操作同一内核钩子,优先级冲突同一主机只保留一个前端;直接用 nft flush ruleset 会清掉 firewalld 的规则
limit 限速对暴力破解无效limit 是全局计数,攻击者换源 IP 即绕过使用 meter 按源 IP 限速,或配合 fail2ban 封禁
误执行 nft flush ruleset 导致所有规则丢失flush ruleset 清空全部表/链/规则,包括正在使用的防火墙策略修改前先 nft list ruleset > /tmp/backup.nft 备份;用 nft -f 从文件恢复

最佳实践

实践原理示例
默认拒绝 + 白名单开放最小权限——未明确允许的一律拒绝input 链 policy drop 后逐条放行必要端口
使用 inet 地址族代替 ip/ip6一份规则同时覆盖 IPv4 和 IPv6table inet filter 而非 table ip filter
先写文件再用 nft -f 加载避免中间状态导致连接断开编辑 /etc/nftables.confnft -f
善用集合(set)简化规则减少规则数量、提高可读性tcp dport { 80, 443, 8080 } accept
规则中加入 log + counter排错时快速定位被丢弃的流量log prefix "NFT_DROP: " counter drop
监控 conntrack 使用率表满导致随机丢包,极难排查监控 nf_conntrack_count / nf_conntrack_max
用命名集合管理可变地址IP 变动只增删元素,不改规则、不重载整个规则集nft add set ... flags interval + nft add element
NAT 规则尽量收敛过宽的 DNAT 会劫持无关流量(如 DNS 被转发)DNAT 加 ip daddr 限制目标公网 IP,仅对指定端口生效
规则文件用 Git 管理防火墙变更可审计、可回滚/etc/nftables.conf 纳入版本控制,变更走 PR 评审

练习题

  1. (概念)nftables 的 inet 地址族和 ip 地址族有什么区别?推荐使用哪个?
  2. (概念)MASQUERADE 和 SNAT 的适用场景分别是什么?为什么动态 IP 场景下应优先使用 MASQUERADE?
  3. (实操)编写 /etc/nftables.conf,实现:默认拒绝所有入站,放行 SSH/HTTP/HTTPS,允许已建立连接和相关连接。加载后用 nft list ruleset 验证。
  4. (实操)配置 DNAT 将主机 8080 端口转发到 127.0.0.1:80,开启 IP 转发。用 curl localhost:8080 验证。
  5. (🔍 挑战)搭建 Linux 路由器:eth0 公网、eth1 内网,配置 MASQUERADE 让内网主机上网,限制内网仅可访问 80/443。用 conntrack -L 验证连接跟踪。
点击查看答案
  1. (概念)inet 地址族同时支持 IPv4 和 IPv6,一张表覆盖双栈;ip 仅支持 IPv4。推荐使用 inet,简化配置且未来兼容。
  2. (概念)SNAT 固定修改源 IP 为指定地址,适合出口 IP 固定的场景;MASQUERADE 自动获取出口接口的当前 IP,适合动态 IP(如 PPPoE 拨号、DHCP 接口)。动态 IP 下若用 SNAT,IP 变化后规则失效。
  3. (实操)参考 chapter 内的 base ruleset 模板。验证命令:nft list ruleset 应显示三条 base chain 规则(input/output/forward)加一条 ct state 放行规则。
  4. (实操)nft add rule ip nat prerouting tcp dport 8080 redirect to :80,确认 net.ipv4.ip_forward=1。curl 应返回 Nginx 默认页。
  5. (🔍)eth0 配置 MASQUERADE,eth1 上限制 tcp dport { 80, 443 } accept 后 drop 其余。conntrack -L 应看到 ESTABLISHED 条目。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 nftables 的表、链、规则层次结构尝试向他人讲解
命令操作能不查文档完成 nftables 规则编写、表和链的创建删除在终端实际执行
原理掌握能说出 nftables 相比 iptables 的优势和数据包处理流程画出流程图
故障排查能独立排查 nftables 规则导致网络不通的问题模拟故障并修复
最佳实践能说明为什么推荐使用 nftables 而非 iptables对比不同方案

本章总结

nftables 是现代 Linux 防火墙的标准配置接口,统一了 IPv4/IPv6 语法,性能优于 iptables。核心理念是有状态包过滤——借助 conntrack 记录连接语境,大幅简化规则编写。NAT 分为 SNAT(改源)、DNAT(改目标)、MASQUERADE(动态 SNAT),端口转发需要配合 IP 转发。

速查表

命令/操作用途
nft list ruleset查看全部规则
nft add table inet <name>创建表
nft add rule ... tcp dport 80 accept放行端口
nft -f /etc/nftables.conf从文件加载规则
nft list ruleset > /etc/nftables.conf保存规则集
nft add rule ... dnat to 10.0.1.50:22DNAT 端口转发
nft add rule ... masqueradeMASQUERADE
sysctl -w net.ipv4.ip_forward=1启用 IP 转发
conntrack -L查看连接跟踪表
conntrack -S查看连接跟踪统计
ufw allow 80/tcpUFW 放行端口

学习路径建议

延伸阅读

常见问题

ufw 和 nftables 是什么关系?
ufw(Uncomplicated Firewall)是 iptables/nftables 的前端工具,提供易用的命令接口。Ubuntu 从 22.04 开始底层使用 nftables 作为默认框架。ufw enable/disable 即可开关,适合简单规则。nftables 直接操作内核 netfilter,配置更灵活但语法更底层。日常用 ufw,复杂 NAT/端口转发场景用 nftables。
NAT 端口转发怎么做?
nftables 示例:table ip nat { chain prerouting { type nat hook prerouting priority -100; tcp dport 8080 redirect to :80 } }。将宿主机 8080 端口流量转发到 80 端口。DNAT:ip daddr 外网IP tcp dport 443 dnat to 内网IP:443。注意开启 ip_forward:sysctl -w net.ipv4.ip_forward=1。
连接跟踪表满了怎么办?
连接跟踪(conntrack)满了会导致新连接被丢弃(kernel: nf_conntrack: table full, dropping packet)。查看当前:sysctl net.netfilter.nf_conntrack_max(默认 65536),cat /proc/net/nf_conntrack | wc -l。临时调整:sysctl -w net.netfilter.nf_conntrack_max=262144。或者缩短超时:sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600。
↑ 回到顶部