net/netutil: check actual IP forwarding sysctls on FreeBSD

CheckIPForwarding unconditionally returned a "not currently officially
supported" warning on FreeBSD without ever reading the forwarding
sysctls. Subnet routers on FreeBSD therefore got a spurious "IP
forwarding is disabled" health warning and admin console banner even
with net.inet.ip.forwarding=1 and net.inet6.ip6.forwarding=1 set.

Read the sysctls instead, and only warn for the protocols actually
required by the advertised routes. FreeBSD has no per-interface
forwarding knob, so only the global sysctls are checked.

dragonfly, netbsd and openbsd keep the previous unsupported warning.

Updates #5573

Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
This commit is contained in:
Martin Minkus authored and Brad Fitzpatrick committed 2026-08-31 19:15:51 -07:00
1 parent 650bb0546a
commit 58f28a1927
2 files changed
+121 -1

No files matched your search

+61 -1
View File
@@ -60,7 +60,12 @@ func protocolsRequiredForForwarding(routes []netip.Prefix, state *netmon.State)
func CheckIPForwarding(routes []netip.Prefix, state *netmon.State) (warn, err error) {
if runtime.GOOS != "linux" {
switch runtime.GOOS {
case "dragonfly", "freebsd", "netbsd", "openbsd":
case "freebsd":
if state == nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration; no link state")
}
return checkIPForwardingFreeBSD(routes, state)
case "dragonfly", "netbsd", "openbsd":
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS), nil
case "illumos", "solaris":
_, err := ipForwardingEnabledSunOS(ipv4, "")
@@ -235,6 +240,61 @@ func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
return on, nil
}
// checkIPForwardingFreeBSD reports whether IP forwarding is enabled for the
// protocols required by routes.
//
// Unlike Linux, FreeBSD has no per-interface forwarding knob: net.inet.ip.forwarding
// and net.inet6.ip6.forwarding are global, so we only check those.
func checkIPForwardingFreeBSD(routes []netip.Prefix, state *netmon.State) (warn, err error) {
const kbLink = "\nSee https://tailscale.com/s/ip-forwarding"
wantV4, wantV6 := protocolsRequiredForForwarding(routes, state)
if !wantV4 && !wantV6 {
return nil, nil
}
var warnings []string
if wantV4 {
on, err := ipForwardingEnabledFreeBSD(ipv4)
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if !on {
warnings = append(warnings, "IPv4 forwarding is disabled.")
}
}
if wantV6 {
on, err := ipForwardingEnabledFreeBSD(ipv6)
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if !on {
warnings = append(warnings, "IPv6 forwarding is disabled.")
}
}
if len(warnings) > 0 {
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink), nil
}
return nil, nil
}
// ipForwardingEnabledFreeBSD reports whether IP forwarding is enabled globally
// for the given protocol.
func ipForwardingEnabledFreeBSD(p protocol) (bool, error) {
k := "net.inet.ip.forwarding"
if p == ipv6 {
k = "net.inet6.ip6.forwarding"
}
bs, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
return false, fmt.Errorf("couldn't check sysctl %s: %w", k, err)
}
val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
if err != nil {
return false, fmt.Errorf("couldn't parse %s: %w", k, err)
}
return val == 1, nil
}
func ipForwardingEnabledSunOS(p protocol, iface string) (bool, error) {
var proto string
if p == ipv4 {
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netutil
import (
"net/netip"
"testing"
"tailscale.com/net/netmon"
)
func TestProtocolsRequiredForForwarding(t *testing.T) {
state := &netmon.State{
InterfaceIPs: map[string][]netip.Prefix{
"em0": {netip.MustParsePrefix("192.168.1.5/24")},
},
}
tests := []struct {
name string
routes []netip.Prefix
wantV4 bool
wantV6 bool
}{
{"none", nil, false, false},
{"v4only", []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}, true, false},
{"v6only", []netip.Prefix{netip.MustParsePrefix("2000::/3")}, false, true},
{
"both",
[]netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("2000::/3")},
true, true,
},
{
// Advertising a route to one of our own local IPs doesn't
// require forwarding, so it must not trigger a warning.
"local single IP",
[]netip.Prefix{netip.MustParsePrefix("192.168.1.5/32")},
false, false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v4, v6 := protocolsRequiredForForwarding(tt.routes, state)
if v4 != tt.wantV4 || v6 != tt.wantV6 {
t.Errorf("got v4=%v v6=%v, want v4=%v v6=%v", v4, v6, tt.wantV4, tt.wantV6)
}
})
}
}
// TestCheckIPForwardingFreeBSDNoRoutes verifies we don't shell out to sysctl
// (and so can't warn) when there are no routes needing forwarding. This is the
// path taken by a FreeBSD node that isn't a subnet router.
func TestCheckIPForwardingFreeBSDNoRoutes(t *testing.T) {
state := &netmon.State{}
warn, err := checkIPForwardingFreeBSD(nil, state)
if warn != nil || err != nil {
t.Fatalf("got warn=%v err=%v, want both nil", warn, err)
}
}