nftables 是一个 netfilter
项目,旨在替换现有的 {ip,ip6,arp,eb}tables 框架,为 {ip,ip6}tables 提供一个新的包过滤框架、一个新的用户空间实用程序(nft)和一个兼容层。它使用现有的钩子、链接跟踪系统、用户空间排队组件和 netfilter
日志子系统。
nftables 主要由三个组件组成:内核实现、libnl netlink 通信和 nftables 用户空间。 其中内核提供了一个 netlink
配置接口以及运行时规则集评估,libnl
包含了与内核通信的基本函数,用户空间可以通过 nft
和用户进行交互。
安装 nftables
首先需要安装 nftables:
$ yum install -y nftables
由于 nftables 默认没有内置的链,但提供了一些示例配置,我们可以将其 include 到主配置文件中。主配置文件为 /etc/sysconfig/nftables.conf
,将下面一行内容取消注释:
# include "/etc/nftables/inet-filter"
然后启动 nftables 服务:
$ systemctl start nftables
现在再次查看规则,就会发现多了一张 filter
表和几条链:
$ nft list ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
}
chain forward {
type filter hook forward priority 0; policy accept;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
在 nftables 中,ipv4
和 ipv6
协议可以被合并到一个单一的地址簇 inet
中,使用了 inet 地址簇,就不需要分别为 ipv4 和 ipv6 指定两个不同的规则了。
nftables VS iptables
nftables 和 iptables 一样,由表(table)、链(chain)和规则(rule)组成,其中表包含链,链包含规则,规则是真正的 action。与 iptables 相比,nftables 主要有以下几个变化:
iptables
规则的布局是基于连续的大块内存的,即数组式布局;而nftables
的规则采用链式布局。其实就是数组和链表的区别,好像 Kubernetes 用户对此应该很兴奋?iptables
大部分工作在内核态完成,如果要添加新功能,只能重新编译内核;而nftables
的大部分工作是在用户态完成的,添加新功能很 easy,不需要改内核。iptables
有内置的链,即使你只需要一条链,其他的链也会跟着注册;而nftables
不存在内置的链,你可以按需注册。由于iptables
内置了一个数据包计数器,所以即使这些内置的链是空的,也会带来性能损耗。简化了
IPv4/IPv6
双栈管理。原生支持集合、字典和映射。
回到 nftables,先来看一下默认的规则集是啥:
$ nft list ruleset
啥也没有,果然是没有内置的链啊(如果你关闭了 firewalld
服务)。
操作说明
创建表
nftables 的每个表只有一个地址簇,并且只适用于该簇的数据包。表可以指定五个簇中的一个:
inet
同时适用于 IPv4 和 IPv6 的数据包,即统一了 ip
和 ip6
簇,可以更容易地定义规则,下文的示例都将采用 inet 簇。
先创建一个新的表:
$ nft add table inet my_table
列出所有的规则:
$ nft list ruleset
table inet my_table {
}
现在表中还没有任何规则,需要创建一个链来保存规则。
创建链
链是用来保存规则的,和表一样,链也需要被显示创建,因为 nftables 没有内置的链。链有以下两种类型:
常规链 : 不需要指定钩子类型和优先级,可以用来做跳转,从逻辑上对规则进行分类。
基本链 : 数据包的入口点,需要指定钩子类型和优先级。
创建常规链:
$ nft add chain inet my_table my_utility_chain
创建基本链:
$ nft add chain inet my_table my_filter_chain { type filter hook input priority 0 \; }
反斜线(
\
)用来转义,这样 shell 就不会将分号解释为命令的结尾。priority
采用整数值,可以是负数,值较小的链优先处理。
列出链中的所有规则:
$ nft list chain inet my_table my_utility_chain
table inet my_table {
chain my_utility_chain {
}
}
$ nft list chain inet my_table my_filter_chain
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
}
}
创建规则
有了表和链之后,就可以创建规则了,规则由语句或表达式构成,包含在链中。下面添加一条规则允许 SSH 登录:
$ nft add rule inet my_table my_filter_chain tcp dport ssh accept
add
表示将规则添加到链的末尾,如果想将规则添加到链的开头,可以使用 insert
。
$ nft insert rule inet my_table my_filter_chain tcp dport http accept
列出所有规则:
$ nft list ruleset
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
tcp dport http accept
tcp dport ssh accept
}
}
注意 http 规则排在 ssh 规则的前面,因为之前使用了 insert
。
也可以将规则插入到链的指定位置,有两种方法:
1、 使用 index
来指定规则的索引。add
表示新规则添加在索引位置的规则后面,inser
表示新规则添加在索引位置的规则前面。index 的值从 0 开始增加。
$ nft insert rule inet my_table my_filter_chain index 1 tcp dport nfs accept
$ nft list ruleset
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
tcp dport http accept
tcp dport nfs accept
tcp dport ssh accept
}
}
$ nft add rule inet my_table my_filter_chain index 0 tcp dport 1234 accept
$ nft list ruleset
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
tcp dport http accept
tcp dport 1234 accept
tcp dport nfs accept
tcp dport ssh accept
}
}
index
类似于 iptables 的 -I
选项,但有两点需要注意:一是 index 的值是从 0 开始的;二是 index 必须指向一个存在的规则,比如 nft insert rule … index 0
就是非法的。
2、 使用 handle
来指定规则的句柄。add
表示新规则添加在索引位置的规则后面,inser
表示新规则添加在索引位置的规则前面。handle
的值可以通过参数 --handle
获取。
$ nft --handle list ruleset
table inet my_table { # handle 10
chain my_filter_chain { # handle 2
type filter hook input priority 0; policy accept;
tcp dport http accept # handle 4
tcp dport 1234 accept # handle 6
tcp dport nfs accept # handle 5
tcp dport ssh accept # handle 3
}
}
$ nft add rule inet my_table my_filter_chain handle 4 tcp dport 1234 accept
$ nft insert rule inet my_table my_filter_chain handle 5 tcp dport nfs accept
$ nft --handle list ruleset
table inet my_table { # handle 10
chain my_filter_chain { # handle 2
type filter hook input priority 0; policy accept;
tcp dport http accept # handle 4
tcp dport 2345 accept # handle 8
tcp dport 1234 accept # handle 6
tcp dport 3456 accept # handle 9
tcp dport nfs accept # handle 5
tcp dport ssh accept # handle 3
}
}
在 nftables 中,句柄值是固定不变的,除非规则被删除,这就为规则提供了稳定的索引。而 index
的值是可变的,只要有新规则插入,就有可能发生变化。一般建议使用 handle
来插入新规则。
也可以在创建规则时就获取到规则的句柄值,只需要在创建规则时同时加上参数 --echo
和 --handle
。
$ nft --echo --handle add rule inet my_table my_filter_chain udp dport 3333 accept
add rule inet my_table my_filter_chain udp dport 3333 accept # handle 10
删除规则
单个规则只能通过其句柄删除,首先需要找到你想删除的规则句柄:
$ nft --handle list ruleset
table inet my_table { # handle 10
chain my_filter_chain { # handle 2
type filter hook input priority 0; policy accept;
tcp dport http accept # handle 4
tcp dport 2345 accept # handle 8
tcp dport 1234 accept # handle 6
tcp dport 3456 accept # handle 9
tcp dport nfs accept # handle 5
tcp dport ssh accept # handle 3
udp dport 3333 accept # handle 10
}
}
然后使用句柄值来删除该规则:
$ nft delete rule inet my_table my_filter_chain handle 8
$ nft --handle list ruleset
table inet my_table { # handle 10
chain my_filter_chain { # handle 2
type filter hook input priority 0; policy accept;
tcp dport http accept # handle 4
tcp dport 1234 accept # handle 6
tcp dport 3456 accept # handle 9
tcp dport nfs accept # handle 5
tcp dport ssh accept # handle 3
udp dport 3333 accept # handle 10
}
}
列出规则
前面的示例都是列出了所有规则,我们还可以根据自己的需求列出规则的一部分。例如:
列出某个表中的所有规则:
$ nft list table inet my_table
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
tcp dport http accept
tcp dport 1234 accept
tcp dport 3456 accept
tcp dport nfs accept
tcp dport ssh accept
udp dport 3333 accept
}
}
列出某条链中的所有规则:
$ nft list chain inet my_table my_other_chain
table inet my_table {
chain my_other_chain {
udp dport 12345 log prefix "UDP-12345"
}
}
集合
nftables
的语法原生支持集合,可以用来匹配多个 IP 地址、端口号、网卡或其他任何条件。
匿名集合
集合分为匿名集合与命名集合,匿名集合比较适合用于将来不需要更改的规则。
例如,下面的规则允许来自源 IP 处于 10.10.10.123 ~ 10.10.10.231
这个区间内的主机的流量。
$ nft add rule inet my_table my_filter_chain ip saddr { 10.10.10.123, 10.10.10.231 } accept
$ nft list ruleset
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
tcp dport http accept
tcp dport nfs accept
tcp dport ssh accept
ip saddr { 10.10.10.123, 10.10.10.231 } accept
}
}
匿名集合的缺点是,如果需要修改集合,就得替换规则。如果后面需要频繁修改集合,推荐使用命名集合。
之前的示例中添加的规则也可以通过集合来简化:
$ nft add rule inet my_table my_filter_chain tcp dport { http, nfs, ssh } accept
iptables 可以借助
ipset
来使用集合,而 nftables 原生支持集合,所以不需要借助ipset
。
命名集合
nftables 也支持命名集合,命名集合是可以修改的。创建集合需要指定其元素的类型,当前支持的数据类型有:
ipv4_addr
: IPv4 地址ipv6_addr
: IPv6 地址ether_addr
: 以太网(Ethernet)地址inet_proto
: 网络协议inet_service
: 网络服务mark
: 标记类型
先创建一个空的命名集合:
$ nft add set inet my_table my_set { type ipv4_addr \; }
$ nft list sets
table inet my_table {
set my_set {
type ipv4_addr
}
}
要想在添加规则时引用集合,可以使用 @
符号跟上集合的名字。下面的规则表示将集合 my_set
中的 IP 地址添加到黑名单中。
$ nft insert rule inet my_table my_filter_chain ip saddr @my_set drop
$ nft list chain inet my_table my_filter_chain
table inet my_table {
chain my_filter_chain {
type filter hook input priority 0; policy accept;
ip saddr @my_set drop
tcp dport http accept
tcp dport nfs accept
tcp dport ssh accept
ip saddr { 10.10.10.123, 10.10.10.231 } accept
}
}
向集合中添加元素:
$ nft add element inet my_table my_set { 10.10.10.22, 10.10.10.33 }
$ nft list set inet my_table my_set
table inet my_table {
set my_set {
type ipv4_addr
elements = { 10.10.10.22, 10.10.10.33 }
}
}
如果你向集合中添加一个区间就会报错:
$ nft add element inet my_table my_set { 10.20.20.0-10.20.20.255 }
Error: Set member cannot be range, missing interval flag on declaration
add element inet my_table my_set { 10.20.20.0-10.20.20.255 }
^^^^^^^^^^^^^^^^^^^^^^^
要想在集合中使用区间,需要加上一个 flag interval
,因为内核必须提前确认该集合存储的数据类型,以便采用适当的数据结构。
支持区间
创建一个支持区间的命名集合:
$ nft add set inet my_table my_range_set { type ipv4_addr \; flags interval
$ nft add element inet my_table my_range_set { 10.20.20.0/24 }
$ nft list set inet my_table my_range_set
table inet my_table {
set my_range_set {
type ipv4_addr
flags interval
elements = { 10.20.20.0/24 }
}
}
子网掩码表示法会被隐式转换为 IP 地址的区间,你也可以直接使用区间
10.20.20.0-10.20.20.255
来获得相同的效果。
级联不同类型
命名集合也支持对不同类型的元素进行级联,通过级联操作符 .
来分隔。例如,下面的规则可以一次性匹配 IP 地址、协议和端口号。
$ nft add set inet my_table my_concat_set { type ipv4_addr . inet_proto . inet_service \; }
$ nft list set inet my_table my_concat_set
table inet my_table {
set my_concat_set {
type ipv4_addr . inet_proto . inet_service
}
}
向集合中添加元素:
$ nft add element inet my_table my_concat_set { 10.30.30.30 . tcp . telnet }
在规则中引用级联类型的集合和之前一样,但需要标明集合中每个元素对应到规则中的哪个位置。
$ nft add rule inet my_table my_filter_chain ip saddr . meta l4proto . tcp dport @my_concat_set accept
这就表示如果数据包的源 IP、协议类型、目标端口匹配 10.30.30.30、tcp、telnet
时,nftables 就会允许该数据包通过。
匿名集合也可以使用级联元素,例如:
$ nft add rule inet my_table my_filter_chain ip saddr . meta l4proto . udp dport { 10.30.30.30 . udp . bootps } accept
现在你应该能体会到 nftables 集合的强大之处了吧。
nftables 级联类型的集合类似于 ipset 的聚合类型,例如
hash:ip,port
。
字典
字典是 nftables 的一个高级特性,它可以使用不同类型的数据并将匹配条件映射到某一个规则上面,并且由于是哈希映射的方式,可以完美的避免链式规则跳转的性能开销。
例如,为了从逻辑上将对 TCP 和 UDP 数据包的处理规则拆分开来,可以使用字典来实现,这样就可以通过一条规则实现上述需求。
$ nft add chain inet my_table my_tcp_chain
$ nft add chain inet my_table my_udp_chain
$ nft add rule inet my_table my_filter_chain meta l4proto vmap { tcp : jump my_tcp_chain, udp : jump my_udp_chain }
$ nft list chain inet my_table my_filter_chain
table inet my_table {
chain my_filter_chain {
...
meta nfproto ipv4 ip saddr . meta l4proto . udp dport { 10.30.30.30 . udp . bootps } accept
meta l4proto vmap { tcp : jump my_tcp_chain, udp : jump my_udp_chain }
}
}
和集合一样,除了匿名字典之外,还可以创建命名字典:
$ nft add map inet my_table my_vmap { type inet_proto : verdict \; }
向字典中添加元素:
$ nft add element inet my_table my_vmap { 192.168.0.10 : drop, 192.168.0.11 : accept }
后面就可以在规则中引用字典中的元素了:
$ nft add rule inet my_table my_filter_chain ip saddr vmap @my_vmap
表与命名空间
在 nftables 中,每个表都是一个独立的命名空间,这就意味着不同的表中的链、集合、字典等都可以有相同的名字。例如:
$ nft add table inet table_one
$ nft add chain inet table_one my_chain
$ nft add table inet table_two
$ nft add chain inet table_two my_chain
$ nft list ruleset
...
table inet table_one {
chain my_chain {
}
}
table inet table_two {
chain my_chain {
}
}
有了这个特性,不同的应用就可以在相互不影响的情况下管理自己的表中的规则,而使用 iptables
就无法做到这一点。
当然,这个特性也有缺陷,由于每个表都被视为独立的防火墙,那么某个数据包必须被所有表中的规则放行,才算真正的放行,即使 table_one
允许该数据包通过,该数据包仍然有可能被 table_two
拒绝。为了解决这个问题,nftables 引入了优先级,priority
值越高的链优先级越低,所以 priority
值低的链比 priority
值高的链先执行。如果两条链的优先级相同,就会进入竞争状态。
备份与恢复
以上所有示例中的规则都是临时的,要想永久生效,我们可以将规则备份,重启后自动加载恢复,其实 nftables 的 systemd
服务就是这么工作的。
备份规则:
$ nft list ruleset > /root/nftables.conf
加载恢复:
$ nft -f /root/nftables.conf
在 CentOS 8 中,nftables.service
的规则被存储在 /etc/nftables.conf
中,其中 include 一些其他的示例规则,一般位于 /etc/sysconfig/nftables.conf
文件中,但默认会被注释掉。
防火墙设置
添加 INPUT 规则
和 iptables 一样,nftables
的 filter 表包含三条链:INPUT
、FORWARD
和 OUTPUT
,一般配置防火墙只需要配置 INPUT
链就好了。
回环接口
首先允许访问 localhost:
$ nft add rule inet filter input iif "lo" accept
$ nft add rule inet filter input iif != "lo" ip daddr 127.0.0.0/8 drop
可以再优化一下,加上注解(comment)和计数器(counter):
$ nft add rule inet filter input \
iif "lo" \
accept \
comment \"Accept any localhost traffic\"
$ nft add rule inet filter input \
iif != "lo" ip daddr 127.0.0.0/8 \
counter \
drop \
comment \"drop connections to loopback not coming from loopback\"
查看规则:
$ nft list chain inet filter input
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
iif "lo" accept comment "Accept any localhost traffic"
iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback"
}
}
连接跟踪模块
接下来的规则用到一个内核模块叫 conntrack(connection tracking)
,它被用来跟踪一个连接的状态。最常见的使用场景是 NAT
,为什么需要跟踪记录连接的状态呢?因为 nftables 需要记住数据包的目标地址被改成了什么,并且在返回数据包时再将目标地址改回来。
和 iptables 一样,一个 TCP 连接在 nftables 中总共有四种状态:NEW
,ESTABLISHED
,RELATED
和 INVALID
。
除了本地产生的包由 OUTPUT
链处理外,所有连接跟踪都是在 PREROUTING
链里进行处理的,意思就是, iptables 会在 PREROUTING
链里从新计算所有的状态。如果我们发送一个流的初始化包,状态就会在 OUTPUT
链里被设置为 NEW
,当我们收到回应的包时,状态就会在 PREROUTING 链里被设置为 ESTABLISHED
。如果收到回应的第一个包不是本地产生的,那就会在 PREROUTING 链里被设置为 NEW
状态。综上,所有状态的改变和计算都是在 nat 表中的 PREROUTING
链和 OUTPUT
链里完成的。
还有其他两种状态:
RELATED
: RELATED 状态有点复杂,当一个连接与另一个已经是ESTABLISHED
的连接有关时,这个连接就被认为是 RELATED。这意味着,一个连接要想成为 RELATED,必须首先有一个已经是ESTABLISHED
的连接存在。这个 ESTABLISHED 连接再产生一个主连接之外的新连接,这个新连接就是 RELATED 状态了。INVAILD
: 表示分组对应的连接是未知的,说明数据包不能被识别属于哪个连接或没有任何状态。有几个原因可以产生这种情况,比如,内存溢出,收到不知属于哪个连接的 ICMP 错误信息。我们需要 DROP 这个状态的任何东西,并打印日志:
$ nft add rule inet filter input \
ct state invalid \
log prefix \"Invalid-Input: \" level info flags all \
counter \
drop \
comment \"Drop invalid connections\"
查看规则:
$ nft list chain inet filter input
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
iif "lo" accept comment "Accept any localhost traffic"
iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback"
ct state invalid log prefix "Invalid-Input: " level info flags all counter packets 0 bytes 0 drop comment "Drop invalid connections"
}
}
令牌桶
为了防止有恶意攻击者利用 ping 泛洪(ping flood)来进行攻击,可以利用令牌桶模型来对 ping 包限速。ping 泛洪的原理很简单,就是采用多线程的方法一次性发送多个 ICMP
请求报文,让目的主机忙于处理大量这些报文而造成速度缓慢甚至宕机。
先来介绍一下令牌桶模型。
熟悉 iptables 的朋友应该知道,iptables 通过 hashlimit
模块来实现限速的功能,而 hashlimit
的匹配方式就是基于令牌桶(Token bucket)的模型,nftables 也类似,
令牌桶是一种网络通讯中常见的缓冲区工作原理,它有两个重要的参数,令牌桶容量 n
和 令牌产生速率 s
:
令牌桶容量 n
:可以把令牌当成是门票,而令牌桶则是负责制作和发放门票的管理员,它手里最多有n张令牌。初始时,管理员开始手里有 n 张令牌,每当一个数据包到达后,管理员就看看手里是否还有可用的令牌。如果有,就把令牌发给这个数据包,limit 就告诉nftables,这个数据包被匹配了,而当管理员把手上所有的令牌都发完了,再来的数据包就拿不到令牌了;这时,limit 模块就告诉 nftables ,这个数据包不能被匹配。令牌产生速率 s
:当令牌桶中的令牌数量少于 n,它就会以速率 s 来产生新的令牌,直到令牌数量到达 n 为止。
通过令牌桶机制,可以有效的控制单位时间内通过(匹配)的数据包数量,又可以容许短时间内突发的大量数据包的通过(只要数据包数量不超过令牌桶 n),真是妙哉啊。
nftables 比 iptables 做的更绝,它不仅可以基于数据包来限速,也可以基于字节来限速。为了更精确地验证令牌桶模型,我们选择基于字节来限速:
$ nft add rule inet filter input \
ip protocol icmp icmp type echo-request \
limit rate 20 bytes/second burst 500 bytes \
counter \
accept \
comment \"No ping floods\"
上面的规则表示:
为所有
echo-request
类型的 ICMP 包建立一个匹配项;匹配项对应的令牌桶容量为
500
个字节;令牌产生速率为
20
字节/s
再添加一条规则,拒绝不满足上诉条件的数据包:
$ nft add rule inet filter input \
ip protocol icmp icmp type echo-request \
drop \
comment \"No ping floods\"
同时还要接收状态为 ESTABLISHED 和 RELATED 的数据包:
$ nft add rule inet filter input \
ct state \{ established, related \} \
counter \
accept \
comment \"Accept traffic originated from us\"
下面来做个实验,直接 ping 该服务器的 IP 地址,ping 包大小设置为 100 字节,每秒发送一次:
$ ping -s 92 192.168.57.53 -i 1
PING 192.168.57.53 (192.168.57.53) 92(120) bytes of data.
100 bytes from 192.168.57.53: icmp_seq=1 ttl=64 time=0.402 ms
100 bytes from 192.168.57.53: icmp_seq=2 ttl=64 time=0.373 ms
100 bytes from 192.168.57.53: icmp_seq=3 ttl=64 time=0.465 ms
100 bytes from 192.168.57.53: icmp_seq=4 ttl=64 time=0.349 ms
100 bytes from 192.168.57.53: icmp_seq=5 ttl=64 time=0.411 ms
100 bytes from 192.168.57.53: icmp_seq=11 ttl=64 time=0.425 ms
100 bytes from 192.168.57.53: icmp_seq=17 ttl=64 time=0.383 ms
100 bytes from 192.168.57.53: icmp_seq=23 ttl=64 time=0.442 ms
100 bytes from 192.168.57.53: icmp_seq=29 ttl=64 time=0.464 ms
...
首先我们能看到前 5 个包的回应都非常正常,然后从第 6 个包开始,我们每 6 秒能收到一个正常的回应。这是因为我们设定了令牌桶的容量为 500
个字节,令牌产生速率为 20
字节/s,而发包的速率是每秒钟 100
个字节,即每个包 100
个字节,当发完 5 个包后,令牌桶的容量变为 0,这时开始以 20
字节/s 的速率产生新令牌(和前面提到的令牌桶算法不太一样,只有当令牌桶容量为 0 才开始产生新的令牌),5 秒钟之后,令牌桶的容量变为 100
个字节,所以 6 秒钟后又能收到正常回应。
ICMP & IGMP
接收其他类型的 ICMP
协议数据包:
$ nft add rule inet filter input \
ip protocol icmp icmp type \{ destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem \} \
accept \
comment \"Accept ICMP\"
接收 IGMP
协议数据包:
$ nft add rule inet filter input \
ip protocol igmp \
accept \
comment \"Accept IGMP\"
分别处理 TCP 和 UDP
这一步我们将 TCP 和 UDP 的流量拆分,然后分别处理。先创建两条链:
$ nft add chain inet filter TCP
$ nft add chain inet filter UDP
然后创建一个命名字典:
$ nft add map inet filter input_vmap \{ type inet_proto : verdict \; \}
字典的键表示协议类型,值表示判决动作。
往字典中添加元素:
$ nft add element inet filter input_vmap \{ tcp : jump TCP, udp : jump UDP \}
最后创建一条规则拆分 TCP 和 UDP 的流量:
$ nft add rule inet filter input meta l4proto vmap @input_vmap
其中,meta l4proto
用来匹配协议的类型。
最后再瞄一眼规则:
$ nft list ruleset
table inet filter {
map input_vmap {
type inet_proto : verdict
elements = { tcp : jump TCP, udp : jump UDP }
}
chain input {
type filter hook input priority 0; policy accept;
iif "lo" accept comment "Accept any localhost traffic"
iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback"
ct state invalid log prefix "Invalid-Input: " level info flags all counter packets 95 bytes 6479 drop comment "Drop invalid connections"
icmp type echo-request limit rate 20 bytes/second burst 500 bytes counter packets 17 bytes 2040 accept comment "No ping floods"
icmp type echo-request drop comment "No ping floods"
ct state { established, related } counter packets 172135 bytes 99807569 accept comment "Accept traffic originated from us"
icmp type { destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept comment "Accept ICMP"
ip protocol igmp accept comment "Accept IGMP"
meta l4proto vmap @input_vmap
}
chain forward {
type filter hook forward priority 0; policy accept;
}
chain output {
type filter hook output priority 0; policy accept;
}
chain TCP {
}
chain UDP {
}
}
处理 TCP 流量
这一步我们来处理 TCP 流量,首当其冲的就是 ssh
了,必须得给这位大哥放行啊:
$ nft add rule inet filter TCP \
tcp dport 22 \
ct state new \
limit rate 15/minute \
log prefix \"New SSH connection: \" \
counter \
accept \
comment \"Avoid brute force on SSH\"
其次需要放行 Web 服务,和上面一样,为了易于管理,方便后续动态添加端口,需要先创建一个命名集合:
$ nft add set inet filter web \{ type inet_service \; flags interval \; \}
查看集合:
$ nft list set inet filter web
table inet filter {
set web {
type inet_service
flags interval
}
}
向集合中添加元素:
$ nft add element inet filter web \{ 80, 443 \}
查看集合:
$ nft list set inet filter web
table inet filter {
set web {
type inet_service
flags interval
elements = { http, https }
}
}
放行 Web 服务:
$ nft add rule inet filter TCP \
tcp dport @web \
counter \
accept \
comment \"Accept web server\"
如果你还有其他应用,可以按照上面的方式添加规则,先创建集合:
$ nft add set inet filter xxxx \{ type inet_service \; flags interval \; \}
再添加元素:
$ nft add element inet filter xxxx \{ 9000-9005, 9007 \}
查看集合:
$ nft list set inet filter xxxx
table inet filter {
set xxxx {
type inet_service
flags interval
elements = { 9000-9005, 9007 }
}
}
现在体会到 nftables 集合的强大了吧,可以是区间,可以是单个元素组成的集合,也可以混合,iptables 麻烦让一让。
放行服务:
$ nft add rule inet filter TCP \
tcp dport @xxxx \
counter \
accept \
comment \"Accept xxxx\"
处理 UDP 流量
这一步我们来处理 UDP 流量,比如上面举例的不可描述的应用,除了 TCP 端口还有 UDP 端口,具体用处我就不解释了,自己面向谷歌找答案吧。
到了这一步,连集合都不用创建, 直接复用之前创建的集合,放行应用的 UDP 数据:
$ nft add rule inet filter UDP \
udp dport @xxxx \
counter \
accept \
comment \"Accept xxxx\"
查看规则:
$ nft list chain inet filter UDP
table inet filter {
chain UDP {
udp dport @xxxx counter packets 0 bytes 0 accept comment "Accept xxxx"
}
}
其他 UDP 数据都可按此套路模块化,简直不要太赏心悦目。
为了使系统或 nftables 重启后能够继续生效,我们需要将这些规则持久化,直接将规则写入 /etc/nftables/inet-filter
:
$ echo "#! /usr/sbin/nft -f" > /etc/nftables/inet-filter
$ nft list ruleset >> /etc/nftables/inet-filter
开机自动加载 nftables 服务:
$ systemctl enable nftables
在 rsyslog 中记录日志
默认情况下,开启日志记录后,日志会直接进入 syslog,和系统日志混在一起,不好读取。最好的办法是将 nftables 的日志重定向到单独的文件。
以本文为例,我们只开启了 ct state invalid
和 ssh
的日志记录,先在 /var/log
目录中创建一个名为 nftables
的目录,并在其中创建两个名为 invalid.log
和 ssh.log
的文件,分别存储各自的日志。
$ mkdir /var/log/nftables
$ touch /var/log/nftables/{ssh.log,invalid.log}
确保系统中已安装 rsyslog。现在进入 /etc/rsyslog.d
目录并创建一个名为 nftables.conf
的文件,其内容如下:
:msg,regex,"Invalid-Input: " -/var/log/nftables/invalid.log
:msg,regex,"New SSH connection: " -/var/log/nftables/ssh.log
最后,为了确保日志是可管理的,需要在 /etc/logrotate.d
中创建一个 nftables
文件:
$ cat /etc/logrotate.d/nftables
/var/log/nftables/* { rotate 5 daily maxsize 50M missingok notifempty delaycompress compress postrotate invoke-rc.d rsyslog rotate > /dev/null endscript }
重新通过 ssh 连接服务器,就能看到日志了:
$ tail -f /var/log/nftables/ssh.log
Dec 19 17:15:33 [localhost] kernel: New SSH connection: IN=ens192 OUT= MAC=00:50:56:bd:2f:3d:00:50:56:bd:d7:24:08:00 SRC=192.168.57.2 DST=192.168.57.53 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=43312 DF PROTO=TCP SPT=41842 DPT=22 WINDOW=29200 RES=0x00 SYN URGP=0
评论