1. 从基础到进阶:理解nftables的核心优势
如果你已经用了一段时间的nftables,可能已经熟悉了创建表、链、规则这些基本操作。但nftables的魅力远不止于此,它更像是一个设计精巧的乐高积木系统,而不仅仅是iptables的替代品。我刚开始从iptables迁移过来时,也以为只是命令换了个写法,但真正深入使用后才发现,它的设计哲学完全不同,能让你用更少的规则做更多、更灵活的事情。
nftables最大的优势在于它统一的语法和强大的集合(Set)、字典(Map)功能。在iptables时代,如果你想针对一个IP列表进行过滤,可能需要写很多条重复的规则,或者依赖ipset这个外部工具。但在nftables里,集合是“一等公民”,直接内置于语法中。这意味着你可以动态地管理一个IP地址、端口号甚至协议类型的列表,而规则本身只需要引用这个集合即可。这种“数据与逻辑分离”的设计,让规则集变得异常清晰和易于维护。我记得有一次需要临时封禁一批扫描的IP,用nftables只需要往一个命名集合里添加IP,规则自动生效,完全不用动原有的规则结构,这在以前是不可想象的。
另一个经常被忽视但极其重要的特性是有状态对象,比如计数器(Counter)和配额(Quota)。它们不仅仅是用来计数的工具,更是实现复杂策略的基石。你可以为某个用户、服务或IP段创建一个计数器,然后基于这个计数器的值来做决策。比如,限制某个客户端每分钟的HTTP连接数,或者记录特定服务的总流量。这些对象是独立于规则存在的,可以被多条规则共享和引用,这大大增强了策略的复用性和可观测性。理解了这些核心概念,你才算真正拿到了玩转nftables的钥匙。
2. 规则集优化:让你的防火墙既快又清晰
当你的规则越来越多时,性能和维护性就成了大问题。一个未经优化的规则集,不仅拖慢网络速度,排查问题也如同大海捞针。我踩过不少坑,总结下来,优化主要围绕两个目标:减少规则遍历次数和提升规则匹配效率。
首先,要善用链的跳转。不要把几百条规则都堆在input或forward这样的基础链里。应该根据流量类型(比如HTTP、SSH、DNS)或者策略逻辑(比如“允许内部访问”、“拒绝可疑扫描”)创建多个常规链(Regular Chain)。然后,在基础链里用一两条元数据匹配规则(比如目标端口、协议),将流量jump到对应的常规链中去处理。这样做的好处是,对于不匹配的流量,它只需要经过基础链的一两次判断就被放行或拒绝,而不用遍历后面所有无关的规则。这就像图书馆的索引,你不会从第一本书开始找,而是先找到对应的书架区域。
其次,集合和字典是性能利器。对于需要匹配多个离散值的情况,一定要用命名集合。例如,你需要放行来自公司几个网段(10.0.1.0/24, 10.0.2.0/24, 192.168.1.0/24)的SSH访问。用匿名集合写一条规则也可以,但如果你后续要增减网段,就得替换整条规则。而使用命名集合,你只需要动态更新集合里的元素:
# 创建命名集合 nft add set inet filter trusted_nets { type ipv4_addr; flags interval; } # 添加网段到集合 nft add element inet filter trusted_nets { 10.0.1.0/24, 10.0.2.0/24, 192.168.1.0/24 } # 在规则中引用集合 nft add rule inet filter input ip saddr @trusted_nets tcp dport 22 accept内核在处理时,对集合的查找是高度优化的哈希或树查找,远比线性遍历一系列-s 10.0.1.0/24、-s 10.0.2.0/24的规则要快得多。对于端口列表、MAC地址列表也是如此。
最后,注意规则的顺序。把最频繁匹配的规则(比如“允许已建立连接的相关流量”ct state established,related accept)放在最前面。把最不可能匹配的、或者需要复杂计算的规则(比如基于字符串匹配的深度包检测)放在后面。你可以使用nft --handle list ruleset命令查看每条规则的句柄(handle)和匹配计数(packets, bytes),这是分析规则热点、进行针对性优化的宝贵数据。
2.1 利用字典实现动态策略路由
字典(Map)是比集合更强大的存在,它实现了“键-值”映射。这让你能根据数据包的特征,动态决定它的命运,而无需写一堆if-else式的链式跳转。一个经典的应用场景是策略路由或服务质量标记。
假设你有一台多出口的网关服务器,有两个外网接口:eth0(联通)和eth1(移动)。你想让来自内网192.168.1.10的流量走eth0,来自192.168.1.11的流量走eth1。用传统方法可能需要写多个链和标记规则。用字典可以非常优雅地解决:
# 创建一个字典,将源IP映射到一个标记值(比如路由表ID或DSCP值) nft add map inet myrouter policy_map { type ipv4_addr : mark; } # 添加映射关系 nft add element inet myrouter policy_map { 192.168.1.10 : 0x00000001, 192.168.1.11 : 0x00000002 } # 在prerouting链应用字典:匹配源IP并设置数据包标记 nft add rule inet myrouter prerouting ip saddr vmap @policy_map meta mark set ip saddr map @policy_map # 后续的规则或路由策略可以根据这个mark值来决定数据包走向这里,vmap(value map)语句会根据匹配的源IP,将其对应的值(0x00000001或0x00000002)设置到数据包的mark元数据中。后续你可以用ip rule命令,根据不同的fwmark将数据包导入不同的路由表。整个过程清晰、高效,添加新的IP映射关系只需要更新字典元素,完全不用修改规则逻辑。
3. 实战应用:构建一个智能的家庭网关防火墙
理论说再多,不如动手搭一个。我们以一个典型的家庭网络环境为例,构建一个功能相对完善的网关防火墙。假设你的Linux机器有两个网卡:eth0连接光猫(WAN口),eth1连接内部交换机(LAN口)。目标是:保护内网安全、提供NAT上网、管理孩子设备的上网时间、防止常见网络攻击。
首先,我们规划表结构。建议使用inet地址簇,因为它能同时处理IPv4和IPv6,省去维护两套规则的麻烦。我们创建两个表:一个用于过滤(filter),一个用于网络地址转换(nat)。
# 清空现有规则,从头开始(操作前请确认) nft flush ruleset # 创建filter表 nft add table inet filter # 创建nat表 nft add table inet nat3.1 设计过滤表(filter)的链结构
在inet filter表中,我们创建几个链来组织规则:
# 基础链:处理进入本机的流量 nft add chain inet filter input { type filter hook input priority 0\; policy drop\; } # 基础链:处理经过本机转发的流量 nft add chain inet filter forward { type filter hook forward priority 0\; policy drop\; } # 基础链:处理本机发出的流量 nft add chain inet filter output { type filter hook output priority 0\; policy accept\; } # 常规链:用于处理已建立/相关的连接,提高效率 nft add chain inet filter established # 常规链:专门处理来自LAN的流量 nft add chain inet filter from_lan # 常规链:专门处理去往LAN的流量 nft add chain inet filter to_lan # 常规链:处理常见的攻击行为(如端口扫描、洪水攻击) nft add chain inet filter attack_protect现在,开始填充规则。首先是input链,它最关键,因为直接面对外部和内部的访问。
# input链:首先放行环回接口,很多本地服务依赖它 nft add rule inet filter input iif lo accept # 立即放行所有已建立和相关联的连接,这是性能关键! nft add rule inet filter input ct state established,related jump established # 将来自内网(eth1)的流量交给专门链处理 nft add rule inet filter input iifname eth1 jump from_lan # 将来自外网(eth0)的流量进行攻击防护检查 nft add rule inet filter input iifname eth0 jump attack_protect # 最后,默认拒绝所有其他入站流量(policy已经是drop,但显式拒绝利于记录) nft add rule inet filter input log prefix \"DROP-input: \" drop接下来,填充from_lan链,这里定义内网设备可以访问本机哪些服务。
# 允许内网设备访问本机的DNS(假设本机运行了DNS解析服务) nft add rule inet filter from_lan ip saddr 192.168.1.0/24 udp dport 53 accept nft add rule inet filter from_lan ip saddr 192.168.1.0/24 tcp dport 53 accept # 允许内网设备通过SSH管理本机 nft add rule inet filter from_lan ip saddr 192.168.1.0/24 tcp dport 22 accept # 允许内网访问本机的Web管理界面(例如路由器管理页) nft add rule inet filter from_lan tcp dport 80 accept nft add rule inet filter from_lan tcp dport 443 acceptattack_protect链是实现安全加固的地方。我们可以用limit表达式来限制连接速率,防止暴力破解和洪水攻击。
# 保护SSH端口:每分钟每个IP只允许新建5个连接 nft add rule inet filter attack_protect tcp dport 22 ct state new limit rate 5/minute accept # 保护Web服务(如自建博客):每秒最多接受25个新连接 nft add rule inet filter attack_protect tcp dport { 80, 443 } ct state new limit rate 25/second accept # 丢弃无效状态的数据包,这些通常是恶意的 nft add rule inet filter attack_protect ct state invalid drop # 记录并丢弃其他所有从外网主动发起的新连接(除了上面明确允许的端口) nft add rule inet filter attack_protect log prefix \"WAN-new: \" drop3.2 设计NAT表实现共享上网
内网设备要上网,需要NAT(网络地址转换)。我们在inet nat表中操作。
# 创建NAT的postrouting链(用于源地址转换SNAT/Masquerade) nft add chain inet nat postrouting { type nat hook postrouting priority 100\; } # 创建NAT的prerouting链(用于目的地址转换DNAT,比如端口转发) nft add chain inet nat prerouting { type nat hook prerouting priority -100\; }实现基本的MASQUERADE(动态SNAT),让内网可以通过WAN口上网:
# 对所有从eth1(LAN)进来,从eth0(WAN)出去的流量进行IP伪装 nft add rule inet nat postrouting iifname eth1 oifname eth0 masquerade如果你有公网IP,想从外网访问内网的某台服务器(比如家里的NAS),就需要端口转发(DNAT):
# 将WAN口(eth0)的TCP 2222端口转发到内网NAS(192.168.1.100)的22端口(SSH) nft add rule inet nat prerouting iifname eth0 tcp dport 2222 dnat to 192.168.1.100:22 # 别忘了,forward链需要允许这条转发的流量通过! # 在inet filter forward链中添加规则(注意,forward链的默认策略我们设为了drop) nft add rule inet filter forward iifname eth0 oifname eth1 ct state { established, related } accept nft add rule inet filter forward iifname eth1 oifname eth0 ct state new accept # 允许端口转发流量 nft add rule inet filter forward ip daddr 192.168.1.100 tcp dport 22 accept3.3 高级功能:基于时间的访问控制
nftables原生支持时间匹配,这太有用了!比如,限制孩子的电脑192.168.1.50只能在周末的晚上7点到9点上网。
# 创建一个名为`kid_pc`的集合,方便管理 nft add set inet filter kid_pc { type ipv4_addr\; } nft add element inet filter kid_pc { 192.168.1.50 } # 在forward链中插入规则:在非允许时间,拒绝该IP的转发流量(即不能上网) # 注意规则顺序,这条拒绝规则要放在默认的允许规则之前 nft insert rule inet filter forward position 0 ip saddr @kid_pc meta hour { 19-21 } meta day { Saturday, Sunday } accept nft insert rule inet filter forward position 1 ip saddr @kid_pc drop这里,meta hour和meta day表达式直接匹配数据包到达的时间。position 0和position 1指定了插入规则的位置,确保时间检查规则优先于其他允许转发的通用规则。这样,在非指定时间段,来自孩子电脑的流量在forward链的起始位置就被丢弃了,简单有效。
4. 排错与维护:让防火墙稳定运行
配置再好的防火墙,不会维护和排错也是白搭。nftables提供了一些强大的工具来帮助你。
首先,规则集的备份与恢复必须成为习惯。我习惯将规则保存为纯文本的.nft文件,它本身就是可执行的脚本。
# 备份当前所有规则到文件 nft list ruleset > /etc/nftables.conf # 恢复规则(原子操作,要么全部成功,要么全部失败) nft -f /etc/nftables.conf你可以将nft -f /etc/nftables.conf命令加到系统启动脚本中。更推荐的做法是使用发行版自带的systemd服务,例如在/etc/systemd/system/nftables.service中定义,并systemctl enable nftables。
其次,监控与日志至关重要。nftables的log语句可以将匹配的数据包信息记录到系统日志(如journalctl或/var/log/messages)。但要注意,被limit限速的规则如果记录日志,可能会产生大量日志。更好的做法是,先使用counter对象进行无日志计数,发现问题后再临时开启日志进行深度排查。
# 为input链添加一个计数器,监控被丢弃的包 nft add rule inet filter input counter drop # 查看所有计数器的值 nft list counters当你遇到网络不通时,一个高效的排查思路是:从宏观到微观,从简单到复杂。
- 检查规则集状态:
nft list ruleset确认规则是否按预期加载。 - 检查链的策略和计数器:
nft list ruleset -a查看每条规则的匹配计数(packets, bytes)。如果某条你期望匹配的规则计数为0,说明流量根本没走到这里。 - 检查连接跟踪状态:对于NAT或状态过滤问题,
conntrack -L命令是神器。它能显示所有被跟踪的连接,帮你确认NAT转换是否正确,连接状态是否正常。 - 启用临时日志:在怀疑的规则前插入一条
log规则。例如,你想知道某个IP的流量为什么被拒绝,可以在input链的drop规则前加一条:nft insert rule inet filter input position X ip saddr 可疑IP log prefix \"DEBUG-drop: \" accept。注意这里先用accept让包通过,避免影响业务,同时记录日志。查看日志后记得删除这条调试规则。
最后,关于性能调优。对于绝大多数家庭和小型办公场景,nftables的性能绰绰有余。如果遇到性能瓶颈,首先应该用上面提到的nft --handle list ruleset查看规则命中计数,优化规则顺序,多用集合和字典。极端情况下,可以考虑使用flowtable(流表)来加速转发。流表能对成功匹配的连接进行“硬件卸载”式的快速转发,但配置相对复杂,且需要网卡驱动支持。对于新手,我建议先扎实掌握基础和上述优化技巧,流表可以在遇到真实性能需求时再深入研究。
防火墙的配置不是一劳永逸的。随着网络服务的变化,你需要不断地审视和调整规则。每次修改前做好备份,修改后充分测试。养成记录配置变更和规则用途注释的习惯,时间久了你会感谢自己的。nftables的语法虽然一开始需要适应,但一旦掌握,其表达能力和简洁性会让你觉得之前的折腾都是值得的。