Linux nftables 防火墙

nftables 是一个 netfilter 项目,旨在替换现有的 {ip,ip6,arp,eb}tables 框架,为 {ip,ip6}tables 提供一个新的包过滤框架、一个新的用户空间实用程序( nft )和一个兼容层( iptables-nft )。它使用现有的钩子、链接跟踪系统、用户空间排队组件和 netfilter 日志子系统。

在 Linux 内核版本高于 3.13 时可用

它由三个主要组件组成:

  • 内核实现: 内核提供了一个 netlink 配置接口以及运行时规则集评估
  • libnl netlink : libnl 包含了与内核通信的基本函数
  • nftables : 用户空间前端。 nftables 的用户空间实用程序 nft 评估大多数规则集并传递到内核。规则存储在链中,链存储在表中。

与 iptables 不同点

  • nftables 在用户空间中运行, iptables 中的每个模块都运行在内核(空间)中
  • 表和链是完全可配置的 。在 nftables 中,表是没有特定语义的链的容器。 iptables 附带了具有预定义数量的基链的表,即使您只需要其中之一,所有链都已注册,未使用的基础链也会损害性能。
  • 可以在一个规则中指定多个操作 。在 iptables 中,您只能指定一个目标。这是一个长期存在的局限性,用户可以通过跳到自定义链来解决,但代价是使规则集结构稍微复杂一些。
  • 每个链和规则没有内置计数器 。在 nftables 中,这些是可选的,因此您可以按需启用计数器。由于 iptables 内置了一个数据包计数器,所以即使这些内置的链是空的,也会带来性能损耗
  • 更好地支持动态规则集更新 。在 nftables 中,如果添加新规则,则剩余的现有规则将保持不变,因为规则集以链表形式表示,这与整体式 blob 表示相反,后者在执行规则集更新时内部状态信息的维护很复杂。
  • 简化的双堆栈 IPv4/IPv6 管理 ,通过新的 inet 系列,可让您注册同时查看 IPv4 和 IPv6 流量的基链。 因此,您不再需要依赖脚本来复制规则集。

服务名称默认为 nftables ,默认配置文件为 /etc/nftables.conf ,其中已经包含一个名为 inet filter 的简单 ipv4/ipv6 防火墙列表。

/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;
}
}

nftables 服务的 systemd 配置文件如下:

/lib/systemd/system/nftables.service
[Unit]
Description=nftables
Documentation=man:nft(8) http://wiki.nftables.org
Wants=network-pre.target
Before=network-pre.target shutdown.target
Conflicts=shutdown.target
DefaultDependencies=no

[Service]
Type=oneshot
RemainAfterExit=yes
StandardInput=null
ProtectSystem=full
ProtectHome=true
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
ExecStop=/usr/sbin/nft flush ruleset

[Install]
WantedBy=sysinit.target

nftables 使用的内核模块如下,加载这些模块,服务才能正常运行

# lsmod | grep nf
nf_log_syslog 20480 1
nft_chain_nat 16384 17
nf_nat 49152 3 xt_nat,nft_chain_nat,xt_MASQUERADE
nf_conntrack_netlink 49152 0
nft_counter 16384 142
nf_reject_ipv4 16384 1 ipt_REJECT
nf_conntrack 172032 6 xt_conntrack,nf_nat,xt_state,xt_nat,nf_conntrack_netlink,xt_MASQUERADE
nf_defrag_ipv6 24576 1 nf_conntrack
nf_defrag_ipv4 16384 1 nf_conntrack
nft_compat 20480 143
nf_tables 249856 570 nft_compat,nft_counter,nft_chain_nat
nfnetlink 20480 5 nft_compat,nf_conntrack_netlink,nf_tables,ip_set

nftables 在启动时(执行 nft -f /etc/nftables.conf )是顺序解析的。在使用了 include 动态加载 set (集合)的场景中, 如果它解析主配置文件没问题,但是子配置( include 文件)有语法错误,可能会出现主配置加载成功并已经应用,但是子配置未加载导致规则不全 ,因此建议修改 nftables 服务的启动流程,启动之前首先检查所有的配置文件( nft -c -f /etc/nftables.conf ),如果配置检查不通过,则不启动。

使用 systemctl edit 创建一个 drop-in 覆盖文件,而不是直接修改 /lib/systemd/system/nftables.service (这样可以防止系统更新时被覆盖)。

systemctl edit nftables.service

在其中写入以下内容

[Service]
# 在执行真正的启动 (ExecStart) 或重载 (ExecReload) 之前,先运行语法检查
# 注意:这里假设你的主配置文件是 /etc/nftables.conf,如果不是,请替换为实际路径
ExecStartPre=/usr/sbin/nft -c -f /etc/nftables.conf

nftables 配置构成组件

nftables Tables

nftables 中 表(Tables) 是没有特定语义的 链(Chains) 的容器, 表 包含 链 。

与 iptables 中的表不同, nftables 中没有内置表。表的数量和名称都由管理员自定义,但是,每个表都只能关联一个地址簇(Address Family),并且只适用于该地址簇的数据包。 表支持的地址簇如下:

nftables 框架地址簇 netfilter 工具 说明
ip iptables IPv4 地址
ip6 ip6tables IPv6 地址
inet iptables 和 ip6tables 同时包含 IPv4 和 IPv6 地址
arp arptables ARP 协议地址
bridge ebtables
  • ip (即 IPv4)是默认的地址簇,如果未明确指定地址簇,则使用 ip

  • 要创建同时适用于 IPv4 和 I Pv6 的规则,请使用 inet 。 inet 允许统一 ip 和 ip6 簇,以便更容易地定义规则。

    • 注意 : inet 不能用于 route 类型的链,只能用于 filter 和 nat 类型的链。 具体信息可以查看 man nft
  • 可以定义多个 相同地址簇(类型) 的表,如定义多个都是 inet 类型的表。每个表可以有自己的链( chain )和规则集( ruleset ),但这些表相互独立地存在,且没有直接的依赖关系。但 每个表中的链的处理顺序由链的优先级决定 。

nftables Chains

nftables 中 链(Chains) 的目的是保存规则。 与 iptables 中的链不同, nftables 没有内置链。 和表一样,链也需要被显示创建 。链有以下两种类型:

  • 基本链 : 数据包的入口点,必须(强制)指定 type , 钩子(hook) 和 优先级(priority) ,相当于 itables 的内置链
  • 常规链 : 不需要指定 钩子类型 和 优先级 ,可以用来做跳转,从逻辑上对规则进行分类,类似于 itables 的自定义链

这意味着与 iptables 不同,如果链没有使用 netfilter 框架中的任何类型或钩子,则流经这些链的数据包不会被 nftables 触及(处理) 。数据包要被实际处理,需要使用 链(Chains) 中的规则捕获包(使用具体的类型( filter , nat , route )和钩子( prerouting , input , forward , output , postrouting ))

  • 每个链( chain )都有一个 优先级(priority) 属性,决定了该链在处理数据包时的优先顺序。 优先级值可以是正数、负数或零,数值越小,优先级越高 。
  • 优先级仅在基本链( base chain )中定义 ,因为这些链直接挂载在网络堆栈的处理钩子( hook )上,参与系统的流量处理流程。

链的默认策略

在 nftables 中, 默认策略 指的是在链( chain )中当 没有匹配到任何规则时 ,数据包会执行的操作。与传统的 iptables 一样, nftables 也允许为每个 基本链( base chain ) 设置默认策略,比如 接受( accept ) 或 丢弃( drop ) 数据包。

默认策略为 accept

在 nftables 中,默认策略是通过设置链的 策略( policy ) 来定义的,只有 基本链( base chain ) 才能有默认策略。对于非基本链(比如用户自定义的链),需要在链中的规则中明确指定处理数据包的行为。

以下示例 创建基本链并设置默认策略 :

nft add table ip filter

# 创建 input 链,默认策略为 drop
nft add chain ip filter input { type filter hook input priority 0 \; policy drop \; }

# 创建 forward 链,默认策略为 accept
nft add chain ip filter forward { type filter hook forward priority 0 \; policy accept \; }

可以使用以下命令来 查看表和链的配置以及默认策略 :

nft list ruleset

如果需要 修改已有链的默认策略 ,可以使用以下命令来更新。例如:

# nft chain ip filter INPUT { policy drop\; }

配置默认策略的注意事项 :

  • 仅基本链支持默认策略 :只有与 hook (如 input 、 output 、 prerouting 等)关联的基本链才能有默认策略。自定义的非基本链必须显式定义处理规则。
  • 优先级 :当链有多个规则时,数据包首先会根据链中的规则进行匹配。如果没有规则匹配,默认策略才会生效。因此,如果链中存在明确的 accept 或 drop 规则,默认策略只会在所有规则都没有匹配时起作用。

nftables Rules

nftables 中 规则(Rules) 是实际处理数据包的语句或者表达式( EXPRESSION ),是真正的 Action,包含在 链(Chains) 中。

通常情况下, EXPRESSION 包含一些要匹配的表达式,然后是判断语句。结论语句包括 accept 、 drop 、 queue 、 continue 、 return 、 jump chain 和 goto chain 。也可能是其他陈述。有关信息信息,请参阅 nft(8) 。

  • 规则本身没有优先级属性 。规则在链中按 添加顺序 依次被处理。也就是说,链中的规则是按它们在链中添加的顺序进行匹配。

EXPRESSIONS

详细的 EXPRESSIONS 可以查看 man nft

DATA TYPES

nftables 中的每个 expressions 都有对应的数据类型( datatype ),具体的数据类型可以使用 nft describe 命令查看,如以下示例:

# nft describe tcp flags
payload expression, datatype tcp_flag (TCP flag) (basetype bitmask, integer), 8 bits

pre-defined symbolic constants (in hexadecimal):
fin 0x01
syn 0x02
rst 0x04
psh 0x08
ack 0x10
urg 0x20
ecn 0x40
cwr 0x80

Data Types 决定了对应的类型的值的大小,其代表的含义以及可以和它兼容的其他表达式。大多数的 data types 有固定的大小(长度),也有些 data types 有可变大小的值(如 字符串( string ))。也有些类型有预定义的常量值,如 tcp_flag 。

查看 data types 使用 nft describe <DATA_TYPE> 命令,如

# nft describe tcp_flag
datatype tcp_flag (TCP flag) (basetype bitmask, integer), 8 bits

pre-defined symbolic constants (in hexadecimal):
fin 0x01
syn 0x02
rst 0x04
psh 0x08
ack 0x10
urg 0x20
ecn 0x40
cwr 0x80

# nft describe ip saddr
payload expression, datatype ipv4_addr (IPv4 address) (basetype integer), 32 bits

有些 data type 是由其他基本的 data type (称为 basetype )组成,比如 ipv4_addr 由 integer 组成

META EXPRESSIONS

META EXPRESSIONS 是对数据包基本信息的描述。 META EXPRESSIONS 有 2 种类型:

  • Qualified Meta Expressions : 必须包含 meta 关键字(在 meta key 之前)
  • Unqualified Meta Expressions : 可以直接使用 meta key 而无需 meta 关键字

Meta expression types ,详细信息请参考 man nft

关键字 datatype 说明 示例
iif
oif
iface_index 出入网卡(接口)的 index 和 iifname 的区别
iifname
oifname
string 出入接口(网卡)名称 iifname eth0
iiftype
oiftype
iface_type 支持的类型包括: ether 、 ppp 、 ipip 、 ipip6 、 loopback 、 sit 、 ipgre
通过 nft describe iface_type 查看
ether lladdr
(Link layer address),( basetype integer )
数据链路层协议(MAC 地址) ether daddr 20:c9:d0:43:12:d9
ether saddr 20:c9:d0:43:12:d9
ip ipv4_addr
( basetype integer )
ipv4 地址 ip daddr 127.0.0.1
ip saddr 127.0.0.1
ip6 ipv6_addr
( basetype integer )
ipv6 地址
IPv6 地址后面带端口时,IPv6 地址要使用 [] ,如 [1ce::d0]:22 ,否则后面的端口会被当作 IPv6 地址的一部分
ip6 saddr ::1
ip6 daddr ::1
ip6 nat prerouting tcp dport 2222 dnat to [1ce::d0]:22
icmp icmp_type
( basetype integer )
ICMP 协议 icmp type { echo-request, echo-reply }
icmpv6 icmpv6_type
( basetype integer )
ICMP_v6 协议 icmpv6 type { echo-request, echo-reply }
length integer 数据包的长度(大小) ,单位为 bytes
nfproto integer 处理的数据包的实际的 Address Family,仅在 inet 类型的表中使用
l4proto integer 4 层协议,不包括 IPv6 的扩展头部
protocol ether_type EtherType 协议,包括(可选) ip 、 ip6 、 vlan 、 arp 、 8021q 、 8021ad
nft describe ether_type
ipsec boolean 数据是否由 ipsec 加密
time
day
hour
integer
integer
string
数据包的时间
iif 和 iifname 的异同

iif 、 oif 和 iifname 、 oifname 都是用来匹配网卡。不同之处在于:

  • iif 和 oif 是通过 interface index 来匹配网卡。使用此方式时, 网卡必须存在 ,并且在网卡名称变更后依然生效,这是因为在内部使用的是 interface index 而不是网卡名称
  • iifname 和 oifname 是通过网卡名称( interface name ) 类匹配网卡。使用此方式时, 网卡名称对应的网卡可以不存在 ,或者在网卡名称变更后不会在匹配到数据包,网卡名称改回来后或者同名的网卡出现后依旧可以匹配数据包。

iptables 规则转换为 nftables 规则

nftables 提供了工具 iptables-translate 用来将 iptables 规则转换为 nftables 规则

  • 转换单个规则

    # iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
    nft add rule ip filter INPUT tcp dport 22 ct state new counter accept

    # ip6tables-translate -A FORWARD -i eth0 -o eth3 -p udp -m multiport --dports 111,222 -j ACCEPT
    nft add rule ip6 filter FORWARD iifname "eth0" oifname "eth3" meta l4proto udp udp dport { 111,222} counter accept
  • 转换规则集

    # iptables-save > iptables_save.txt
    # cat iptables_save.txt
    # Generated by xtables-save v1.8.2 on Wed Feb 5 12:05:26 2020
    *filter
    :INPUT ACCEPT [0:0]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [0:0]
    -A INPUT -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
    COMMIT
    # Completed on Wed Feb 5 12:05:26 2020

    # iptables-restore-translate -f iptables_save.txt
    # Translated by iptables-restore-translate v1.8.2 on Wed Feb 5 12:05:38 2020
    add table ip filter
    add chain ip filter INPUT { type filter hook input priority 0; policy accept; }
    add chain ip filter FORWARD { type filter hook forward priority 0; policy accept; }
    add chain ip filter OUTPUT { type filter hook output priority 0; policy accept; }
    add rule ip filter INPUT tcp dport 22 ct state new counter accept
    # Completed on Wed Feb 5 12:05:38 2020

nftables 中规则的处理顺序

在 nftables 中,规则的处理顺序主要由以下几个因素共同决定: 表(table) 、 链(chain) 的 type 和 hook 、优先级( priority ) ,以及 规则的添加顺序 。

  • nftables 规则首先按表来组织。每个表包含一组链,链是由特定类型( type )和挂载点( hook )决定的。 表并不会直接影响规则的处理顺序,基本链( type hook )和优先级才是主要决定因素 。

  • 链的 type 和 hook 决定了数据包在网络堆栈中在哪个阶段处理,例如,流量在进入 input 链时,可能已经经过了 prerouting 链的处理。

    • type

      • filter : 用于过滤数据包(常见的过滤链类型)。
      • nat : 用于网络地址转换(NAT)。
      • route : 路由

      type 不决定顺序,只决定链的用途 。它定义了链的规则是处理 NAT 还是过滤数据包,但 链的实际处理顺序仍由优先级( priority )决定 。

    • hook

      • prerouting
      • input
      • forward
      • output
      • postrouting
  • 优先级( priority ), nftables 允许为每条链定义优先级, 优先级的值可以是正数或负数,数值越小,优先级越高 。

  • 规则的 处理顺序由添加顺序决定 。也就是说,先添加的规则会先处理。

  • 同一个挂载点上,不同表中,同一优先级的规则,其其执行顺序没有明确保证(可能取决于内核内部注册的顺序)

    比如存在以下表以及规则,由于两个链的 hook 和优先级都相同,其执行顺序没有明确保证:

    table ip filter { # handle 24
    chain INPUT { # handle 1
    type filter hook input priority filter; policy accept;
    counter packets 3412 bytes 823132 jump KUBE-FIREWALL # handle 6
    }

    table inet filter {
    chain INPUT { # handle 1
    type filter hook input priority filter; policy drop;
    }
    }

    因此, 最终数据包是否被允许 取决于:

    • 是否被跳转到 KUBE-FIREWALL 并在该链中得到放行
    • 以及两个链的执行顺序(由于两者优先级相同,顺序不确定)。

    为了避免因链顺序未定义而导致不确定的行为,建议显式设置不同的优先级。例如,如果希望在 table ip filter 的 INPUT 链先执行,可以修改其优先级低于(数值更小) table inet filter 的 INPUT 链

nft 命令用法

nft 命令的基本格式如下:
nft <OPERATOR> <TARGET> <EXPRESSIONS> <STATEMENTS>

OPERATOR 常用命令 :

OPERATOR 说明 示例
list 列出目标(TARGET)中的内容 nft list tables
nft list chains
nft list ruleset
add 添加目标
支持 table 、 chain 、 rule
默认如果不指定 handle 则规则添加到链的末尾
nft add table <TABLE_NAME>
nft add chain <CHAIN>
nft add rule <RULE>
nft add rule family table chain handle handle statement
create 和 add 相同,如果存在同名的规则,返回错误
rename 重命名链
delete 删除目标
支持 table 、 chain 、 rule
单个规则只能通过其 handle 删除
nft delete table <TABLE_NAME>
nft delete chain <CHAIN>
nft delete rule <RULE>
flush 清空目标
支持 table 、 chain 、 rule
nft flush tables <TABLE>
nft flush chain <ADD_FAMILIY> <TABLE> <CHAIN>

针对 rule 的其他 OPERATOR

OPERATOR 说明 示例
add 添加目标
支持 table 、 chain 、 rule
默认如果不指定 handle 则规则添加到链的末尾
nft add table <TABLE_NAME>
nft add chain <CHAIN>
nft add rule <RULE>
nft add rule family table chain handle handle statement
insert 插入规则
如果不指定 handle ,则规则插入到链的开头
nft insert rule family table chain handle handle statement
replace 替换指定的 rule

nft 命令常用选项,具体说明可以通过 nft -h 或者 man nft 查看

选项 说明 示例
-h, --help
-v, --version
显示帮助信息
显示版本信息
-c, --check 检查命令的有效性,而不实际应用更改
-f , --file
从指定文件读取配置 nft -f /etc/nftables.conf
-i, --interactive 交互模式,从终端读取输入
-j, --json 以 JSON 格式输出
-n, --numeric 指定一次后,以数字方式显示网络地址(默认行为)。指定两次以数字方式显示 Internet 服务(端口号)。指定三次以数字方式显示协议,用户 ID 和组 ID。
-N 将 IP 地址转换为名称(反向域名解析)
-a, --handle 显示规则句柄 handle
-e, --echo 输出 追加 , 插入 , 替换(更新) 的内容
-I, --includepath <directory> 添加 <directory> 目录到包含文件的搜索路径中。默认为: /etc
--debug <level [,level...]> 添加调试,在 level 处( scanner , parser , eval , netlink , mnl , proto-ctx , segtree , all )

常见配置示例

配置默认策略为 drop

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

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

在链中使用 policy drop; 配置默认策略为 drop ,如果不指定默认策略,默认为 accept 。链的默认策略相关说明

priority filter 表示优先级为 0,具体可参考 man nft

如果需要 修改已有链的默认策略 ,可以使用以下命令来更新。例如:

# nft chain ip filter INPUT { policy drop\; }

允许关联流量

在 nftables 中,允许关联流量( related traffic )进入是一种常见的防火墙配置。关联流量指的是与已经建立的连接相关的流量。例如,服务器的出站连接可能会产生响应数据包,这些响应属于已经建立的连接,或者是与原始连接有关的消息(如 ICMP 错误消息)。

如果不特意配置允许关联流量( related traffic )进入,则需要为每一个连接的出方向和入方向同时配置规则 ,假如本地向外发出的连接建立请求,也需要配置对应的允许响应入站,否则无法受到请求的响应,导致最终无法建立连接。

为了允许关联流量进入,通常需要使用 conntrack 模块来跟踪连接的状态。可以通过匹配连接的状态来允许 ESTABLISHED 和 RELATED 状态的流量。

  • conntrack 状态包括
    • ESTABLISHED :表示连接已经建立,允许流量在此连接上继续通信。
    • RELATED :表示与现有连接有关的流量,例如 ICMP 错误消息等。
    • NEW :表示新的连接请求。
    • INVALID :表示无法识别的连接状态,通常应丢弃。
# nft describe ct state
ct expression, datatype ct_state (conntrack state) (basetype bitmask, integer), 32 bits

pre-defined symbolic constants (in hexadecimal):
invalid 0x00000001
new 0x00000008
established 0x00000002
related 0x00000004
untracked 0x00000040

可以通过以下规则配置允许关联流量( ESTABLISHED 和 RELATED )进入,同时丢弃无效的流量:

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

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

# 允许已建立和关联的流量进入
ct state established,related accept;

# 丢弃无效连接状态的数据包
ct state invalid drop;

# 允许本地环回接口上的流量
iif "lo" accept;

# 允许 ICMP 请求
ip protocol icmp accept;

# 允许 SSH 连接(示例)
tcp dport 22 accept;
}

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

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

配置允许 ICMP 协议,及允许主机响应 ping

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ip protocol icmp accept; # allow all IPv4 ICMP
ip6 nexthdr icmpv6 accept; # allow all IPv6 ICMP

# 以下配置允许特定的 ICMP 类型
ip protocol icmp icmp type echo-request accept;
ip protocol icmp icmp type echo-reply accept;
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter;
}
}
  • ip protocol icmp 指定 ip 携带(处理)的是 ICMP 协议,支持的协议类型包括:

    # nft describe ip protocol
    payload expression, datatype inet_proto (Internet protocol) (basetype integer), 8 bits
    ip 0
    icmp 1
    igmp 2
    ggp 3
    ipencap 4
    st 5
    tcp 6
    egp 8
    igp 9
    pup 12
    udp 17
    hmp 20
    ...
  • 在指定特定的 ICMP 类型时,要先指定 ICMP 协议( ip protocol icmp ) 再指定 ICMP 类型( icmp type <TYPE> ),具体支持的 type 可以通过命令 nft describe icmp type 查看

  • 每个 ICMP type 需要单独配置

为规则启用计数器

在 nftables 中, 默认情况下规则不启用计数器(counters) 。如果你想为特定规则启用计数器来统计匹配的数据包数量和字节数,可以通过 counter 关键字来显式启用。

当添加规则时,只需在 规则末尾、终止语句( terminal statement )之前 添加 counter 关键字即可。这将为该规则统计所有匹配的数据包数和字节数

/etc/nftables.conf
ip protocol icmp icmp type echo-request accept counter;

meta l4proto tcp ip daddr 127.0.0.11 tcp dport 53 counter dnat to 127.0.0.11:45143;

添加默认规则处理数据包的计数器

nftables 不能直接在默认策略( policy )上启用计数器 。为了实现对默认策略处理的流量进行统计,可以通过添加一条 匹配 未匹配到其他规则 的所有流量的规则 并启用 counter 来实现相同的效果。

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

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

# 添加你想要的其他规则
ip protocol icmp accept;

# 添加一条显式的 drop 规则,并启用计数器
counter drop;
}
}

为规则添加注释

在 nftables 中,你可以为规则添加注释,以帮助描述规则的用途或功能。注释可以为规则提供可读性,尤其在复杂的规则集里,可以帮助系统管理员更容易地理解规则的目的。

nftables 提供了 comment 关键字来为规则添加注释 。

语法 : nft add rule <table> <chain> <match conditions> <action> comment "<your comment>"

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

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

# 添加你想要的其他规则
ip protocol icmp accept;

# 添加一条显式的 drop 规则,并启用计数器
counter drop comment "添加一条显式的 drop 规则,并启用计数器";
}
}

在 nftables 中 直接修改规则的注释是不可能的 。如果需要更改注释,可以删除旧规则并重新添加带有更新注释的新规则。

在 cmd 交互命令中添加注释时, comment 后面的内容要使用 双引号(") ,并且 双引号(") 需要 转义( \ ) ,否则命令会报错

# nft insert rule inet filter input handle 11 tcp dport { 80,443 } counter accept comment \"for nginx\"

记录日志消息

在定位防火墙规则相关问题的过程中,经常需要将经过防火墙的数据报文信息记录到日志中,以便定位问题。 nftables 提供了 log 表达式来记录日志,日志信息默认写入到 kernel log (通常是 dmesg 或者 syslog ),如 Ubuntu 系统一般在 /var/log/kern.log 中。具体配置可以参考 man nft

以下示例将所有经过 log 所在规则的数据报文信息写入日志

nft add rule ip filter INPUT log prefix "nftables-log"

以下示例指定日志级别

nft replace rule ip filter INPUT handle 38 log prefix "nftables-log"  level debug

查看日志信息

# tail /var/log/kern.log
Sep 16 15:29:47 U-3TSDMAL9IVFAQ kernel: [2870115.965957] nftables-logIN=eth0 OUT= MAC=06:93:c0:af:74:fb:06:8e:4c:95:63:0a:08:00 SRC=10.17.183.12 DST=198.19.78.194 LEN=62 TOS=0x00 PREC=0x00 TTL=255 ID=0 DF PROTO=UDP SPT=55617 DPT=8220 LEN=42
Sep 16 15:29:47 U-3TSDMAL9IVFAQ kernel: [2870115.966114] nftables-logIN=eth0 OUT= MAC=06:93:c0:af:74:fb:06:8e:4c:95:63:0a:08:00 SRC=10.17.183.12 DST=198.19.78.194 LEN=62 TOS=0x00 PREC=0x00 TTL=255 ID=0 DF PROTO=UDP SPT=55617 DPT=8220 LEN=42
Sep 16 15:29:48 U-3TSDMAL9IVFAQ kernel: [2870116.003624] nftables-logIN=eth0 OUT= MAC=06:93:c0:af:74:fb:06:8e:4c:95:63:0a:08:00 SRC=10.17.183.12 DST=198.19.78.194 LEN=85 TOS=0x00 PREC=0x00 TTL=255 ID=0 DF PROTO=UDP SPT=55617 DPT=8220 LEN=65
Sep 16 15:29:48 U-3TSDMAL9IVFAQ kernel: [2870116.003737] nftables-logIN=eth0 OUT= MAC=06:93:c0:af:74:fb:06:8e:4c:95:63:0a:08:00 SRC=10.17.183.12 DST=198.19.78.194 LEN=85 TOS=0x00 PREC=0x00 TTL=255 ID=0 DF PROTO=UDP SPT=55617 DPT=8220 LEN=65
Sep 16 15:29:48 U-3TSDMAL9IVFAQ kernel: [2870116.029536] nftables-logIN=eth0 OUT= MAC=06:93:c0:af:74:fb:06:8e:4c:95:63:0a:08:00 SRC=10.17.183.12 DST=198.19.78.194 LEN=85 TOS=0x00 PREC=0x00 TTL=255 ID=0 DF PROTO=UDP SPT=55617 DPT=8220 LEN=65

替换规则

假如有以下规则,替换 handle 38 的规则

# nft -a list ruleset
table ip filter { # handle 2
chain INPUT { # handle 1
type filter hook input priority filter; policy accept;
log prefix "nftables-log" # handle 38
}
}

nft replace rule ip filter INPUT handle 38 log prefix "nftables-log" level debug

删除规则

删除所有规则

nft flush ruleset

删除指定的规则:

nft delete rule inet filter input handle 16

限制 IP 和 端口

使用关键字 ip saddr 和 ip daddr 分别对 源 IP( source IP Address ) 和 目标 IP(Destination IP Address) 进行过滤

tcp sport 和 tcp dport 对 TCP 源端口(Source Port) 和 TCP 目标端口(Destination Port) 做过滤

以下示例放通指定源 IP 和 目标端口的数据包

ip saddr 172.27.0.2 tcp dport 22 accept comment "allow ssh from 172.27.0.2"

同时指定多个目标,使用 集合( {} )的语法 来指定多个目标。

nft insert rule inet filter input handle 11 ip saddr { 172.27.0.2,172.27.0.4 } tcp dport { 80,443 } counter accept comment \"for nginx\"

指定连续的端口,使用 集合格式 { 6379, 7380-7382 } 指定多个连续端口

# nft insert rule inet filter input handle 11 ip saddr { 172.27.0.2,172.27.0.4 } tcp dport { 6379,7380-7383 } counter accept comment \"for redis\"

多个目标 IP 的配置方法

nftables 中 支持定义连续的 IP 地址范围 ,如 192.168.1.1-192.168.1.10 ,也支持使用 CIDR 格式( 192.168.1.0/24 ),如果连续的 IP 地址不在 CIDR 范围内,则需要使用 集合( set )格式 : { 172.27.0.2,172.27.0.4 } 将所有的 IP 地址列入 。也可以使用如 { 192.168.1.1-192.168.1.10, 10.0.0.0/8 }

假如需要配置的 IP 较多或者分散,不足以使用 CIDR 表示,可以考虑使用 命名的集合 ( set ) 先定义目标 IP 地址,然后在 规则中引用命名集合 ,以下示例演示 set 基本用法, SET STATEMENT 详细说明请参考 man nft

  1. 创建命名集合,在指定的表( table )中创建 命名的集合( set )

    # nft add set inet filter allowed_ips { type ipv4_addr\; elements = { 192.168.1.1-192.168.1.3, 192.168.2.0/24, 192.168.3.3 } }
    • 如果集合中有 CIDR 格式的 IP 地址,创建 set 时需要使用 flags interval 选项 ,否则会报错: You must add 'flags interval' to your set declaration if you want to add prefix elements

      nft add set inet filter ssh_allowed_ips { type ipv4_addr \; flags interval \; }
      nft add element inet filter ssh_allowed_ips { 192.168.1.0/24, 192.168.2.0/24 }
  2. 引用 命名的集合( set )

    # nft insert rule inet filter input handle 11 ip saddr @allowed_ips tcp dport { 80, 443 } counter accept comment \"for nginx\"
  3. 查看 规则集( ruleset )

    # nft -a list ruleset
    table inet filter { # handle 14
    set allowed_ips { # handle 28
    type ipv4_addr
    elements = { 192.168.1.1, 192.168.1.2,
    192.168.1.3 }
    }

    chain input { # handle 1
    type filter hook input priority filter; policy drop;

    ip protocol icmp icmp type echo-reply accept # handle 4
    ip protocol icmp icmp type echo-request accept # handle 5

    ct state established,related accept # handle 6
    ct state invalid drop # handle 7
    iif "lo" accept # handle 8


    ip saddr @allowed_ips tcp dport { 80, 443 } counter packets 0 bytes 0 accept comment "for nginx" # handle 30

    counter packets 20 bytes 1200 drop # handle 11
    }
    }
  4. 更新 命名的集合( set ) 中的元素

    # nft -a list set inet filter allowed_ips
    table inet filter {
    set allowed_ips { # handle 28
    type ipv4_addr
    elements = { 192.168.1.1, 192.168.1.2,
    192.168.1.3 }
    }
    }

    ## 添加元素
    # nft add element inet filter allowed_ips { 192.168.1.4, 192.168.1.5 }

    # nft -a list set inet filter allowed_ips
    table inet filter {
    set allowed_ips { # handle 28
    type ipv4_addr
    elements = { 192.168.1.1, 192.168.1.2,
    192.168.1.3, 192.168.1.4,
    192.168.1.5 }
    }
    }

    ## 删除元素
    # nft delete element inet filter allowed_ips { 192.168.1.1 }

    # nft -a list set inet filter allowed_ips
    table inet filter {
    set allowed_ips { # handle 28
    type ipv4_addr
    elements = { 192.168.1.2, 192.168.1.3,
    192.168.1.4, 192.168.1.5 }
    }
    }

以上方法相当于 iptables 中配合使用 ipset

NAT

以下示例测试 DNAT,当请求端口为 8000 时,在 output 钩子上( type nat hook output priority dstnat; policy accept; )对其进行 DNAT,转为 172.27.0.3:80

   chain OUTPUT { # handle 2
type nat hook output priority dstnat; policy accept;
tcp dport 8000 counter packets 1 bytes 60 dnat to 172.27.0.3:80 # handle 11
}

172.27.0.3:80 上监听了 Nginx HTTP ( 80 端口),通过 curl -Iv 172.27.0.3:8000 测试,可以正常请求到 Nginx 上面的内容

# curl -Iv 172.27.0.3:8000

* IPv4: 172.27.0.3
* Trying 172.27.0.3:8000...
* Connected to (172.27.0.3 port 8000
> HEAD / HTTP/1.1
> Host: target1:8000
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: nginx/1.24.0 (Ubuntu)
Server: nginx/1.24.0 (Ubuntu)
< Date: Tue, 17 Sep 2024 08:01:29 GMT
Date: Tue, 17 Sep 2024 08:01:29 GMT
< Content-Type: text/html
Content-Type: text/html

为什么不需要 SNAT?

本示例中假设本机为 T2,目标 Nginx 服务器为 T1,数据包的流向分析如下:

  1. 初始请求

    在 T2 上执行 curl -Iv T1:8000 ,T2 发出数据包,源地址为 t2:10000 (假设源端口为 10000 ),目标地址为 t1:8000 ,数据报文流向:

    t2:10000 -> t1:8000

  2. 数据包流经网络协议栈, nftables 的 output 钩子对数据包进行了 DNAT,数据报文流向变为:

    t2:10000 -> t1:80

  3. 数据报文经路由后到达 T1,并被监听在 80 端口上的 Nginx 服务处理,Nginx 返回响应,响应包的源和目标地址为

    t1:80 -> t2:10000

  4. T1 上响应报文( t1:80 -> t2:10000 ) 经路由后返回 T2。此时,数据包直接进入 T2 的本地网络堆栈。并被 curl 进程接收。

连接跟踪( conntrack )机制

在 T2 上,当 nftables 执行 DNAT 改写目标端口时,Linux 内核的连接跟踪( conntrack )模块会记录下这一连接的状态,跟踪这个 NAT 连接。当返回数据包到达 T2 时, 内核会根据连接跟踪表自动识别出该数据包属于一个经过 DNAT 的连接,并自动对返回数据包执行逆向的地址转换(即将 t1:80 识别为 t1:8000 )

nftables 的 DNAT 改写了出站连接的目标地址(端口 8000 被改写为 80 ),但返回时依赖 连接跟踪( conntrack ) 机制来追踪这个连接,因此不需要显式的 SNAT。

多层级配置管理

在 Docker 和 Kubernetes 的场景中,如果修改 nftables 防火墙配置后,重启 nftables 可能会导致 Docker 创建的规则失效,重启 nftables 后需要重启 Docker 使 Docker 重新生成防火墙规则。这不利于线上业务的稳定性。

在这种场景下, 尽量用运行时增量更新,不要用 flush ruleset 清空整套规则,也不要重启 nftables 服务,应该仅变更自有表 。

因此推荐只在主配置文件 /etc/nftables.conf 中写好主框架,具体的规则写入其他目录,用 include 命令包含子目录,在子目录中修改具体规则,修改后不重启 nftables 服务,而是更新子配置文件

nft --check -f /etc/nftables.d/myfilter.nft
nft -f /etc/nftables.d/myfilter.nft

参考主配置文件 /etc/nftables.conf ,在主配置文件中定义基础框架。

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
set sshd_allow_ips { # 声明一个专门用于 SSH 白名单的集合, elements 留空,具体 IP 由外部文件动态注入
type ipv4_addr;
flags interval;

}

chain input {
type filter hook input priority 10; policy drop; # 可以将 priority 改大些作为最后生效的规则,用以兜底
ct state established,related accept;
ct state invalid drop;
iif "lo" accept;
ip protocol icmp accept;
ip saddr @sshd_allow_ips tcp dport 22 accept comment "for sshd"; # 引用集合 sshd_allow_ips
tcp dport 2222 accept comment "for jumpserver";
ip saddr 192.168.0.0/16 accept comment "for docker network";
ip saddr 172.17.0.1/16 accept comment "for docker network";
counter drop;
}
chain forward {
type filter hook forward priority 0;
}
chain output {
type filter hook output priority 0;
}
}

# 引入子配置
include "/etc/nftables.d/*.nft"

子配置文件 /etc/nftables.d/sshd_allow_ips.nft 示例

/etc/nftables.d/sshd_allow_ips.nft
# 1. 仅清空特定的集合(绝对不会影响其他规则和 Docker 规则)中的 elements,不会清除该集合的定义属性(type、flags 等)
flush set inet filter sshd_allow_ips

# 2. 向集合中注入最新的 IP 列表
add element inet filter sshd_allow_ips {
ip1,
ip2,
...
}

每次你修改完 /etc/nftables.d/sshd_allow_ips.nft 后, 不需要重启 nftables 服务 ,只需要在终端直接执行命令即可 :

nft -c -f /etc/nftables.d/sshd_allow_ips.nft

nft -f /etc/nftables.d/sshd_allow_ips.nft
  • 零中断 : nft -f 加载更新集合是原子操作,瞬间生效,已建立的连接不会断开。
  • 不影响 Docker : flush set 仅仅清空了 sshd_allow_ips 这一个变量里的内容。Docker 在 ip filter 和 ip nat 表里维护的规则(如端口映射、DOCKER 链等)完全不会被触碰。
  • 极简维护 :以后维护人员只需要改那个纯 IP 列表的文件,不用担心误改了核心的规则链逻辑。

如果使用了 Docker Compose,要为其中的服务配置准入防火墙,可以参考以下主配置:

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
set sshd_allow_ips { # 声明一个专门用于 SSH 白名单的集合, elements 留空,具体 IP 由外部文件动态注入
type ipv4_addr;
flags interval;

}

chain input {
type filter hook input priority 10; policy drop; # 可以将 priority 改大些作为最后生效的规则,用以兜底
ct state established,related accept;
ct state invalid drop;
iif "lo" accept;
ip protocol icmp accept;
ip saddr @sshd_allow_ips tcp dport 22 accept comment "for sshd"; # 引用集合 sshd_allow_ips
tcp dport 2222 accept comment "for jumpserver";
ip saddr 192.168.0.0/16 accept comment "for docker network";
ip saddr 172.17.0.1/16 accept comment "for docker network";
counter drop;
}
chain forward {
type filter hook forward priority 0;
}
chain output {
type filter hook output priority 0;
}
}

# For Docker
table ip filter {
set docker_compose_srv_allow_ips { # 定义一个空的集合,用于对 docker compose 中的服务添加白名单
type ipv4_addr;
flags interval;
}

chain DOCKER-USER {
# 引用集合创建白名单,如果 源 IP 不在白名单中,目的端口是 80,则 drop(拒绝访问),只有在白名单中的请求才被允许
ip saddr != @docker_compose_srv_allow_ips tcp dport 80 counter drop comment "for Jumpserver";
}
}


# 引入子配置
include "/etc/nftables.d/*.nft"

在子配置 docker_compose_srv_allow_ips.nft 中动态修改白名单

docker_compose_srv_allow_ips.nft
# 1. 仅清空特定的集合(绝对不会影响其他规则和 Docker 规则)中的 elements,不会清除该集合的定义属性(type、flags 等)
flush set ip filter docker_compose_srv_allow_ips

# 2. 向集合中注入最新的 IP 列表
add element ip filter docker_compose_srv_allow_ips {
ip1,
ip2,
...
}

执行以下命令生效

nft -c -f /etc/nftables.d/docker_compose_srv_allow_ips.nft

nft -f /etc/nftables.d/docker_compose_srv_allow_ips.nft