nftables搭建防火墙
0. 概要
nftables
是iptables
的替代版本,nftables解决了iptables的功能有限、性能差的问题。nftables
的实现在内核的虚拟机上运行,nftables
的逻辑更像是一个编程语言
,从而更灵活、高效。
nftables
设计的特点:
1. 防火墙规则的编写类似于 编程语言;
2. hook机制,hook对应Linux内核的网络处理流程的阶段(stage);
3. rule规则:每个rule
包含N个expression
(表达式)、N个非终止statement
(语句)、至多1个终止statement
(语句);
想要了解更多信息请查看帮助手册:man 8 nft
, 👉 nftables official documentation。
小技巧:
1
# 安装pandoc
2
apt install pandoc
3
# 把man转化为pdf(竖版A4)
4
zcat /usr/share/man/man8/nft.8.gz | pandoc -V geometry:a4paper,margin=2cm -f man -t pdf -o nft.8.pdf
5
# 把man转化为pdf(横版A4)
6
zcat /usr/share/man/man8/nft.8.gz | pandoc -V geometry:a4paper,margin=2cm -V classoption:landscape -f man -t pdf -o nft.8.pdf
1. 核心概念
首先要明确概念的关系,概念从大到小:ruleset
> address family
> table
> chain
> rule
> expression & statement
。
每个address family
包含N个table
,每个table
包含chain
,每个chain
包含N个rule
,每个rule
包含N个expression
和N个statement
。
1.1 ruleset(规则集合)
ruleset
代表整个nftables
的规则。
常用的命令:
1
# 1. 列出所有的规则
2
nft list ruleset
3
# 2. 列出某address family(此处是`ip`)的规则
4
nft list ruleset ip
5
# 3. 清空所有的规则
6
nft flush ruleset
7
# 4. 清空某address family(此处是`ip`)的规则
8
nft flush ruleset ip
1.2 address family
address family
决定了正在处理的报文的类型。
address family | 说明 |
---|---|
ip | IPv4 address family |
ip6 | IPv6 address family |
inet | IPv4 和 IPv6 |
arp | ARP address family,处理IPv4 ARP报文 |
bridge | Bridge address family,处理与网桥相关的报文 |
netdev | 处理ingress(进站)和egress(出站)的报文 |
1.3 table
table
用来承载 chain
、set
、map
、stateful objects
(有状态的对象)。
table
可以被当作编程语言的namespace
,table的名称是什么不重要,但习惯上可以仍旧使用iptables
的table name
。
常见的table name
有:filter
,nat
,route
。
1.4 chain
chain
是rule
的容器,从编程角度来看,chain
就是一个list
,chain
中的元素rule
是有序的,排在前边的rule
优先被处理。
有两种chain
:
1. base chain
,使用hook嵌入到Linux的networking stack
(网络栈)中。可以jump到regular chain
;
2. regular chain
,普通的chain,可以jump到其他的regular chain
。
通常我们先定义base chain
,如果base chain
的rule
太复杂,可以把部分的rule
提取出来放到regular chain
。
1.4.1 chain type
chain type 指定了base chain
的类型(用途)。
chain type table:
Type | Family | Hook | 说明 |
---|---|---|---|
fitler | ip ,ip6 ,inet ,arp ,bridge ,netdev | ingress ,prerouting ,input ,forward ,output ,postrouting ,egress | |
nat | ip ,ip6 ,inet | prerouting ,input ,output ,postrouting | |
route | ip ,ip6 | output |
1.4.2 hook
hook
是nftables
在内核网络处理流程上预留的接入点
,hook
对应内核的网络处理流程的某个processing stage
(处理阶段);base chain
要hook
到networking stack
(网络栈)的预留的接入点
,才能处理packets
(报文)。
完整的processing stage
:
序号 | stage or hook | 说明 |
---|---|---|
0 | ingress | 报文刚刚被网卡接收(进入主机),且被第3层协议栈(IP层)处理之前 |
1 | prerouting | 报文被第3层协议栈(IP层)处理之后,且在被路由之前 |
- | routing | 报文被路由,这个stage不能被hook,此时分叉:input->output 分支和forward 分支 |
2 (分支1) | input | 报文被发向本机进程之前 |
3 (分支1) | output | 报文被本机进程发出之后 |
2 (分支2) | forward | 报文被转发到其他的主机或网卡。注意forward 与 input -> output 是并行的分支,参考下文的netfilter流程图。 |
- | routing | 报文被路由,这个stage不能被hook。input->output 分支和forward 分支在此处汇合。 |
4 | postrouting | 报文被路由之后,且被第3层协议栈(IP层)处理之前。 |
5 | egress | 报文被第3层协议栈(IP层)处理之后,且被发送到网卡(离开主机)前 |
- IPv4/IPv6/inet address family支持的
hook
:prerouting
,intput
,forward
,output
,postrouting
,ingress
- ARP address family支持的
hook
:input
,output
- bridge address family支持的
hook
:prerouting
,intput
,forward
,output
,postrouting
,ingress
- netdev address family支持的
hook
:ingress
,egress
参考下图理解netfilter预留的hook点:
❶ iptables流程图
来源:https://www.frozentux.net/iptables-tutorial/images/tables_traverse.jpg
❷ netfilter简要流程图
来源:http://linux-ip.net/nf/nfk-traversal.png
❸ netfilter详细且复杂的流程图
来源:wikimedia.org
1.4.3 example nftables script
1
table inet filter {
2
# chain: inet filter input
3
chain input {
4
type filter hook input priority 0; policy drop;
5
# ===== rules =====
6
}
7
8
# chain: inet filter forward
9
chain forward {
10
type filter hook forward priority 0; policy accept;
11
# ===== rules =====
12
}
13
14
# chain: inet filter output
15
chain output {
16
type filter hook output priority 0; policy accept;
17
# ===== rules =====
18
}
19
}
代码解释:
type filter hook input priority 0; policy drop;
表示type=filter,hook=input,priority=0。
priority
数值越小越优先,priority可以看作队列的序号,序号越小越被优先处理。
1.5 rule
rule
是nftables
的核心,是用来处理packets
代码。
一个rule
由N个expression
、N个non-terminal statement
、至多1个terminal statement
构成。
❶ 从编程语言的角度理解rule
、expression
、statement
:
1
// 对于每个rule
2
if (expression1 && expression2 && expression3 && ...)
3
{
4
// N个`non-terminal statement`
5
...
6
// 至多1个`terminal statement`
7
...
8
}
❷ 与iptables的关系
expression
等价于iptables
的matches
,statement
对应iptables
的target
/action
,不同的是nftables
可以有多个statement
;
1.5.1 expression
expression
表示一个value
(数值),value
可以是常量,也可以是复杂的计算表达式expression
。
expression
也有自己的数据类型(如同编程语言)。
使用命令查看 expression的信息:
1
# 查看 expression `ip saddr` 的信息
2
nft describe ip saddr
3
# 查看 expression `tcp flags` 的信息
4
nft describe tcp flags
built-in operations(内置算子): 👉 Building rules through expressions
写法1 | 写法2 | 说明 |
---|---|---|
eq | == | equal(等于)可以省略 |
ne | != | not equal(不等于) |
lt | < | less than(小于) |
gt | > | greater than(大于) |
le | <= | less than or equal to(小于或等于) |
ge | >= | greater than or equal to(大约或等于) |
expression
的常见用法:
算子 | 示例 | 说明 |
---|---|---|
相等 | tcp dport 22 | 如果 tcp dport 等于 22 ,则匹配成功,反之则匹配失败跳过此rule。 |
不相等 | tcp dport != 22 | 如果 tcp dport 不等于 22 ,则匹配成功,反之则匹配失败跳过此rule。 |
包含 | tcp dport {20,21,22} | 如果tcp dport 是 20,21,22 的其中之一,则匹配成功,反之则匹配失败跳过此rule。 |
映射map | tcp dport map {20: 192.168.0.20, 21: 192.168.0.21} | 如果tcp dport 等于20 ,则此表达=192.168.0.20;如果 tcp dport 等于21 ,则此表达=192.168.0.21;否则rule匹配失败,跳过此rule |
映射vmap | ip protocol vmap {tcp: jump tcp_chain, udp: jump udp_chain} | 如果ip protocol 等于tcp ,则执行jump tcp_chain ;如果 ip protocol 等于udp ,则执行jump udp_chain ;否则rule匹配失败,跳过此rule |
常用的expression
:
expression | 说明 |
---|---|
meta length | 报文的长度,单位:bytes |
meta l4proto | 第4层的协议名称,参考nft describe meta l4proto 。预定义的符号常量:ip,icmp,igmp,ggp,ipencap,st,tcp,egp,igp,pup,udp,hmp,xns-idp,rdp,iso-tp4,dccp,xtp,ddp,idpr-cmtp,ipv6,ipv6-route,ipv6-frag,idrp,rsvp,gre,esp,ah,skip,ipv6-icmp,ipv6-nonxt,ipv6-opts,rspf,vmtp,eigrp,ospf,ax.25,ipip,etherip,encap,pim,ipcomp,vrrp,l2tp,isis,sctp,fc,mobility-header,udplite,mpls-in-ip,manet,hip,shim6,wesp,rohc,ethernet. |
meta iif | 报文进站的网卡设备Index |
meta iffname | 报文进站的网卡设备名称 |
meta oif | 报文出站使用的网卡设备Index |
meta oifname | 报文出站使用的网卡设备名称 |
meta ibrname | 报文进站使用的网桥设备名称 |
meta obrname | 报文出站使用的网桥设备名称 |
meta skuid | 关联socket的进程的UID |
meta skgid | 关联socket的进程的GID |
socket transparent | socket IP_TRANSPARENT option |
socket mark | socket SO_MARK |
socket wildcard | wildcard-bound例如:0.0.0.0 |
rt [ip|ip6] classid | routing realm |
rt [ip|ip6] nexthop | routing nexthop |
rt [ip|ip6] mtu | TCP MTU |
rt [ip|ip6] ipsec | route via ipsec |
ether saddr | 源MAC地址 |
ether daddr | 目的MAC地址 |
ether type | Ether Type:ip,arp,ip6,8021q,8021ad,vlan |
ip length | IPv4报文总长度 |
ip protocol | 传输层协议,参考nft describe ip protocol .预定义的符号常量:ip,icmp,igmp,ggp,ipencap,st,tcp,egp,igp,pup,udp,hmp,xns-idp,rdp,iso-tp4,dccp,xtp,ddp,idpr-cmtp,ipv6,ipv6-route,ipv6-frag,idrp,rsvp,gre,esp,ah,skip,ipv6-icmp,ipv6-nonxt,ipv6-opts,rspf,vmtp,eigrp,ospf,ax.25,ipip,etherip,encap,pim,ipcomp,vrrp,l2tp,isis,sctp,fc,mobility-header,udplite,mpls-in-ip,manet,hip,shim6,wesp,rohc,ethernet. |
ip saddr | 源IPv4地址 |
ip daddr | 目的IPv4地址 |
ip6 length | IPv6报文总长度 |
ip6 protocol | 传输层协议 |
ip6 saddr | 源IPv6地址 |
ip6 daddr | 目的IPv6地址 |
icmp type | icmp type 字段,参考nft describe icmp type .预定义的符号常量:echo-reply,destination-unreachable,source-quench,redirect,echo-request,router-advertisement,router-solicitation,time-exceeded,parameter-problem,timestamp-request,timestamp-reply,info-request,info-reply,address-mask-request,address-mask-reply. |
icmp code | icmp code 字段,参考nft describe icmp code .预定义的符号常量:net-unreachable,host-unreachable,prot-unreachable,port-unreachable,net-prohibited,host-prohibited,admin-prohibited,frag-needed. |
icmpv6 type | ICMPv6 type 字段,参考nft describe icmpv6 type .预定义的符号常量:destination-unreachable,packet-too-big,time-exceeded,parameter-problem,echo-request,echo-reply,mld-listener-query,mld-listener-report,mld-listener-done,mld-listener-reduction,nd-router-solicit,nd-router-advert,nd-neighbor-solicit,nd-neighbor-advert,nd-redirect,router-renumbering,ind-neighbor-solicit,ind-neighbor-advert,mld2-listener-report. |
icmpv6 code | ICMPv6 code 字段,参考nft describe icmpv6 code .预定义的符号常量:no-route,admin-prohibited,addr-unreachable,port-unreachable,policy-fail,reject-route. |
tcp sport | tcp source port |
tcp dport | tcp destination port |
tcp flags | tcp flags字段,参考nft describe tcp flags .预定义的符号常量: fin,syn,rst,psh,ack,urg,ecn,cwr. |
udp sport | udp source port |
udp dport | udp destination port |
sctp sport | sctp source port |
sctp dport | sctp destination port |
ct state | connection track,连接的状态:pre-defined,invalid,new,established,related,untracked |
ct status | connection track,连接的状态:expected,seen-reply,assured,confirmed,snat,dnat,dying |
ct mark | connection mark |
ct count | 当前连接的数量 |
1.5.2 statement
statement | 示例 | 说明 |
---|---|---|
accept | tcp dport 22 accept | 如果tcp dport 等于 22 ,则接受 packet并终止执行此chain;否则,继续查看下个rule。 |
drop | tcp dport 22 drop | 如果tcp dport 等于 22 ,则丢弃 packet并终止后续ruleset;否则,继续查看下个rule。 |
queue | tcp dport 22 queue | 如果tcp dport 等于 22 ,则提交packet到用户态的队列中并终止后续ruleset;否则,继续查看下个rule。 |
continue | tcp dport 22 continue | 如果tcp dport 等于 22 ,继续查看下个rule。continue 是rule的默认statement。 |
jump | tcp dport 22 other_chain | 如果tcp dport 等于 22 ,则跳转到other_chain执行,执行完后再跳回当前chain的rule继续执行;否则,继续查看下个rule。jump类似于函数调用。 |
goto | tcp dport 22 other_chain | 如果tcp dport 等于 22 ,则跳转到other_chain执行,执行流程不会返回当前chain;否则,继续查看下个rule。 |
reject | tcp dport 22 reject with icmpx admin-prohibited | 如果tcp dport 等于 22 ,则丢弃 packet并发送ICMP错误代码(admin-prohibited)并终止后续ruleset;否则,继续查看下个rule。 |
limit rate | tcp dport 22 limit rate over 10/second burst 10 drop | 如果tcp dport 等于 22 并且 发送的报文速率超过10个每秒,则丢弃该报文。 |
limit rate | tcp dport 22 limit rate over 10mbytes/second burst 4mbytes drop | 如果tcp dport 等于 22 并且 发送的报文速率超过10Mbytes每秒,则丢弃该报文。 |
snat | oif eth0 snat to 1.2.3.4 | 如果报文来自eth0,则修改报文的源IP地址为1.2.3.4 |
dnat | iif eth0 dnat to 192.168.1.120 | 如果报文流向eth0,则修改报文的目的IP地址为192.168.1.120 |
masquerade | iif eth0 masquerade | 如果报文流向eth0,则修改报文的源IP地址为eth0的IP地址。masquerade 是 snat 的特化版本 |
set | tcp flags syn tcp option maxseg size set rt mtu | 如果tcp flags 等于syn ,则修改 tcp option maxseg size 为rt mtu 。 |
2. 编写nftables script基础流程
2.0 nftables script基础知识
❶ 语法格式
- 按行解析,每个rule对应一行;
- 如果每行的结尾是
\
,表示续行; - 一行可以有多个命令,用
;
分割; #
表示(行)注释,#
之后的此行的字符被忽略掉;- 标识符(id),以字母(a-z,A-Z)开头,后续N个字母、数字、
/
、\
、_
、.
;特别的,如果id与nftables的关键字重名,需要加上“
;id可用于table
、chain
、variable
、set
、map
等的名字。
❷ 加载其他nftables script文件
1
# 加载"/etc/xxx.nft"文件内容
2
include "/etc/xxx.nft"
3
# 加载"/etc/nftables/"文件夹下面的以`.nft`结尾的文件内容
4
include "/etc/nftables/*.nft"
❸ 变量
1
# `<variable name>`替换为实际的id。
2
# 定义名字是`<variable name>`的变量
3
define <variable name> = <some value>
4
# 删除变量`<variable name>`
5
undefine <variable name>
6
# 重新定义变量`<variable name>`,改变了数值与类型。
7
redefine <variable name> = <new value>
8
# 定义变量ports
9
define ports = {22,23,24}
10
# 使用变量ports,需要加上前缀`$`。
11
tcp dport $ports accept
❹ set 和 map
1
# 把`<your name>`, `<your type>`, `<your flags>`, `<your element1>`, `<your element2>`替换为实际的值。
2
table inet filter {
3
# 在某table中定义set 或 map
4
set <your name> {
5
type <your type>; flags <your flags>;
6
elements = {
7
# element1
8
<your element1>,
9
<your element2>,
10
}
11
}
12
}
示例:
1
table inet filter {
2
# 定义set,名字是denylist,type是ipv4_addr,flags interval,元素类型就是Ipv4地址且支持CIDR.
3
set denylist {
4
type ipv4_addr; flags interval;
5
elements = {
6
1.2.3.4/8,
7
5.6.7.8,
8
}
9
}
10
# 使用
11
chain input {
12
type filter hook input priority 0; policy drop;
13
# 此处使用denylist,需要加上前缀@
14
ip saddr @denylist drop
15
# more rules ...
16
}
17
}
2.1 install nftables
on debian 12:
1
apt install nftables
2
systemctl disable firewalld
3
systemctl stop firewalld
2.2 edit nftables script file
创建临时文件/root/nftables.conf
并编辑,可以参考下文的模版。
2.3 防呆措施
在root用户的crontab上添加
1
*/5 * * * * /usr/sbin/nft flush ruleset >/dev/null 2>/dev/null
这样每5分钟,nftables ruleset
就会被清空,当错误的修改了nftables
时,有机会远程登录。
2.4 测试nftables script
1
# 语法检查并加载`/root/nftables.conf`
2
nft -c -f /root/nftables.conf && nft -f /root/nftables.conf
检查防火墙是否符合预期。若是改错了nftables导致不能远程登录,耐心等5分钟直至防火墙ruleset被清空,方可再次登录。
修改/root/nftables.nft
并测试 /root/nftables.nft
,这样反复进行直至符合预期。
2.5 提交
在debian 12
上,查看/usr/lib/systemd/system/nftables.service
的内容:
1
# 省略...
2
[Service]
3
Type=oneshot
4
RemainAfterExit=yes
5
StandardInput=null
6
ProtectSystem=full
7
ProtectHome=true
8
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
9
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
10
ExecStop=/usr/sbin/nft flush ruleset
11
# 省略...
可以看到nftables
的主配置文件就是/etc/nftables.conf
。对于其他系统,查看nftables.service
的内容以确定配置文件的路径。
1
# 把临时script文件内容更新到正式的配置文件`/etc/nftables.conf`
2
cp /root/nftables.conf /etc/nftables.conf
3
# 确保正确的权限
4
chmod 755 /etc/nftables.conf
5
# 仅需首次执行
6
systemctl enable --now nftables
7
# 重新加载nftables配置文件,使得新配置生效。
8
systemctl reload nftables
2.6 关闭防呆措施
在root用户的crontab上注释掉
1
# 注释掉这行,关闭防呆措施。
2
# */5 * * * * /usr/sbin/nft flush ruleset >/dev/null 2>/dev/null
3. nftables script template
1
#!/usr/sbin/nft -f
2
3
flush ruleset
4
5
table inet filter {
6
# deny access from these hosts.
7
set denylist {
8
type ipv4_addr; flags interval;
9
elements = {
10
# below IP address are examples, replace them by yours.
11
1.1.1.0/24,
12
4.5.6.7,
13
}
14
}
15
# limit upload speed for these hosts.
16
set slowdown_ratelimiter {
17
type ipv4_addr; flags dynamic; size 65536; timeout 60s;
18
}
19
set slowdown {
20
type ipv4_addr; flags interval;
21
elements = {
22
# below IP address are examples, replace them by yours.
23
1.2.3.0/8,
24
4.5.6.0/13,
25
7.8.9.0/24,
26
}
27
}
28
# chain: inet filter input
29
chain input {
30
type filter hook input priority 0; policy drop;
31
# ===== deny list =====
32
ip saddr @denylist reject with icmpx host-unreachable
33
# ===== default =====
34
meta nfproto ipv6 drop
35
# allow icmp
36
meta l4proto { icmp, ipv6-icmp } accept
37
ct state { established, related } accept
38
ct status dnat accept
39
iifname "lo" accept
40
ct state invalid drop
41
# ===== custom =====
42
# ssh
43
tcp dport 22 accept
44
# smb
45
udp dport { 137, 138 } accept
46
tcp dport { 139, 445 } accept
47
# ftp
48
tcp dport { 20, 21, 65000-65535 } accept
49
# xrdp
50
tcp dport 3389 accept
51
# haproxy
52
tcp dport 9000 accept
53
# ===== libvirt =====
54
iifname "virbr0" tcp dport { 53, 67 } accept
55
iifname "virbr0" udp dport { 53, 67 } accept
56
}
57
# chain: inet filter forward
58
chain forward {
59
type filter hook forward priority 0; policy accept;
60
# ===== default =====
61
meta nfproto ipv6 drop
62
# ct state { established, related } accept
63
# ct status dnat accept
64
# ct state invalid drop
65
# iifname "lo" accept
66
# oifname "lo" accept
67
# iifname "enp0s31f6" accept
68
# oifname "enp0s31f6" accept
69
# ===== libvirt =====
70
# iifname "virbr0" accept
71
# oifname "virbr0" accept
72
# oifname "virbr0" ct state { established, related } accept
73
# oifname "virbr0" reject
74
}
75
76
# chain: inet filter output
77
chain output {
78
type filter hook output priority 0; policy accept;
79
# ===== deny list =====
80
ip daddr @denylist reject with icmpx host-unreachable
81
# ===== slowdown =====
82
ip daddr @slowdown update @slowdown_ratelimiter { ip daddr limit rate over 100 kbytes/second } drop
83
# # ===== default =====
84
# meta nfproto ipv6 drop
85
# ct state { established, related } accept
86
# oifname "lo" accept
87
# oifname "virbr0" accept
88
# # accept all private IP address
89
# ip daddr { 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } accept
90
# # ===== custom =====
91
}
92
}
93
94
table inet nat {
95
# chain: inet nat postrouting
96
chain postrouting {
97
type nat hook postrouting priority srcnat; policy accept;
98
# ===== default =====
99
meta nfproto ipv6 drop
100
# ===== libvirt =====
101
oifname != "virbr0" iifname "virbr0" masquerade
102
}
103
}