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 配置构成组件

nftables Tables

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

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

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

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

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

nftables Chains

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

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

这意味着与 iptables 不同,如果链没有使用 netfilter 框架中的任何类型或钩子,则流经这些链的数据包不会被 nftables 触及(处理) 。数据包要被实际处理,需要使用 链(Chains) 中的规则捕获包(使用具体的类型( filternatroute )和钩子( 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(如 inputoutputprerouting 等)关联的基本链才能有默认策略。自定义的非基本链必须显式定义处理规则。
  • 优先级 :当链有多个规则时,数据包首先会根据链中的规则进行匹配。如果没有规则匹配,默认策略才会生效。因此,如果链中存在明确的 acceptdrop 规则,默认策略只会在所有规则都没有匹配时起作用。

nftables Rules

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

通常情况下,EXPRESSION 包含一些要匹配的表达式,然后是判断语句。结论语句包括 acceptdropqueuecontinuereturnjump chaingoto 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_addrinteger 组成

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 支持的类型包括:etherpppipipipip6loopbacksitipgre
通过 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 协议,包括(可选) ipip6vlanarp8021q8021ad
nft describe ether_type
ipsec boolean 数据是否由 ipsec 加密
time
day
hour
integer
integer
string
数据包的时间
iif 和 iifname 的异同

iifoifiifnameoifname 都是用来匹配网卡。不同之处在于:

  • iifoif 是通过 interface index 来匹配网卡。使用此方式时,网卡必须存在,并且在网卡名称变更后依然生效,这是因为在内部使用的是 interface index 而不是网卡名称
  • iifnameoifname 是通过网卡名称(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)typehook、优先级(priority ,以及 规则的添加顺序

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

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

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

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

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

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

nft 命令用法

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

OPERATOR 常用命令

OPERATOR 说明 示例
list 列出目标(TARGET)中的内容 nft list tables
nft list chains
nft list ruleset
add 添加目标
支持 tablechainrule
默认如果不指定 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 删除目标
支持 tablechainrule
单个规则只能通过其 handle 删除
nft delete table <TABLE_NAME>
nft delete chain <CHAIN>
nft delete rule <RULE>
flush 清空目标
支持 tablechainrule
nft flush tables <TABLE>
nft flush chain <ADD_FAMILIY> <TABLE> <CHAIN>

针对 rule 的其他 OPERATOR

OPERATOR 说明 示例
add 添加目标
支持 tablechainrule
默认如果不指定 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 模块来跟踪连接的状态。可以通过匹配连接的状态来允许 ESTABLISHEDRELATED 状态的流量。

  • 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

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

/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 saddrip daddr 分别对 源 IP(source IP Address目标 IP(Destination IP Address) 进行过滤

tcp sporttcp dportTCP 源端口(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 地址列入

假如需要配置的 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.2, 192.168.1.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. 数据包流经网络协议栈,nftablesoutput 钩子对数据包进行了 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。