玩转 NFTables
2021年3月17日 | Lars Vogdt 和 Darix | CC-BY-SA-3.0
默认情况下,openSUSE Leap 15.x 使用 firewalld 防火墙实现(并且 firewalld 后端在底层使用 iptables)。
但是,openSUSE 已经有一段时间支持 nftables 了——但 YaST 或其他专用工具目前尚未配置为直接支持它。但我们有一些机器在我们的基础设施中,它们既不是直接的桌面机器,也不是大部分时间处于空闲状态。所以让我们尝试一下我们有多好,可以尝试和测试新事物,并使用我们中央管理机器之一:VPN 网关,它使所有 openSUSE 英雄都可以访问 openSUSE 基础设施的内部世界。
这台机器已经有点特殊了
- “外部”接口持有与互联网的连接
- “私有”接口位于 openSUSE 英雄私有网络内部
- 我们运行 openVPN,使用 tun 设备(一个用于 udp,一个用于 tcp),以允许 openSUSE 英雄通过个人证书 + 他们的用户凭据连接
- 此外,我们运行 wireguard 以连接普罗沃和纽伦堡(在我们的赞助商处)的私有网络
- 而且在我们忘记之前:我们的 VPN 网关不仅是 VPN 网关:它还被用作所有内部机器的互联网网关,只允许“预知流量”目的地
所有这些都使防火墙设置变得更加复杂。
顺便说一下:通过给接口命名,例如在我们的示例中“external”或“private”,如果你使用服务或防火墙,这将带来巨大的好处。只需查看一下 /etc/udev/rules.d/70-persistent-net.rules 你的设备启动后,并根据你的需要重命名它们(你也可以使用 YaST 来完成此操作)。但请记住也要检查/重命名 */etc/sysconfig/network/ifcfg-** 中的接口,以使用相同的名称,然后再重新启动机器。否则你将陷入无法工作的网络设置。
让我们简要了解一下我们所说的领域

正如你可能注意到的,社区侧面的任何服务都不会受到影响。我们拥有基于标准(iptables)的防火墙,并使用代理将用户请求转发到正确的服务器。
在 openSUSE 英雄侧,我们用基于 nftables 的新设置取代了旧的 SuSEfirewall2 设置。
有几个原因影响了我们切换到 nftables
- 旧的 SuSEfirewall2 正常工作,但在我们的问题机器上生成了一个巨大的 iptables 列表
- 使用 ipsets 或变量与 SuSEfirewall2 是可行的,但不是一件容易的任务
- 我们在使用 firewalld 作为前端时遇到了一些 NAT 和 Masquerading 问题
- Salt 是另一个有趣的领域
- 通过在机器上部署一些文件来 Salt'ing SuSEfirewall2 总是可能的,但并不是很直接
- 没有适用于 SuSEfirewall2 的 Salt 模块(可能永远也不会有)
- 有适用于 firewalld 和 nftables 的 Salt 模块,两者水平几乎相同
- nftables 已经集成到内核中一段时间了,应该长期取代所有 *tables 模块。所以为什么不直接跳转到它,因为(作为管理员)我们反正不使用 YaST 或 firewalld-gui 等 GUI 工具?
那么主要优势是什么?
- 集合是核心功能的一部分。你可以拥有端口、接口名称和地址范围的集合。不再需要 ipset。不再需要 multiport。
ip daddr { 1.1.1.1, 1.0.0.1 } tcp dport { dns, https } oifname { "external", "wg_vpn1" } accept;这意味着你可以用几个规则覆盖大量的防火墙集合。 - 不再需要额外的规则来记录。只需在需要时打开计数器。
counter log prefix "[nftables] forward reject " reject - 当使用
table inet时,你可以用单个规则集覆盖 IPv4 和 IPv6,但你也可以拥有每个 IP 协议的表。有时甚至需要它们,例如用于后路由。
从头开始
一个非常基本的 /etc/nftables.conf 看起来像这样
#!/usr/sbin/nft -f
flush ruleset
# This matches IPv4 and IPv6
table inet filter {
# chain names are up to you.
# what part of the traffic they cover,
# depends on the type line.
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;
}
}
但到目前为止,我们还没有停止或允许任何流量。好吧,实际上我们现在允许所有流量进出,因为所有链都具有接受策略。
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain base_checks {
## another set, this time for connection tracking states.
# allow established/related connections
ct state {established, related} accept;
# early drop of invalid connections
ct state invalid drop;
}
chain input {
type filter hook input priority 0; policy drop;
# allow from loopback
iif "lo" accept;
jump base_checks;
# allow icmp and igmp
ip6 nexthdr icmpv6 icmpv6 type { echo-request, echo-reply, packet-too-big, time-exceeded, parameter-problem, destination-unreachable, packet-too-big, mld-listener-query, mld-listener-report, mld-listener-reduction, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report } accept;
ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } accept;
ip protocol igmp accept;
# for testing reject with logging
counter log prefix "[nftables] input reject " reject;
}
chain forward {
type filter hook forward priority 0; policy accept;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
你可以使用 nft --file nftables.conf 激活配置,但不要在远程机器上这样做。在实际加载文件之前运行 nft --check --file nftables.conf 以捕获语法错误也是一个好习惯。
那么我们改变了什么?
- 最重要的是,我们将链的策略更改为 drop,并在末尾添加了一个 reject 规则。所以现在没有任何东西可以进入。
- 我们允许所有流量在 localhost 接口上。
- base_checks 链处理与已建立连接相关的所有数据包。这确保了来自传出连接的传入数据包能够通过。
- 我们允许重要的 ICMP/IGMP 数据包。同样,这是使用集合和类型名称而不是一些神秘的数字。为可读性欢呼。
现在,如果有人尝试通过 ssh 连接到我们的机器,我们会看到
[nftables] input reject IN=enp1s0 OUT= MAC=52:54:00:4c:51:6c:52:54:00:73:a1:57:08:00 SRC=172.16.16.2 DST=172.16.16.30 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=22652 DF PROTO=TCP SPT=55574 DPT=22 WINDOW=64240 RES=0x00 SYN URGP=0
并且 nft list ruleset 将向我们显示
counter packets 1 bytes 60 log prefix "[nftables] input reject " reject
所以我们现在是安全的。虽然也许允许 SSH 重新进入会很好。你知道,以防万一。我们现在有两个选择。选项 1 将是在我们的 reject 行之前插入以下行。
tcp dport 22 accept;
但是我们已经提到过我们有集合,它们很棒了吗?特别是当我们需要在多个地方使用相同的端口/IP 范围/接口名称时?
我们有两种定义集合的方式
define wanted_tcp_ports {
22,
}
是的,尾随逗号是可以的。并且它使向列表中添加元素更容易。所以我们一直这样做。这将改变我们上面的规则为
tcp dport $wanted_tcp_ports accept;
如果加载配置文件并运行 nft list ruleset,我们会看到
tcp dport { 22 } accept
但实际上有一种稍微更好的方法来做到这一点
set wanted_tcp_ports {
type inet_service; flags interval;
elements = {
ssh
}
}
这样我们的防火墙规则就变成了
tcp dport @wanted_tcp_ports accept;
如果我们使用 nft list ruleset 之后转储防火墙,它仍然会显示为 @wanted_tcp_ports,并且不会用值替换变量。虽然这已经很棒了,但第二种语法实际上还有一个优势。
$ nft add element inet filter wanted_tcp_ports \{ 443 \}
现在我们的 wanted_tcp_ports 列表将允许端口 22 和 443。这当然更有效,如果我们用它来使用 IP 地址。
set fail2ban_hosts {
type ipv4_addr; flags interval;
elements = {
192.168.0.0/24
}
}
让我们向该集合添加一些元素
$ nft add element inet filter fail2ban_hosts \{ 192.168.254.255, 192.168.253.0/24 \}
$ nft list ruleset
… 并且我们得到…
set fail2ban_hosts {
type ipv4_addr
flags interval
elements = { 192.168.0.0/24, 192.168.253.0/24,
192.168.254.255 }
}
现在我们可以更改 fail2ban 以向集合中添加元素,而不是为它想要阻止的每个新机器创建一个新规则。更少的规则。更快的处理。
但是,在重新加载防火墙时,我们从端口列表中删除了端口 443。糟糕。不过……如果你对规则感到满意。你只需要运行
$ nft list ruleset > nftables.conf
当你使用所有集合而不是变量时,所有的防火墙规则仍然会看起来不错。
我们的完整防火墙看起来像
table inet filter {
set wanted_tcp_ports {
type inet_service
flags interval
elements = { 22, 443 }
}
set fail2ban_hosts {
type ipv4_addr
flags interval
elements = { 192.168.0.0/24, 192.168.253.0/24,
192.168.254.255 }
}
chain base_checks {
ct state { established, related } accept
ct state invalid drop
}
chain input {
type filter hook input priority filter; policy drop;
iif "lo" accept
jump base_checks
ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply, mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report } accept
ip protocol icmp icmp type { echo-reply, destination-unreachable, echo-request, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept
ip protocol igmp accept
tcp dport @wanted_tcp_ports accept
counter packets 12 bytes 828 log prefix "[nftables] input reject " reject
}
chain forward {
type filter hook forward priority filter; policy accept;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
更多信息请参见 nftables wiki