diff --git a/util/linuxfw/iptables_runner.go b/util/linuxfw/iptables_runner.go index 9211bbd16..c5d915cb7 100644 --- a/util/linuxfw/iptables_runner.go +++ b/util/linuxfw/iptables_runner.go @@ -6,8 +6,10 @@ package linuxfw import ( + "bytes" "fmt" "net/netip" + "os" "os/exec" "strconv" "strings" @@ -59,6 +61,7 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) { supportsV6, supportsV6NAT := false, false v6err := checkIPv6(logf) ip6terr := checkIP6TablesExists() + var ipt6 *iptables.IPTables switch { case v6err != nil: logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err) @@ -66,20 +69,54 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) { logf("disabling tunneled IPv6 due to missing ip6tables: %v", ip6terr) default: supportsV6 = true - supportsV6NAT = supportsV6 && checkSupportsV6NAT() - logf("v6nat = %v", supportsV6NAT) - } - - var ipt6 *iptables.IPTables - if supportsV6 { ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6) if err != nil { return nil, err } + supportsV6NAT = checkSupportsV6NAT(ipt6, logf) + logf("v6nat = %v", supportsV6NAT) } return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil } +// checkSupportsV6NAT returns whether the system has a "nat" table in the +// IPv6 netfilter stack. +// +// The nat table was added after the initial release of ipv6 +// netfilter, so some older distros ship a kernel that can't NAT IPv6 +// traffic. +// ipt must be initialized for IPv6. +func checkSupportsV6NAT(ipt *iptables.IPTables, logf logger.Logf) bool { + if ipt == nil || ipt.Proto() != iptables.ProtocolIPv6 { + return false + } + natListErr, _ := ipt.ListChains("nat") + if natListErr == nil { + return true + } + + // TODO (irbekrm): the following two checks were added before the check + // above that verifies that nat chains can be listed. It is a + // container-friendly check (see + // https://github.com/tailscale/tailscale/issues/11344), but also should + // be good enough on its own in other environments. If we never observe + // it falsely succeed, let's remove the other two checks. + + bs, err := os.ReadFile("/proc/net/ip6_tables_names") + if err != nil { + return false + } + if bytes.Contains(bs, []byte("nat\n")) { + logf("[unexpected] listing nat chains failed, but /proc/net/ip6_tables_name reports a nat table existing") + return true + } + if exec.Command("modprobe", "ip6table_nat").Run() == nil { + logf("[unexpected] listing nat chains failed, but modprobe ip6table_nat succeeded") + return true + } + return false +} + // HasIPV6 reports true if the system supports IPv6. func (i *iptablesRunner) HasIPV6() bool { return i.v6Available diff --git a/util/linuxfw/linuxfw.go b/util/linuxfw/linuxfw.go index e381e1f52..152d2eb07 100644 --- a/util/linuxfw/linuxfw.go +++ b/util/linuxfw/linuxfw.go @@ -8,7 +8,6 @@ package linuxfw import ( - "bytes" "errors" "fmt" "os" @@ -170,28 +169,6 @@ func checkIPv6(logf logger.Logf) error { return nil } -// checkSupportsV6NAT returns whether the system has a "nat" table in the -// IPv6 netfilter stack. -// -// The nat table was added after the initial release of ipv6 -// netfilter, so some older distros ship a kernel that can't NAT IPv6 -// traffic. -func checkSupportsV6NAT() bool { - bs, err := os.ReadFile("/proc/net/ip6_tables_names") - if err != nil { - // Can't read the file. Assume SNAT works. - return true - } - if bytes.Contains(bs, []byte("nat\n")) { - return true - } - // In nftables mode, that proc file will be empty. Try another thing: - if exec.Command("modprobe", "ip6table_nat").Run() == nil { - return true - } - return false -} - func CheckIPRuleSupportsV6(logf logger.Logf) error { // First try just a read-only operation to ideally avoid // having to modify any state. diff --git a/util/linuxfw/nftables_runner.go b/util/linuxfw/nftables_runner.go index bca882fce..18d7bd5ae 100644 --- a/util/linuxfw/nftables_runner.go +++ b/util/linuxfw/nftables_runner.go @@ -551,12 +551,15 @@ func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) { logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err) } supportsV6 := v6err == nil - supportsV6NAT := supportsV6 && checkSupportsV6NAT() - var nft6 *nftable + if supportsV6 { - logf("v6nat availability: %v", supportsV6NAT) nft6 = &nftable{Proto: nftables.TableFamilyIPv6} + // Kernel support for nftables was added after support for IPv6 + // NAT, so no need for a separate IPv6 NAT support check. + // https://tldp.org/HOWTO/Linux+IPv6-HOWTO/ch18s04.html + // https://wiki.nftables.org/wiki-nftables/index.php/Building_and_installing_nftables_from_sources + logf("v6nat availability: true") } // TODO(KevinLiang10): convert iptables rule to nftable rules if they exist in the iptables @@ -566,7 +569,7 @@ func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) { nft4: nft4, nft6: nft6, v6Available: supportsV6, - v6NATAvailable: supportsV6NAT, + v6NATAvailable: supportsV6, // if nftables are supported, IPv6 NAT is supported }, nil }