AI 摘要
AI
正在生成摘要...

最近搞了一个轻量型的服务器,由于是小厂,所以价格上很优惠,但是买完了之后,我人蒙了:

image-20260326170450582

服务器的物理网卡直接绑定的公网IP、没有安全组、没有内网IP,那我说白了,这个小厂的轻量服务器的网络架构,就是把服务器网线直接绑死公网 IP,中间没搞 NAT 转换,也没加一层防火墙、交换机做防护,直接把服务器怼到公网里了。

等于就是少了路由器、少了交换机、少了防火墙这几层,公网流量直接跑到服务器网卡上,服务器自己拿着公网 IP 直面全网,没有任何中间设备帮你挡一下,纯纯裸奔接入公网。

仔细想想,我也不会防火墙啊,也不是说不会,对于防火墙整体性设计还不是很懂,但是你说那也不能直接用,如果遇到端口扫描、ssh暴力破解,我服务器连端口限制都没有,让人随便打了,**宴请八方?**所以了解了一下防火墙的相关内容,决定不使用Iptables,去使用nftables,具体原因,我们往下讲。

一、nftables介绍

1、nftables理解

nftables内核引擎为Linux内核添加了一个简单的虚拟机,能够执行字节码来检查网络数据包并决定如何处理该数据包。该虚拟机实现的操作被刻意设计得基础化,它能够从数据包本身获取数据,查看关联的元数据(例如入站接口),并管理连接跟踪数据。基于这些数据,可使用算术运算符、位运算符和比较运算符进行决策。该虚拟机还支持操作数据集合(通常为IP地址),从而能用单次集合查询替代多次比较操作。

怎么更简单的去理解呢?

简单来说,nftables 的核心就是 Linux 内核里的这个迷你虚拟机,我们通过表、链、规则这类简洁语法编写防火墙策略,告诉这个内核虚拟机:哪些流量可以进出、哪些 IP 允许访问、哪些端口开放、哪些流量直接拦截,这套语法就是我给虚拟机下达的指令,虚拟机收到后会自动按照我的规则处理所有网络数据包。

2、iptables与nftables对比

nftables 与 iptables 核心横向对比:

对比维度 iptables nftables
核心架构 传统四表五链固定架构,多模块独立 内核级迷你虚拟机架构,统一灵活
协议支持 分裂管理:iptables (IPv4)、ip6tables (IPv6)、独立工具 统一管理:单配置支持 IPv4/IPv6/ARP/ 网桥
语法风格 命令行参数式,记忆难、冗余多 声明式简洁语法,可读性高、易编写
规则匹配 线性遍历匹配,规则越多速度越慢 哈希 / 红黑树查询,万条规则无性能衰减
规则更新 全量重载规则,高并发易闪断、丢连接 原子增量更新,热修改无中断、无损耗
动态集合 依赖第三方 ipset,配置繁琐 原生支持 IP / 端口集合,一键增删改查
Docker 兼容 需兼容层,规则混乱、性能一般 原生适配,网络转发 / NAT 完美支持
兼容性 老旧系统全兼容,新系统逐步弃用 新系统原生支持,提供 iptables 兼容层
维护状态 停止新功能开发,仅维护修复 官方主推、持续更新、Linux 未来标准

更主要的原因是我使用的ubuntu22.04系统出厂默认就自带nftables,无需安装更多插件,系统原生适配,并且当规则变多的时候,也不会增加CPU使用负担,多方考虑决定学习并使用nftables。

3、nftables工作流程

BASH
┌─────────────────────────────────────────────────────────────┐
│ 第一步:数据包入站                                             │
│ 网络数据包抵达Linux内核网络栈 → 进入Netfilter框架处理流程        │
└───────────────────────────┬─────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 第二步:钩子触发                                                    │
│ 内核按流量路径触发钩子 → prerouting/input/forward/output/postrouting │
└───────────────────────────┬──────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 第三步:内核虚拟机启动                                        │
│ nftables内核引擎加载 → 初始化字节码运行环境 → 加载规则集        │
└───────────────────────────┬─────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 第四步:规则匹配检索                                         │
│ 按表/链优先级遍历 → 提取数据包IP/端口/接口/状态等元数据         │
│ 支持集合/哈希高效匹配 → 替代传统逐条线性比对                    │
└───────────────────────────┬─────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 第五步:逻辑决策执行                                          │
│ 执行算术/位运算/比较运算 → 匹配规则定义的过滤/转发/NAT策略       │
└───────────────────────────┬─────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 第六步:动作处理                                             │
│ 执行最终动作 → ACCEPT放行/DROP丢弃/REJECT拒绝/NAT地址转换      │
│ 原子化操作无中断 → 支持增量更新规则集                          │
└───────────────────────────┬─────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 第七步:流量出站                                             │
│ 处理完成的数据包离开内核网络栈 → 转发至目标地址/本机应用/公网    │
└───────────────────────────┬─────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 核心解决问题                                                 │
│ 1. 统一管理IPv4/IPv6/网桥/ARP全协议栈防火墙规则               │
│ 2. 高性能数据包匹配,规则量越大优势越明显                      │
│ 3. 灵活自定义过滤逻辑,替代老旧iptables固定架构                │
│ 4. 原子更新、无中断热修改,适配生产环境动态规则调整             │
└─────────────────────────────────────────────────────────────┘

网络数据包抵达 Linux 内核网络栈后,会进入 Netfilter 框架并触发对应的钩子节点,nftables 内核虚拟机随之启动,加载配置好的规则集与字节码执行环境。虚拟机从数据包中提取 IP 地址、端口、入站接口、连接状态等核心数据,结合算术、位运算和比较逻辑,在定义好的表和链中执行规则匹配,利用哈希、集合等高效数据结构替代传统线性遍历,快速完成流量匹配判断。

完成匹配后,虚拟机将执行规则中定义的动作,包括放行、丢弃、拒绝、网络地址转换等,处理后的数据包按照决策结果离开内核网络栈,转发至本机应用、目标主机或公网。整个过程支持原子化的规则更新与热修改,无需中断网络服务,同时统一覆盖 IPv4、IPv6、网桥等多种网络场景,实现灵活且高性能的网络流量管控。

二、Nftables使用

为了简化理解、提升使用效率,我选择不深入讲解表、链、规则等底层概念,而是从实际操作和应用场景出发,让我们快速掌握 nftables 的核心用法,更好地理解并落地使用。并且,我们更多选择以配置文件的方式进行配置,而不是更多以命令的方式。

更加详细使用请参考相关文档:

Red Hat官方文档:https://docs.redhat.com/zh-cn/documentation/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/getting-started-with-nftables_configuring-and-managing-networking

维基百科:https://wiki.archlinuxcn.org/wiki/Nftables

2.0 配置文件格式

我们先来了解一下配置文件格式,ubuntu系统默认出厂就会给一个nftables配置文件,在/etc/nftables.conf 路径之下,我们查看一下这个配置文件:

BASH
root@tencent:~# cat /etc/nftables.conf 
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        # 入向
        chain input {
                type filter hook input priority 0;
        }
        # 转发
        chain forward {
                type filter hook forward priority 0;
        }
        # 出向
        chain output {
                type filter hook output priority 0;
        }
}

第一行:#!/usr/sbin/nft -f,这是配置文件的固定声明,作用是告诉操作系统:“请使用 nftables 工具来执行这个文件”。

flush ruleset:作用是清空系统中所有旧的防火墙规则,避免新旧规则冲突、导致异常,这也是配置文件的固定开头,每次加载配置都会先清空,再加载新规则。

table inet filter{}:这是 nftables 的核心规则容器,我们所有的防火墙规则(放行、拦截、端口限制等),都写在这个大括号 { } 里面。

chain input:专门管理进入服务器的流量(别人访问你的服务器)。后面我们要加的 SSH 白名单、放行业务端口、拦截恶意流量,全都写在这个 input 链里。

chain forward:专门管理转发流量(服务器中转的流量)。如果你使用 Docker 容器、搭建网关,就需要配置这里,默认是空的。

chain output:专门管理服务器主动发出的流量(服务器访问外网、下载软件、发送数据)。默认没有任何限制,服务器可以自由访问外部网络。

**内部的 type filter hook ... priority 0:**这是链的核心三要素——type 指定链的类型(如过滤、NAT),hook 指定流量经过的钩子节点,priority 控制执行优先级。初学者保留默认值即可,后面我们还会遇到不同的组合方式。

小知识:priority 0priority filter 是完全等价的,filter 只是 0 的别名写法,后面章节中我们统一使用更具可读性的 priority filter

总结三个部分:

  • 我想访问别人(本机发起):走 output

  • 别人想访问我(访问本机服务):走 input

  • 别人通过我访问别人(路由转发):走 forward

所以在初始状态下,Ubuntu 默认的这个配置文件,没有任何拦截 / 放行规则,相当于:

  • ✅ 所有流量都可以进入服务器

  • ✅ 所有流量都可以转发

  • ✅ 所有流量都可以发出

简单说:默认配置 = 服务器裸奔在公网,没有任何防护。一般情况下,云服务器会有安全组,但是我们也讲了,我们现在不是一般情况,hhh,无奈之举让自己学了一点新东西。

这也是我们接下来要一步步修改、加固配置的原因。

2.1 基础安全加固

这是服务器防火墙的核心安全底座,也是我们必须最先配置的内容。Ubuntu 默认的 nftables 是完全放开所有流量的,等于服务器在公网裸奔。

基础安全加固的作用:默认拒绝所有外部流量,只放行系统正常运行必需的流量,从根源上堵住安全漏洞。

BASH
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        chain input {
                # policy drop:拒绝所有进入服务器的流量
                type filter hook input priority filter;policy drop;
                # 放行本地回环接口,本机内部访问
                iif "lo" accept
                # 放行已建立连接和关联连接
                ct state established,related accept
                # 丢弃无效、恶意的数据包
                ct state invalid drop
        }
        chain forward {
                type filter hook forward priority 0;
        }
        chain output {
                type filter hook output priority 0;
        }
}

policy drop:最关键的一句,默认拒绝所有进入服务器的流量。没有被我们特意放行的流量,全部拦截,这是安全的核心。

iif "lo" accept:放行本机自己访问自己的流量(比如程序调用本地服务),系统必需,不能删。

ct state established,related accept:放行服务器主动对外请求后,外界返回的流量(比如你用服务器下载软件,服务器能收到回包)。

ct state invalid drop:自动丢弃非法、畸形、攻击类的数据包,简单防御基础网络攻击。

配置效果

配置完成后,你的服务器立刻从「裸奔模式」变成「安全锁闭模式」:

  • 所有陌生流量无法进入服务器
  • 系统正常运行不受任何影响
  • 为后续 SSH 白名单、端口放行打下安全基础

核心配置内容:

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

    # 放行本地回环接口
    iif "lo" accept

    # 放行已建立连接和关联连接(服务器对外回复的流量)
    ct state established,related accept

    # 丢弃无效、恶意的数据包
    ct state invalid drop
}

如果你不想修改配置文件,只想临时生效这条基础安全规则(服务器重启后会失效),可以直接执行以下命令;想要永久保留规则,依然推荐使用配置文件方式。

BASH
# 设置默认策略:拒绝所有入站流量(注意:policy 不是规则,需要用专用命令设置)
nft chain inet filter input '{ policy drop; }'

# 放行本地回环接口
nft add rule inet filter input iif "lo" accept

# 放行已建立连接和关联连接
nft add rule inet filter input ct state established,related accept

# 丢弃无效、恶意的数据包
nft add rule inet filter input ct state invalid drop

在这里我就不展示直接应用本配置后的效果了,因为这是一台公网云服务器。如果现在直接加载这套基础安全规则,会因为默认拦截所有入站流量,导致我的 SSH 连接立刻断开,无法再操作服务器。

等后面讲解完成 2.2 SSH IP 白名单 之后,我会统一演示如何完整配置并正确应用 nftables 规则,保证安全的同时不会断开服务器连接。

2.2 SSH IP白名单

基础安全加固完成后,服务器已经默认拒绝所有入站流量,接下来我们要做的就是开放 SSH 权限,但不是开放给所有人,而是只允许你信任的 IP 登录。这是公网服务器最重要的安全配置之一,能彻底防止 SSH 暴力破解

我们在 input 链中添加以下规则,替换成你自己的 IP 即可:

BASH
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        chain input {
                # policy drop:拒绝所有进入服务器的流量
                type filter hook input priority filter;policy drop;
                # 放行本地回环接口,本机内部访问
                iif "lo" accept
                # 放行已建立连接和关联连接
                ct state established,related accept
                # 丢弃无效、恶意的数据包
                ct state invalid drop

                # 仅允许指定IP地址访问SSH
                ip saddr { 192.168.0.0/24 , 1.2.3.4 } tcp dport 22 accept
        }
        chain forward {
                type filter hook forward priority 0;
        }
        chain output {
                type filter hook output priority 0;
        }
}

核心就是增添一条:

BASH
# 仅允许指定 IP 访问 SSH(22 端口)
ip saddr { 192.168.0.0/24, 1.2.3.4 } tcp dport 22 accept

配置说明

  • ip saddr { ... }:只允许这些 IP 访问
  • tcp dport 22:目标端口是 SSH(22)
  • accept:允许通过

如何修改成你的 IP:把示例里的 IP 换成你自己的办公 IP、家庭 IP、常用外网 IP 即可。多个 IP 用逗号分隔,格式固定为 { IP1, IP2, IP3 }

如果使用命令添加,执行下面这条命令:

BASH
nft add rule inet filter input ip saddr { 192.168.0.0/24, 1.2.3.4 } tcp dport 22 accept

这回我们就可以执行规则的应用了,执行以下命令进行规则的使用:

BASH
nft -f /etc/nftables.conf

-f 参数代表从指定配置文件加载规则,执行后所有配置立即生效。执行后没有任何弹出显示,就代表我们的规则配置正确,反之就要检查我们编写的规则语法是否存在问题进行修改。

查看已经生效的规则:

BASH
nft list ruleset 

执行之后就能查看到已经生效的规则:

BASH
table inet filter {
        chain input {
                type filter hook input priority filter; policy drop;
                iif "lo" accept
                ct state established,related accept
                ct state invalid drop
                ip saddr { 192.168.0.0/24, 1.2.3.4 } tcp dport 22 accept
        }

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

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

list ruleset 是 nftables 专用的查看命令,执行后会完整输出我们配置的基础安全规则、SSH IP 白名单等所有已生效规则,方便我们核对配置是否准确。

如果使用iptables,推荐搭配Fail2ban,也可以实现ssh防火墙效果

2.3 命令添加规则方式

顺便来讲一下命令添加规则的方式,我们把这条规则拆成 4 个部分,它们是**「并且」的关系**——必须同时满足所有条件,才会放行流量,缺一不可。nftables 命令行添加规则,有固定的万能组合格式

BASH
nft add rule [表名称] [链名称] [条件1] [条件2] [最终动作]

我以添加ssh这条规则作为示例:

nft add rule:固定开头 → 代表向 nftables 里添加一条规则

inet filter:固定表名 → 我们配置文件里的 table inet filter

input:固定链名 → 代表「入站流量规则」

ip saddr { 你的IP }:条件 1 → 来源 IP 必须是白名单里的 IP

tcp dport 22:条件 2 → 目标端口必须是 22

accept:最终动作 → 满足所有条件,放行流量

组合 1:只加一个条件,仅放行 IP

TEXT
nft add rule inet filter input ip saddr { 192.168.1.1 } accept

含义:这个 IP 发来的所有流量,全部放行

组合 2:再加一个条件(IP + 端口)

TEXT
nft add rule inet filter input ip saddr { 192.168.1.1 } tcp dport 22 accept

含义:只有这个 IP,访问 22 端口,才放行(我们的 SSH 规则)

组合 3:再加一个条件(IP + 端口 + 新连接)

TEXT
nft add rule inet filter input ip saddr { 192.168.1.1 } tcp dport 22 ct state new accept

含义:仅允许这个 IP 新建 22 端口连接,更安全

同样的道理,删除规则方式。但这里有一个重要区别:推荐使用 handle 号(规则的唯一编号)来删除规则,这种方式最安全、不会误删。虽然 nftables 也支持按条件删除,但 handle 方式更精准,是生产环境的推荐做法。

先使用 -a 参数查看每条规则的 handle 编号:

BASH
nft -a list ruleset

输出中每条规则后面都会带有 # handle 数字,找到你要删除的规则编号,执行删除:

BASH
nft delete rule [表名称] [链名称] handle [编号]

# 示例:删除 inet filter 表 input 链中 handle 编号为 10 的规则
nft delete rule inet filter input handle 10

2.4 放行业务端口

经过基础安全加固和 SSH 白名单配置后,服务器默认拦截了所有外部入站流量,我们的网站、应用程序等业务服务自然也无法被外部访问。

解决这个问题的核心方法,就是放行业务服务对应的端口,让外部流量可以正常访问我们的服务。同时,我们还可以结合 IP 限制,实现更精细化的流量管控。将以下规则添加到/etc/nftables.confinput链中,根据你的业务端口进行修改:

BASH
# 放行单一业务端口
tcp dport 3000 accept

# 放行多个业务端口
tcp dport { 80 , 443 , 3000 , 8080 } accept

单个端口:直接修改数字即可,例如将3000替换为你的服务端口;

多个端口:使用大括号包裹,端口间用英文逗号分隔,格式固定为{ 端口1, 端口2 }

协议说明:我们示例中使用tcp协议,适用于绝大多数 Web 服务、应用服务。

当然这里有个小细节:

执行规则进行查看:

BASH
$ nft list ruleset 
table inet filter {
        chain input {
                # ......
                
                # 单个端口
                tcp dport 3000 accept
                # 多个端口
                tcp dport { 80, 443, 8080 } accept
        }

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

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

如果需要限制仅指定 IP可以访问业务端口,我们可以组合 IP 和端口条件,实现精细化管控,这是生产环境常用的安全策略:

BASH
# 仅允许指定IP访问80/443端口
ip saddr { 1.2.3.4, 192.168.0.0/24 } tcp dport { 80, 443 } accept


# 查看规则
table inet filter {
        chain input {
        		# ...
                tcp dport 3000 accept
                tcp dport { 80, 443, 8080 } accept
                ip saddr { 1.2.3.4, 192.168.0.0/24 } tcp dport { 80, 443 } accept
        }

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

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

那么问题来了:

BASH
tcp dport { 80, 443, 8080 } accept
ip saddr { 1.2.3.4, 192.168.0.0/24 } tcp dport { 80, 443 } accept

前面允许了所有IP访问80、443端口,下面又配置了特定的IP才能访问80、443端口,这个策略到底听谁的呢?

用一句话总结:

nftables 规则是 从上到下 逐行匹配的,只要匹配到了第一条符合条件的规则,立刻执行动作,后面的所有规则直接跳过、不再看!

所以这两条规则,只有一条生效了:任何 IP 访问 80/443 端口,立刻匹配到规则 1 直接执行 accept 放行,规则 2 完全不会被执行,等于白写。

So,这也就诞生了规则的相关管控:严格规则放上面,宽松规则放下面。

2.5 Docker 网络兼容配置

在配置好 nftables 防火墙后,如果你直接启动 Docker 容器,有概率会遇到容器无法启动、端口映射失败、网络报错的问题,我先给大家看一个真实的启动报错:

BASH
docker run -d --name nginxpulse \
  -p 8088:8088 \
  -v ./docker_local/logs:/share/logs:ro \
  -v ./docker_local/nginxpulse_data:/app/var/nginxpulse_data \
  -v ./docker_local/pgdata:/app/var/pgdata \
  -v ./docker_local/configs:/app/configs \
  -v /etc/localtime:/etc/localtime:ro \
  magiccoders/nginxpulse:latest

3d43009c5bcc16053915f71d1983bfe57a6b8e4267d7e51eec00bbed667d8cdb
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint nginxpulse (586d37fbd055569b34d46eb43514bcade4182c5c21e75344fb9ae3b7dcd1f9a3): Unable to enable DNAT rule:  (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 8088 -j DNAT --to-destination 172.17.0.3:8088 ! -i docker0: iptables: No chain/target/match by that name.
 (exit status 1))

Run 'docker run --help' for more information

为什么会出现这个错误?

这是 Docker + nftables 最常见的兼容问题:

我们的防火墙默认禁止了所有流量转发,而 Docker 运行必须依赖流量转发才能实现端口映射、容器联网、容器间通信;Docker 需要自动创建网络地址转换(NAT)规则,没有防火墙放行权限,就会直接创建失败;

简单说:防火墙拦住了 Docker 的网络请求,导致容器无法启动。想要正常使用 Docker,我们必须在 nftables 中添加专属的网络兼容规则。整体配置:

BASH
# 转发链
chain forward {
    type filter hook forward priority filter; policy drop;
    # 允许外部流量通过端口映射访问Docker容器(新连接)
    iifname != "docker0" oifname "docker0" ct state new accept
    # 允许外部网络回复的流量,进入Docker容器
    oifname "docker0" ip daddr 172.17.0.0/16 ct state established,related accept
    # 允许Docker容器主动发出的流量,访问外部网络
    iifname "docker0" ip saddr 172.17.0.0/16 accept
}

# NAT 规则表(Docker 端口映射、联网核心配置)
table inet nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname != "docker0" ip saddr 172.17.0.0/16 ip daddr != 127.0.0.0/8 masquerade
    }
    chain DOCKER {
        type nat hook prerouting priority dstnat; policy accept;
    }
    chain DOCKER_OUTPUT {
        type nat hook output priority -100; policy accept;
    }
}

我们先来讲解转发部分内容:

BASH
# 允许外部流量通过端口映射访问Docker容器(新连接)
iifname != "docker0" oifname "docker0" ct state new accept

# 允许外部网络回复的流量,进入Docker容器
oifname "docker0" ip daddr 172.17.0.0/16 ct state established,related accept

# 允许Docker容器主动发出的流量,访问外部网络
iifname "docker0" ip saddr 172.17.0.0/16 accept

字段配置:

  • oifname流量要出去的网卡名称(output interface name)
  • iifname流量进来的网卡名称(input interface name)
  • "docker0":Docker 自动创建的虚拟网卡,所有容器都通过它联网
  • ip daddr流量的目标 IP 地址(destination address)
  • 172.17.0.0/16:Docker 容器默认的内部局域网 IP 段,注意相关网段进行修改
  • ct state established,related外部服务器返回的正常响应流量
  • ct state new新建连接,用于放行外部通过端口映射主动访问容器的流量
  • accept:放行

为什么需要 ct state new 这条规则? Docker 通过 DNAT(目标地址转换)实现端口映射,外部访问宿主机映射端口时,流量会被转发到容器内部。如果 forward 链没有放行这类新连接,policy drop 会先丢弃数据包,导致端口映射失败。这条规则确保外部流量能正常到达容器。

总结:forward 链做了三件事——①放行外部主动访问容器的端口映射流量(新连接),②放行外部服务回复给容器的响应流量(已建立连接),③放行容器主动发出的所有出站流量。


下面讲解NAT 规则表:

BASH
# NAT 规则表:专门管「网络地址转换」,Docker 联网、端口映射全靠它
table inet nat {
    # 出口转发规则:容器访问外网的核心
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        # IP 伪装:让Docker容器可以正常访问互联网(排除回环地址)
        oifname != "docker0" ip saddr 172.17.0.0/16 ip daddr != 127.0.0.0/8 masquerade
    }

    # 以下两条是 Docker 内置固定规则
    # Docker 端口映射必需的系统链
    chain DOCKER {
        type nat hook prerouting priority dstnat; policy accept;
    }
    # Docker 内部网络转发必需链
    chain DOCKER_OUTPUT {
        type nat hook output priority -100; policy accept;
    }
}

table inet nat

这是全新的规则表,和之前的 filter 表分工不同:

  • filter 表:管放行 / 拦截
  • nat 表:管容器上网、端口映射,没有它 Docker 完全无法工作;

chain postrouting:它是流量出门检查站。所有Docker 容器想访问外网的流量,都必须经过这里。

oifname != "docker0":流量不是发给 Docker 内部的(要出公网的)

ip saddr 172.17.0.0/16:流量来源是 Docker 容器内部网段

ip daddr != 127.0.0.0/8:排除目标为回环地址的流量,避免容器访问本机 127.0.0.1 时被错误伪装

masquerade(IP 伪装):Docker 容器没有独立公网 IP,靠这个规则共享服务器的网络上网;

DOCKER / DOCKER_OUTPUT:这是 Docker 自己要求的固定系统链,必须原样写,是端口映射的底层支撑。

So,容器想访问外网时,把容器的内部 IP,伪装成服务器的公网 IP。加上 ip daddr != 127.0.0.0/8 是为了确保容器访问宿主机本地服务时走正常路径,不被伪装规则干扰。

这个部分可能会有点绕,我也花了好长时间理解,总结就使用下面的模版:

BASH
#!/usr/sbin/nft -f
flush ruleset

# 防火墙核心过滤规则表
table inet filter {
    # 入站流量规则
    chain input {
        type filter hook input priority filter; policy drop;
    }
    # docker核心转发部分
    chain forward {
        type filter hook forward priority filter; policy drop;
        # 放行外部通过端口映射访问容器的新连接
        iifname != "docker0" oifname "docker0" ct state new accept
        oifname "docker0" ip daddr 172.17.0.0/16 ct state established,related accept
        iifname "docker0" ip saddr 172.17.0.0/16 accept
    }

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

# Docker NAT 网络规则
table inet nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname != "docker0" ip saddr 172.17.0.0/16 ip daddr != 127.0.0.0/8 masquerade
    }
    chain DOCKER {
        type nat hook prerouting priority dstnat; policy accept;
    }
    chain DOCKER_OUTPUT {
        type nat hook output priority -100; policy accept;
    }
}

配置加载完成后,我们执行 nft list ruleset 命令,重启Docker后就能看到服务器所有生效的防火墙规则,以下是真实运行效果展示(这里仅加载了 Docker 兼容模版做演示,input 链中尚未添加前面章节的基础安全规则,实际生产环境应合并所有规则后统一加载):

BASH
$ nft list ruleset 
table inet filter {
        chain input {
                type filter hook input priority 0;
        }

        chain forward {
                type filter hook forward priority filter; policy drop;
                oifname "docker0" ip daddr 172.17.0.0/16 ct state established,related accept
                iifname "docker0" ip saddr 172.17.0.0/16 accept
        }

        chain output {
                type filter hook output priority filter; policy accept;
        }
}
table inet nat {
        chain postrouting {
                type nat hook postrouting priority srcnat; policy accept;
                oifname != "docker0" ip daddr != 127.0.0.0/8 masquerade
        }

        chain DOCKER {
                type nat hook prerouting priority dstnat; policy accept;
        }

        chain DOCKER_OUTPUT {
                type nat hook output priority -100; policy accept;
        }
}
table ip nat {
        chain DOCKER {
                iifname != "docker0" meta l4proto tcp tcp dport 5432 counter packets 60 bytes 2844 dnat to 172.17.0.2:5432
        }

        chain PREROUTING {
                type nat hook prerouting priority dstnat; policy accept;
                fib daddr type local counter packets 175176 bytes 9571421 jump DOCKER
        }

        chain OUTPUT {
                type nat hook output priority -100; policy accept;
                ip daddr != 127.0.0.0/8 fib daddr type local counter packets 27678 bytes 1660680 jump DOCKER
        }

        chain POSTROUTING {
                type nat hook postrouting priority srcnat; policy accept;
                oifname != "docker0" ip saddr 172.17.0.0/16 counter packets 0 bytes 0 masquerade 
                oifname != "br-afdd646856cb" ip saddr 172.18.0.0/16 counter packets 0 bytes 0 masquerade 
        }
}
table ip filter {
        chain DOCKER {
                iifname != "docker0" oifname "docker0" meta l4proto tcp ip daddr 172.17.0.2 tcp dport 5432 counter packets 60 bytes 2844 accept
                iifname != "br-afdd646856cb" oifname "br-afdd646856cb" counter packets 0 bytes 0 drop
                iifname != "docker0" oifname "docker0" counter packets 0 bytes 0 drop
        }

        chain DOCKER-FORWARD {
                counter packets 209 bytes 11180 jump DOCKER-CT
                counter packets 209 bytes 11180 jump DOCKER-INTERNAL
                counter packets 209 bytes 11180 jump DOCKER-BRIDGE
                iifname "br-afdd646856cb" counter packets 0 bytes 0 accept
                iifname "docker0" counter packets 0 bytes 0 accept
        }

        chain DOCKER-BRIDGE {
                oifname "br-afdd646856cb" counter packets 0 bytes 0 jump DOCKER
                oifname "docker0" counter packets 209 bytes 11180 jump DOCKER
        }

        chain DOCKER-CT {
                oifname "br-afdd646856cb" ct state related,established counter packets 0 bytes 0 accept
                oifname "docker0" ct state related,established counter packets 0 bytes 0 accept
        }

        chain DOCKER-INTERNAL {
        }

        chain FORWARD {
                type filter hook forward priority filter; policy accept;
                counter packets 209 bytes 11180 jump DOCKER-USER
                counter packets 209 bytes 11180 jump DOCKER-FORWARD
        }

        chain DOCKER-USER {
        }
}
table ip6 nat {
        chain DOCKER {
        }

        chain PREROUTING {
                type nat hook prerouting priority dstnat; policy accept;
                fib daddr type local counter packets 0 bytes 0 jump DOCKER
        }

        chain OUTPUT {
                type nat hook output priority -100; policy accept;
                ip6 daddr != ::1 fib daddr type local counter packets 0 bytes 0 jump DOCKER
        }
}
table ip6 filter {
        chain DOCKER {
        }

        chain DOCKER-FORWARD {
                counter packets 0 bytes 0 jump DOCKER-CT
                counter packets 0 bytes 0 jump DOCKER-INTERNAL
                counter packets 0 bytes 0 jump DOCKER-BRIDGE
        }

        chain DOCKER-BRIDGE {
        }

        chain DOCKER-CT {
        }

        chain DOCKER-INTERNAL {
        }

        chain FORWARD {
                type filter hook forward priority filter; policy accept;
                counter packets 0 bytes 0 jump DOCKER-USER
                counter packets 0 bytes 0 jump DOCKER-FORWARD
        }

        chain DOCKER-USER {
        }
}
table ip raw {
        chain PREROUTING {
                type filter hook prerouting priority raw; policy accept;
                iifname != "docker0" ip daddr 172.17.0.2 counter packets 0 bytes 0 drop
        }
}

table inet filter + table inet nat

这是我们手动配置的 Docker 兼容规则,包含流量转发、NAT 地址转换,是 Docker 网络正常运行的核心。

剩余所有规则(ip nat/ip filter/ip6 等):这是 Docker 服务自动生成的系统规则,用于管理容器端口映射、网络桥接,无需我们手动编写、修改、维护,Docker 会自动管理。

补充说明: 以上 ipip6 家族的表(如 ip natip filterip6 natip6 filterip raw 等)全部是 Docker 服务启动时自动创建和维护的,它们负责处理容器间的网络桥接、端口转发、安全隔离等底层网络逻辑。我们不需要手动修改这些规则,也不建议删除或调整,重启 Docker 后它们会自动重建。

看到这个输出,就证明 nftables + Docker 已完全兼容,容器可以正常启动、联网、端口映射。

配置完成后记得重启Docker,才会生成相应的Docker转发工具。


🚨 重要细节

如果大家之前启动过一次Docker,加载完Docker的转发之后,后续新增了相关规则,并重新执行,会发现加载完 nftables 配置后,Docker 自动生成的规则表全部消失了。

这是完全正常的现象,原因如下:

我们的配置文件第一行 flush ruleset清空服务器所有防火墙规则,Docker 自动生成的规则属于内存临时规则,会被一并清空,所以在新一次加载防火墙规则之后,要重启Docker:

BASH
# 1. 加载防火墙配置
nft -f /etc/nftables.conf

# 2. 重启 Docker 自动重建网络规则
systemctl restart docker

2.6 端口速率限制

配置完端口放行后,我们还可以给端口加上速率限制,这是服务器安全的重要一环。

简单来说:限制同一个 IP,在单位时间内访问指定端口的最大次数,可以有效防止 SSH 暴力破解、CC 攻击、端口恶意扫描,保护服务器不被流量压垮。

nftables 内置了速率限制模块,我们可以直接配置:

  • 允许正常访问
  • 拦截短时间内疯狂请求的恶意 IP
  • 支持自定义:每分钟 / 每秒允许多少次请求

场景 1:SSH 端口速率限制

这应该是最常用的了,可以有效防止ssh暴力破解,我们优先给 SSH(22 端口)加限制,这是服务器最容易被攻击的端口。

规则示例:同一个 IP,每分钟最多尝试连接 SSH 3 次,超过直接拦截。

BASH
# SSH 端口速率限制:每分钟最多3次连接,突发5次(容错)
ip saddr { 1.2.3.4 } tcp dport 22 ct state new limit rate 3/minute burst 5 accept

说明: 这条规则仅对白名单 IP 生效。如果你已在 2.2 节配置了 SSH IP 白名单,可以用这条规则替换原来的白名单规则,在允许访问的同时限制白名单 IP 的访问频率,防止内部滥用或账号泄露后的暴力破解。

limit rate 3/minute核心,每分钟最多允许 3 次新连接。

burst 5:突发流量容错,允许短时间多几次请求,避免正常操作被误拦,仅对白名单 IP 生效,更安全。

场景 2:Web 端口速率限制(防 CC 攻击)

给 80/443 等 Web 端口加限制,防止恶意请求刷爆服务。

规则示例:同一个 IP,每秒最多 20 次请求

BASH
# Web端口速率限制:每秒最多20次请求
tcp dport { 80, 443 } ct state new limit rate 20/second accept

场景 3:自定义业务端口速率限制

针对服务器开放的 3000、4096、8088 等自定义业务端口,添加专属限速,避免单个 IP 占用大量资源。

规则示例:单个 IP 每秒最多 10 次新连接

BASH
# 自定义业务端口限速
tcp dport { 3000, 4096, 8088 } ct state new limit rate 10/second accept

2.7 限制 Ping 请求

Ping 是我们日常测试服务器网络连通性的常用工具,但它也存在安全风险:黑客可能利用 ICMP 洪水攻击(疯狂发送 Ping 请求)消耗服务器资源,也能通过 Ping 扫描判断你的服务器是否在线。

借助 nftables,我们可以灵活管控 Ping 请求:既可以保留正常的 Ping 功能,又限制请求频率;也可以直接禁止 Ping,让服务器在网络中更隐蔽。

小tips:我们使用的 Ping 命令,底层依靠 ICMP 协议传输数据,所以我们只需要针对这个协议配置规则即可。

场景 1:允许 Ping + 速率限制

不影响正常的网络连通性测试,同时拦截恶意高频 Ping,是最实用的方案。

规则示例:同一个 IP,每分钟最多允许发起 10 次 Ping 请求

BASH
chain input {
                type filter hook input priority filter; policy drop;
                # 限制Ping请求:每分钟最多10次,防止ICMP洪水攻击
                ip protocol icmp limit rate 10/minute accept
        }

ip protocol icmp:匹配 Ping 所使用的 ICMP 协议。

limit rate 10/minute:核心限速,每分钟最多允许 10 次请求。

accept:放行符合频率要求的正常 Ping 请求。

场景 2:完全禁止 Ping 请求

如果你不需要任何人 Ping 你的服务器,可以直接拦截所有 ICMP 请求,让外部无法通过 Ping 探测到服务器。

BASH
chain input {
                type filter hook input priority filter; policy drop;
                # 完全禁止所有Ping请求
				ip protocol icmp drop
        }

drop:直接丢弃所有 Ping 请求,外界 Ping 服务器会显示超时 / 不通。

2.8 IP黑名单配置

如果我们发现有恶意 IP持续攻击、扫描、暴力破解我们的服务器,单纯的速率限制已经不够用了,这时候我们可以直接将其加入IP 黑名单彻底封禁该 IP 访问服务器的所有权限,这是最直接、最强硬的安全防护手段。

nftables 配置黑名单非常简单,核心规则只有一个:drop(丢弃),支持单个 IP、多个 IP、整个 IP 段批量封禁。

**黑名单规则必须放在 input 链的最顶部!**正如我们在2.4中提到的,nftables 规则是 从上到下 逐行匹配的,只要匹配到了第一条符合条件的规则,立刻执行动作,后面的所有规则直接跳过、不再看。

因为 nftables 规则从上到下匹配,先拦截黑名单 IP,再执行后续的放行、限速规则,否则黑名单会失效。

场景 1:封禁单个恶意 IP

直接封禁某个攻击你的 IP,禁止其访问服务器任何端口(SSH、Web、业务端口全部禁止)。

BASH
# 封禁单个恶意IP 192.168.1.100,直接丢弃所有请求
ip saddr 192.168.1.100 drop

场景 2:封禁多个恶意 IP

同时封禁多个扫描、攻击 IP,用大括号 + 英文逗号分隔。

BASH
# 批量封禁多个恶意IP
ip saddr { 1.2.3.4, 5.6.7.8, 11.22.33.44 } drop

场景 3:封禁整个IP 段

如果发现一整个网段都在恶意攻击,可以直接封禁整个网段。

BASH
# 封禁整个IP段 123.45.0.0/16
ip saddr 123.45.0.0/16 drop

那么完整配合使用:

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

    # IP黑名单,最顶部,优先执行
    ip saddr { 1.2.3.4, 5.6.7.8 } drop
    ip saddr { 1.2.3.4, 5.6.7.8, 11.22.33.44 } drop
    ip saddr 123.45.0.0/16 drop

    # 基础安全规则
    iif "lo" accept
    ct state established,related accept
    ct state invalid drop
    # ......
}

小提醒:千万不要封禁自己的 IP,否则你会无法连接服务器!黑名单优先级最高,一旦封禁,该 IP 无法访问任何服务;如需解除封禁,删除配置中对应的 IP 规则,重新加载即可。

2.9 IPv6 基础防护

随着服务器网络环境的完善,IPv6 已经成为很多服务器的标配网络协议。和 IPv4 一样,IPv6 也面临着暴力破解、端口扫描、恶意访问等安全风险,我们需要在 nftables 中为 IPv6 配置基础防护,做到IPv4 与 IPv6 双重安全

IPv4 和 IPv6 是两套完全独立的网络协议,防火墙规则互不通用、互不继承;我们仅配置了 IPv4 的防火墙规则,限制了 80、443、3000 等业务端口的访问权限;由于未配置 IPv6 防火墙规则,IPv6 流量默认不受限制、所有端口均放行,因此纯 IPv6 地址的用户可以正常访问你的网站 / 服务,不会被拦截。

接下来我们先看一个独立配置 IPv6 防火墙规则的示例。不过请注意:由于 inet 族本身同时覆盖 IPv4 和 IPv6 流量,这里单独建一个 filter6也会处理 IPv4 流量,表名只是标识符,不会限定协议。所以后面我们会讲更推荐的做法——直接在同一个 inet filter 表中统一管理,这里仅作对比理解使用。

BASH
# IPv6 防火墙配置(说明性示例,推荐使用后文的统一方案)
table inet filter6 {
    chain input {
        type filter hook input priority filter; policy drop;

        # 1. 放行本地回环接口
        iif "lo" accept

        # 2. 放行已建立、相关的连接
        ct state established,related accept

        # 3. 丢弃无效的数据包
        ct state invalid drop

        # 4. IPv6 SSH 白名单
        # 仅允许信任的 IPv6 地址访问 SSH 端口,按需开启
        # ip6 saddr { 240e:xxx:xxx::1/64 } tcp dport 22 ct state new accept

        # 5. 放行业务端口
        # 示例:放行 Web 端口、自定义业务端口
        tcp dport { 80, 443, 3000, 8088 } ct state new accept

        # 6. IPv6 Ping 限速
        meta l4proto icmpv6 limit rate 10/minute accept
    }

    # 转发链:保持默认拦截,仅放行必要流量
    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    # 出站链:默认放行,保障服务器主动访问网络
    chain output {
        type filter hook output priority filter; policy accept;
    }
}

在 nftables 防火墙体系中,inet 是协议家族关键字,代表这张表同时管理 IPv4 和 IPv6 流量,是现代 Linux 系统配置双栈防火墙的核心。

它打破了传统 iptables(仅 IPv4)和 ip6tables(仅 IPv6)协议分离的架构,将两种协议的流量规则整合在同一张表中,实现网络流量过滤规则的集中化管理。

BASH
# 你的服务器配置:标准双栈统一表(表名 filter)
table inet filter {
    # 入站流量链:管控所有进入服务器的流量
    chain input {
        type filter hook input priority filter; policy drop;

        # 放行本地回环流量
        iif lo accept

        # 放行已建立连接的流量(保障会话正常通信)
        ct state established,related accept

        # 丢弃无效的数据包
        ct state invalid drop

        # 放行网站/业务端口(双协议通用:IPv4+IPv6同时生效)
        tcp dport {80, 443, 81, 4096, 3000} ct state new accept

        # Ping 限速(防洪水攻击)
        ip protocol icmp limit rate 10/minute accept
        meta l4proto icmpv6 limit rate 10/minute accept
    }

    # 转发流量链
    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    # 出站流量链
    chain output {
        type filter hook output priority filter; policy accept;
    }
}

总结:

规则复用:双协议流量共享同一套规则逻辑,减少重复配置,降低运维成本。

性能更优:nftables 内核原生支持,规则执行效率高于传统的 iptables 组合,在高流量场景下表现更稳定。

适配现代网络:随着 IPv6 网络的普及,table inet filter 成为服务器防火墙的标准配置,完美适配双栈网络环境。

配置灵活:支持分链管理流量(input/forward/output),规则结构清晰,便于后期扩展和维护。

2.10 Docker Compose 多网桥兼容

前面我们讲的 Docker 兼容配置,都是基于默认的 docker0 网桥。但实际生产环境中,如果你使用了 Docker Compose,它会为每个项目自动创建独立的自定义网桥(名称类似 br-afdd646856cb),分配独立的内网网段(如 172.18.0.0/16172.19.0.0/16 等)。

这时候,我们之前只针对 docker0 写的转发和 NAT 规则,对自定义网桥是不生效的,会导致 Docker Compose 项目中的容器无法联网、端口映射失败。

怎么解决?

我们需要在 forward 链和 nat 表中,为每个自定义网桥添加对应的规则

第一步:查看你的 Docker 网桥

先确认服务器上有哪些 Docker 网桥,以及它们的网段:

BASH
# 查看所有 Docker 网络
docker network ls

# 查看具体网络的网段信息(替换 network_name 为实际名称)
docker network inspect network_name | grep Subnet

假设你的 Docker Compose 项目创建了一个网桥 br-afdd646856cb,网段为 172.18.0.0/16

第二步:补充 forward 链规则

forward 链中,为新网桥添加和 docker0 一样的三条规则:

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

    # === 默认网桥 docker0 (172.17.0.0/16)===
    iifname != "docker0" oifname "docker0" ct state new accept
    oifname "docker0" ip daddr 172.17.0.0/16 ct state established,related accept
    iifname "docker0" ip saddr 172.17.0.0/16 accept

    # === Docker Compose 自定义网桥(按你的实际网桥名和网段修改)===
    iifname != "br-afdd646856cb" oifname "br-afdd646856cb" ct state new accept
    oifname "br-afdd646856cb" ip daddr 172.18.0.0/16 ct state established,related accept
    iifname "br-afdd646856cb" ip saddr 172.18.0.0/16 accept
}

第三步:补充 NAT 表规则

nat 表的 postrouting 链中,为新网桥添加 IP 伪装规则:

BASH
chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    # 默认网桥
    oifname != "docker0" ip saddr 172.17.0.0/16 ip daddr != 127.0.0.0/8 masquerade
    # Docker Compose 自定义网桥
    oifname != "br-afdd646856cb" ip saddr 172.18.0.0/16 ip daddr != 127.0.0.0/8 masquerade
}

核心逻辑和 docker0 完全一样,只是把网桥名和网段替换成自定义网络的即可。如果有多个 Docker Compose 项目,就依次添加对应的规则。

小提示: 每次新建 Docker Compose 项目并创建了新的自定义网络后,记得检查防火墙规则是否覆盖了新网桥。如果漏了,该项目的容器就会出现网络不通或端口映射失败的情况。

2.11 流量日志与计数审计

在生产环境中,防火墙不仅要拦得住攻击,还要看得见攻击。nftables 提供了 counter(计数器)和 log(日志)两个工具,帮助我们统计流量数据、记录被拦截的访问,便于安全审计和问题排查。

2.11.1 计数器 counter

counter 可以统计每条规则匹配了多少个数据包、多少字节的流量,是最轻量的审计手段,对性能几乎没有影响

使用方式非常简单,在任意规则的动作前加上 counter 关键字即可:

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

    # 统计被丢弃的无效数据包数量
    ct state invalid counter drop

    # 统计 SSH 白名单的访问次数
    ip saddr { 1.2.3.4 } tcp dport 22 ct state new counter accept

    # 统计 Web 端口的访问量
    tcp dport { 80, 443 } ct state new counter accept
}

加载规则后,通过 nft list ruleset 查看,每条带 counter 的规则后面会自动显示统计数据:

BASH
$ nft list ruleset
# 输出示例(截取部分)
ct state invalid counter packets 1523 bytes 72816 drop
ip saddr { 1.2.3.4 } tcp dport 22 ct state new counter packets 47 bytes 2820 accept
tcp dport { 80, 443 } ct state new counter packets 18692 bytes 1121520 accept

packets 1523 表示该规则命中了 1523 个数据包,bytes 72816 表示总流量大小。通过这些数据,你可以清晰地看到:有多少无效包被拦截、SSH 被尝试了多少次、Web 服务承受了多少请求。

2.11.2 日志 log

如果你需要详细记录某类流量的具体信息(来源 IP、目标端口、协议等),可以使用 log 将信息写入系统日志。

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

    # 记录所有被拦截的入站流量(写入系统日志)
    counter log prefix "[nftables-drop] " drop

    # 记录可疑的 SSH 尝试
    tcp dport 22 ct state new log prefix "[nftables-ssh] " counter accept
}

log prefix "[nftables-drop] ":给日志条目加一个前缀标签,方便在系统日志中快速筛选。

日志会写入系统日志文件,通过以下命令查看:

BASH
# 实时查看防火墙日志
journalctl -k | grep "nftables"

# 或者查看 syslog
grep "nftables" /var/log/syslog

⚠️ 注意: log 在高流量场景下会产生大量日志数据,可能影响磁盘 I/O 和存储空间。建议仅对关键规则启用日志(如被拦截的流量、SSH 尝试),不要对所有规则都加 log。日常运维中,counter 已经够用,log 作为排查问题时的按需开启工具即可。

2.11.3 推荐组合

日常生产环境,推荐以下轻量审计策略

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

    iif "lo" accept
    ct state established,related accept

    # 统计无效包数量(轻量审计)
    ct state invalid counter drop

    # SSH 白名单 + 计数
    ip saddr { 1.2.3.4 } tcp dport 22 ct state new counter accept

    # 业务端口 + 计数
    tcp dport { 80, 443 } ct state new counter accept

    # Ping 限速 + 计数
    ip protocol icmp limit rate 10/minute counter accept
}

这套配置零性能损耗,随时通过 nft list ruleset 就能看到每条规则的命中情况,是生产环境最实用的审计方式。

2.12 开机自启与持久化

到目前为止,我们所有的防火墙配置,都是通过 nft -f /etc/nftables.conf 手动加载的。但如果服务器重启了怎么办?

答案是:规则会全部丢失,服务器重新回到裸奔状态。

因为 nftables 的规则是加载到内核内存中的,服务器重启后内存被清空,规则自然就没了。所以我们必须配置开机自动加载防火墙规则,确保服务器在任何情况下重启后,防火墙都能自动生效。

2.12.1 确认配置文件路径

确保你的防火墙规则已经完整写入 /etc/nftables.conf,这是 nftables 服务默认读取的配置文件。

BASH
# 验证配置文件语法是否正确(不会实际加载,只做语法检查)
nft -c -f /etc/nftables.conf

如果没有任何输出,说明语法正确;如果报错,需要先修正配置文件。

2.12.2 启用 nftables 系统服务

Ubuntu 系统自带 nftables 的 systemd 服务单元,我们只需要启用它:

BASH
# 设置 nftables 开机自启
systemctl enable nftables

# 立即启动服务(加载配置文件中的规则)
systemctl start nftables

# 查看服务状态,确认正常运行
systemctl status nftables

执行 systemctl enable nftables 后,系统会在每次开机时自动执行 /etc/nftables.conf,加载防火墙规则。

2.12.3 验证持久化效果

配置完成后,建议通过以下步骤验证:

BASH
# 1. 确认当前规则已生效
nft list ruleset

# 2. 重启服务器
reboot

# 3. 重启后重新登录,再次检查规则是否自动加载
nft list ruleset

如果重启后 nft list ruleset 能看到完整的防火墙规则,说明持久化配置成功

2.12.4 完整的规则更新流程

日后每次修改防火墙规则,推荐按以下标准流程操作:

BASH
# 1. 编辑配置文件
vim /etc/nftables.conf

# 2. 语法检查(养成好习惯,避免加载错误的配置导致断连)
nft -c -f /etc/nftables.conf

# 3. 加载新规则
nft -f /etc/nftables.conf

# 4. 如果有 Docker,重启 Docker 重建网络规则
systemctl restart docker

# 5. 验证规则生效
nft list ruleset

⚠️ 特别注意: 语法检查这一步非常重要!如果配置文件有语法错误,直接加载可能导致 flush ruleset 执行成功(清空所有规则),但新规则加载失败,服务器瞬间变成无防火墙状态。先用 -c 参数检查,确认无误后再加载,这是生产环境的铁律。

三、总结

在公网直连、无安全组防护的 Ubuntu 22.04 服务器上,搭配 Docker 容器使用时,我们基于 nftables 打造了一套极简高性能的防火墙方案。不依赖云厂商防护工具,仅依靠系统自身能力,彻底解决服务器直面公网的安全风险,适配轻量服务器的硬件特性,零额外性能损耗。

从基础安全加固到 Docker 网络兼容(包括 Docker Compose 多网桥场景),从 SSH 防护到端口限流,从流量审计到开机持久化,我们从零落地全流程配置,打造生产可用的防火墙体系,让裸奔服务器获得专业级安全防护,兼顾安全、稳定与运维便捷性。

附、运维命令

BASH
# 1. 加载防火墙规则(修改配置后必执行,立即生效)
nft -f /etc/nftables.conf

# 2. 加载前语法检查(强烈推荐,避免加载失败导致防火墙失效)
nft -c -f /etc/nftables.conf

# 3. 查看当前所有生效的防火墙规则
nft list ruleset

# 4. 查看带 handle 编号的规则(删除规则时使用)
nft -a list ruleset

# 5. 设置 nftables 开机自启(服务器重启后自动生效)
systemctl enable nftables

# 6. 启动 / 查看 nftables 服务状态
systemctl start nftables
systemctl status nftables

# 7. 重启 Docker(加载防火墙规则后必执行,修复容器网络)
systemctl restart docker

# 8. 查看 Docker 网桥和网段(配置多网桥兼容时使用)
docker network ls
docker network inspect <网络名称> | grep Subnet

# 9. 查看防火墙日志(配合 log 规则使用)
journalctl -k | grep "nftables"
grep "nftables" /var/log/syslog

# 10. 临时清空所有防火墙规则(紧急救急使用)
nft flush ruleset

# 11. 查看服务器对外开放端口(验证防护效果)
ss -tuln
netstat -lnpt

本人示例

BASH
#!/usr/sbin/nft -f
flush ruleset

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

        iif "lo" accept
        ct state established,related accept
        ct state invalid counter drop

        ip saddr { 1.2.3.4 } tcp dport 22 ct state new limit rate 3/minute burst 5 packets counter accept

        tcp dport { 80, 443 } ct state new limit rate 20/second burst 40 packets counter accept

        ip protocol icmp limit rate 10/minute counter accept
        meta l4proto icmpv6 limit rate 10/minute counter accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
        iifname != "docker0" oifname "docker0" ct state new accept
        oifname "docker0" ip daddr 172.17.0.0/16 ct state established,related accept
        iifname "docker0" ip saddr 172.17.0.0/16 accept
    }

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

table inet nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname != "docker0" ip saddr 172.17.0.0/16 ip daddr != 127.0.0.0/8 masquerade
    }

    chain DOCKER {
        type nat hook prerouting priority dstnat; policy accept;
    }

    chain DOCKER_OUTPUT {
        type nat hook output priority -100; policy accept;
    }
}

评论