如何使用 nftables 将端口 80 上的请求重定向到 localhost:3000?

sta*_*ter 9 nftables

我希望将到达 192.168.0.1:80 的网络流量重定向到 127.0.0.1:3000。而且,我还希望处理响应的映射。我完整的 NAT 和 Filter 表规则粘贴在下面。

我能够在端口 80 上接收连接。但是,我无法将流量重定向到 localhost:3000。

add table inet filter
add chain inet filter input { type filter hook input priority 0; policy accept; }
add chain inet filter forward { type filter hook forward priority 0; policy accept; }
add chain inet filter output { type filter hook output priority 0; policy accept; }
add rule inet filter input ct state related,established  counter accept
add rule inet filter input ip protocol icmp counter accept
add rule inet filter input iifname "lo" counter accept
add rule inet filter input ct state new  tcp dport 80 counter accept
add rule inet filter input ct state new  tcp dport 4489 counter accept
add rule inet filter input ct state new  tcp dport 8080 counter accept
add rule inet filter input iifname "tun0" ct state new  tcp dport 139 counter accept
add rule inet filter input iifname "tun0" ct state new  tcp dport 445 counter accept
add rule inet filter input ct state new  udp dport 1194 counter accept
add rule inet filter input counter reject with icmp type host-prohibited
add rule inet filter forward counter reject with icmp type host-prohibited
add table nat
add chain nat prerouting { type nat hook prerouting priority -100; }
add chain nat postrouting { type nat hook postrouting priority 100; }
add rule nat prerouting redirect
add rule nat prerouting tcp dport 80 redirect to 3000
add rule nat prerouting iifname eth0 tcp dport { 80, 443 } dnat 127.0.0.1:3000
add rule nat postrouting oifname eth0 snat to 192.168.0.1
Run Code Online (Sandbox Code Playgroud)

A.B*_*A.B 10

我将尝试解决并完成OP自己的工作答案和进一步的评论,其中包括一些剩余的问题:

  • 为什么net.ipv4.conf.eth0.route_localnet=1需要?
  • 为什么eth0需要允许端口 3000而不是lo

还将解决其中的一个小安全问题。

首先,这是有关 Netfilter 和通用网络中数据包流的强制性示意图:

Netfilter 和通用网络中的数据包流

这个原理图是根据iptables制作的,但是nftables可以(并且在大多数默认规则集中)在相同的位置使用相同的钩子。

当数据包到达网络层(IP 第 3 层)时,由各个子系统处理。通常只有路由堆栈,但这里Netfilter为其自身(conntrack,包括初始数据包后的 NAT 处理)或nftables提供钩子。

Netfilter ( conntrack ) 或nftables不关心路由(除非nftables使用与路由相关的专用表达式),它们将其留给路由堆栈:它们操作地址和端口,然后nftables检查可用属性,例如接口、地址和端口。

所以:

  • 新连接中的数据包(因此也穿过ip nat prerouting)从eth0到达(例如)源地址 192.0.2.2 和端口 45678 目的地到地址 192.168.0.1 和端口 80(或 443)。

  • ip nat prerouting dnat规则匹配并告诉netfilter(其conntrack子系统)将目标地址更改为 127.0.0.1,将目标端口更改为 3000。这不会更改数据包的任何其他属性。特别是数据包仍然从eth0到达。

  • 路由堆栈(原理图中的路由决策)不依赖于Netfilter,因此在逻辑上保持独立于它并且不知道之前的更改。它现在必须处理来自 192.0.2.2 和目标 127.0.0.1 的数据包。

    这是一个异常现象:它将允许为环回保留的地址范围在“互联网上”可见,如RFC 1122中所述:

    (G) { 127, <any> }

    内部主机环回地址。这种形式的地址不得出现在主机外部。

    这是在 Linux 内核的路由堆栈中明确处理的:将其视为火星目的地(即:丢弃数据包),除非通过在相关接口上使用来放松。这就是为什么必须针对这种特定情况进行设置。route_localnet=1net.ipv4.conf.eth0.route_localnet=1

  • 同样,下一个nftables规则(这次来自过滤器输入挂钩)看到输入接口仍然为eth0但目标端口现在为 3000 的数据包。因此它必须允许目标端口 3000,并且不再允许 80(或 443)接受它。所以规则应该缩短为:

    iifname "eth0" tcp dport {4489, 3000} counter accept
    
    Run Code Online (Sandbox Code Playgroud)

    因为它永远不会看到来自eth0且目标 tcp 端口为 80 或 443 的数据包:它们在之前的 nat 预路由挂钩中全部更改为端口 3000。此外,为了解释起见,假设看到这样的数据包,它们将被接受,但由于端口 80 或 443 上没有侦听过程(它正在侦听端口 3000),tcp 堆栈将发回 TCP 重置以拒绝连接。

    此外,虽然路由堆栈强制执行 127.0.0.0/8 和lo接口之间的某些关系(使用 进一步放宽route_localnet=1),但正如之前所述,这不涉及netfilternftables,它们不介意任何路由问题。此外,如果是这种情况,对于输入接口来说,这将是未更改的源地址,而不是与输出接口相关的目标地址,该输出接口在输入路径中甚至没有真正的含义:或不能在这里使用。位于过滤器输入挂钩中的事实已经意味着评估的数据包正在到达主机上的本地进程,如原理图所示。oifoifname

    更新:实际上,出于安全原因,应该进一步更改之前给出的规则:允许端口 3000,但不仅仅是目的地 127.0.0.1。因此,与 192.168.0.1:3000 的连接可以接收 TCP RST,这暗示这里有一些特殊的东西,而不是得不到任何回复。为了解决这个问题:

    • 要么使用这个(其中包括看起来非常奇怪的第二条规则):

      iifname "eth0" tcp dport 4489 counter accept
      iifname "eth0" ip daddr 127.0.0.1 tcp dport 3000 counter accept
      
      Run Code Online (Sandbox Code Playgroud)

      因为,route_localnet=1仍然允许同一 192.168.0.0/24 LAN 中的调整系统通过在线路上发送带有 127.0.0.1 的数据包来访问服务,而无需使用 NAT,即使这样做可能没有任何好处。例如其他 Linux 系统,有以下 4 个命令:

      sysctl -w net.ipv4.conf.eth0.route_localnet=1
      ip address delete 127.0.0.1/8 dev lo # can't have 127.0.0.1 also local
      ip route add 127.0.0.1/32 via 192.168.0.1 # via, that way no suspicious ARP *broadcast* for 127.0.0.1 will be seen elsewhere.
      socat tcp4:127.0.0.1:3000 -
      
      Run Code Online (Sandbox Code Playgroud)
    • 或者相反,也保护上述情况,更通用并且是首选:

      iifname "eth0" tcp dport 4489 counter accept
      ct status dnat counter accept
      
      Run Code Online (Sandbox Code Playgroud)
      • 它像以前一样保留不相关的端口 4489/tcp 允许
      • ct status dnat 如果数据包之前已被主机 DNAT 过,则匹配:因此,它将接受任何先前的更改,而不必明确重述它是哪个端口(仍然可以声明它或其他任何内容以进一步缩小所接受的范围):现在端口值 3000不必再明确说明。
      • 因此,它也不允许直接连接到端口 3000,因为这种情况不会被 DNATed。
  • 只是为了完整起见:同样的事情以(不完全)相反的顺序发生在输出和回复中。允许最初生成的从 127.0.0.1 到 192.0.2.2 的传出数据包在输出路径的路由决策net.ipv4.conf.eth0.route_localnet=1中不被视为火星源(=> 丢弃),然后它们才有机会“un-DNATed”回到原始预期源地址(192.168.0.1) 由netfilter ( conntrack ) 单独使用。


当然,使用route_localnet=1是一种放松的安全性(与足够的防火墙规则并不真正相关,但并非所有系统都使用防火墙)并且需要有关其使用的相关知识(例如:在其他地方单独复制nftables规则集将不再起作用,除非route_localnet=1环境)。

既然安全问题已在上面的解释中得到解决(请参阅“更新”),如果允许应用程序侦听 192.168.0.1(或任何地址)而不是仅侦听 127.0.0.1,则可以在不启用的情况下完成等效配置route_localnet=1,通过更改ip nat prerouting

iif eth0 tcp dport { 80, 443 } counter dnat 127.0.0.1:3000
Run Code Online (Sandbox Code Playgroud)

到:

iif eth0 tcp dport { 80, 443 } counter dnat to 192.168.0.1:3000
Run Code Online (Sandbox Code Playgroud)

或者只是为了:

  iif eth0 tcp dport { 80, 443 } counter redirect to :3000
Run Code Online (Sandbox Code Playgroud)

区别不大:redirect将目标更改为接口eth0上主机的主 IP 地址(192.168.0.1),因此大多数情况下行为相同。