From 4c593412fbaba4f6c113226c9225d107f5f233ca Mon Sep 17 00:00:00 2001 From: HaizakiKu Date: Sun, 14 Jun 2026 03:17:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(firewall):=20Fix=20nftables=20redirect=20fa?= =?UTF-8?q?ilure=20when=20binding=20to=20a=20specific=20IPv6=20address=20/?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=20nftables=20=E5=9C=A8=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E7=89=B9=E5=AE=9A=20IPv6=20=E5=9C=B0=E5=9D=80=E6=97=B6?= =?UTF-8?q?=E9=87=8D=E5=AE=9A=E5=90=91=E5=A4=B1=E6=95=88=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#1598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(firewall): 修复 nftables 在绑定特定 IPv6 地址时重定向失效的问题 * fix(firewall): use DNAT for specific IPv6 bind in iptables port redirect * chore: format fix --------- Co-authored-by: Toby --- app/internal/firewall/firewall_linux.go | 22 ++- app/internal/firewall/firewall_linux_test.go | 134 +++++++++++++++++++ 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/app/internal/firewall/firewall_linux.go b/app/internal/firewall/firewall_linux.go index d841617..accb16e 100644 --- a/app/internal/firewall/firewall_linux.go +++ b/app/internal/firewall/firewall_linux.go @@ -111,7 +111,13 @@ func setupNFTablesRedirect(r commandRunner, listenAddr *net.UDPAddr, ports, redi if match := nftDestinationMatch(family, listenAddr); match != nil { args = append(args, match...) } - args = append(args, "udp", "dport", nftPortExpr(portRange), "redirect", "to", fmt.Sprintf(":%d", ports[0].Start)) + if family == "ip6" && listenAddr.IP != nil && !listenAddr.IP.IsUnspecified() { + args = append(args, "udp", "dport", nftPortExpr(portRange), "dnat", "to", + fmt.Sprintf("[%s]:%d", listenAddr.IP.String(), ports[0].Start)) + } else { + args = append(args, "udp", "dport", nftPortExpr(portRange), "redirect", "to", + fmt.Sprintf(":%d", ports[0].Start)) + } if err := nft(args...); err != nil { _ = cleanup.Close() return nil, err @@ -142,7 +148,19 @@ func setupIPTablesRedirect(r commandRunner, listenAddr *net.UDPAddr, ports, redi _ = ipt("-t", "nat", "-F", chainName) _ = ipt("-t", "nat", "-X", chainName) }) - if err := ipt("-t", "nat", "-A", chainName, "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", ports[0].Start)); err != nil { + var targetArgs []string + if bin == "ip6tables" && listenAddr.IP != nil && !listenAddr.IP.IsUnspecified() { + targetArgs = []string{ + "-t", "nat", "-A", chainName, "-p", "udp", "-j", "DNAT", + "--to-destination", fmt.Sprintf("[%s]:%d", listenAddr.IP.String(), ports[0].Start), + } + } else { + targetArgs = []string{ + "-t", "nat", "-A", chainName, "-p", "udp", "-j", "REDIRECT", + "--to-ports", fmt.Sprintf("%d", ports[0].Start), + } + } + if err := ipt(targetArgs...); err != nil { _ = cleanup.Close() return nil, err } diff --git a/app/internal/firewall/firewall_linux_test.go b/app/internal/firewall/firewall_linux_test.go index f6003ce..fc46f97 100644 --- a/app/internal/firewall/firewall_linux_test.go +++ b/app/internal/firewall/firewall_linux_test.go @@ -78,6 +78,140 @@ func TestSetupUDPPortRedirectWithRunnerIPTablesFallback(t *testing.T) { require.True(t, foundDelete) } +func TestSetupUDPPortRedirectWithRunnerNFTablesIPv6Specific(t *testing.T) { + runner := &fakeRunner{paths: map[string]bool{"nft": true}} + addr := &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 20000} + ports := eUtils.PortUnion{{20000, 20002}} + + cleanup, err := setupUDPPortRedirectWithRunner(runner, addr, ports) + require.NoError(t, err) + require.NotNil(t, cleanup) + + for _, cmd := range runner.cmds { + hasUDP := false + for _, arg := range cmd { + if arg == "udp" { + hasUDP = true + break + } + } + if !hasUDP { + continue + } + hasDnat := false + hasRedirect := false + for _, arg := range cmd { + if arg == "dnat" { + hasDnat = true + } + if arg == "redirect" { + hasRedirect = true + } + } + require.True(t, hasDnat, "IPv6 specific address rule should use dnat: %v", cmd) + require.False(t, hasRedirect, "IPv6 specific address rule must not use redirect: %v", cmd) + require.Contains(t, cmd, "[2001:db8::1]:20000") + } + + require.NoError(t, cleanup.Close()) +} + +func TestSetupUDPPortRedirectWithRunnerNFTablesIPv6Unspecified(t *testing.T) { + runner := &fakeRunner{paths: map[string]bool{"nft": true}} + addr := &net.UDPAddr{IP: net.IPv6unspecified, Port: 20000} + ports := eUtils.PortUnion{{20000, 20002}} + + cleanup, err := setupUDPPortRedirectWithRunner(runner, addr, ports) + require.NoError(t, err) + require.NotNil(t, cleanup) + + for _, cmd := range runner.cmds { + hasDnat := false + for _, arg := range cmd { + if arg == "dnat" { + hasDnat = true + break + } + } + require.False(t, hasDnat, "IPv6 unspecified address rule must not use dnat: %v", cmd) + } + + require.NoError(t, cleanup.Close()) +} + +func TestSetupUDPPortRedirectWithRunnerIPTablesIPv6Specific(t *testing.T) { + runner := &fakeRunner{paths: map[string]bool{"ip6tables": true}} + addr := &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 20000} + ports := eUtils.PortUnion{{20000, 20002}} + + cleanup, err := setupUDPPortRedirectWithRunner(runner, addr, ports) + require.NoError(t, err) + require.NotNil(t, cleanup) + + foundTarget := false + for _, cmd := range runner.cmds { + hasDNAT := false + hasREDIRECT := false + for _, arg := range cmd { + if arg == "DNAT" { + hasDNAT = true + } + if arg == "REDIRECT" { + hasREDIRECT = true + } + } + require.False(t, hasREDIRECT, "IPv6 specific address rule must not use REDIRECT: %v", cmd) + if hasDNAT { + foundTarget = true + require.Contains(t, cmd, "[2001:db8::1]:20000") + } + } + require.True(t, foundTarget, "expected a DNAT rule for specific IPv6 bind") + + require.NoError(t, cleanup.Close()) +} + +func TestSetupUDPPortRedirectWithRunnerIPTablesIPv6Unspecified(t *testing.T) { + runner := &fakeRunner{paths: map[string]bool{"iptables": true, "ip6tables": true}} + addr := &net.UDPAddr{IP: net.IPv6unspecified, Port: 20000} + ports := eUtils.PortUnion{{20000, 20002}} + + cleanup, err := setupUDPPortRedirectWithRunner(runner, addr, ports) + require.NoError(t, err) + require.NotNil(t, cleanup) + + for _, cmd := range runner.cmds { + for _, arg := range cmd { + require.NotEqual(t, "DNAT", arg, "unspecified address rule must not use DNAT: %v", cmd) + } + } + + require.NoError(t, cleanup.Close()) +} + +func TestSetupUDPPortRedirectWithRunnerIPTablesIPv4Specific(t *testing.T) { + runner := &fakeRunner{paths: map[string]bool{"iptables": true}} + addr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 20000} + ports := eUtils.PortUnion{{20000, 20002}} + + cleanup, err := setupUDPPortRedirectWithRunner(runner, addr, ports) + require.NoError(t, err) + require.NotNil(t, cleanup) + + foundRedirect := false + for _, cmd := range runner.cmds { + for _, arg := range cmd { + require.NotEqual(t, "DNAT", arg, "IPv4 rule must keep REDIRECT, not DNAT: %v", cmd) + if arg == "REDIRECT" { + foundRedirect = true + } + } + } + require.True(t, foundRedirect, "expected REDIRECT rule for IPv4 bind") + + require.NoError(t, cleanup.Close()) +} + func TestSetupUDPPortRedirectWithRunnerRollback(t *testing.T) { runner := &fakeRunner{ paths: map[string]bool{"iptables": true},