diff --git a/util/linuxfw/fake.go b/util/linuxfw/fake.go index e76431d00..8fd26dca7 100644 --- a/util/linuxfw/fake.go +++ b/util/linuxfw/fake.go @@ -121,6 +121,6 @@ func NewFakeIPTablesRunner() *iptablesRunner { ipt4 := newFakeIPTables() ipt6 := newFakeIPTables() - iptr := &iptablesRunner{ipt4, ipt6, true, true} + iptr := &iptablesRunner{ipt4, ipt6, true, true, true} return iptr } diff --git a/util/linuxfw/iptables_runner.go b/util/linuxfw/iptables_runner.go index 196046ce6..f5996346d 100644 --- a/util/linuxfw/iptables_runner.go +++ b/util/linuxfw/iptables_runner.go @@ -36,8 +36,9 @@ type iptablesRunner struct { ipt4 iptablesInterface ipt6 iptablesInterface - v6Available bool - v6NATAvailable bool + v6Available bool + v6NATAvailable bool + v6FilterAvailable bool } func checkIP6TablesExists() error { @@ -58,7 +59,7 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) { return nil, err } - supportsV6, supportsV6NAT := false, false + supportsV6, supportsV6NAT, supportsV6Filter := false, false, false v6err := CheckIPv6(logf) ip6terr := checkIP6TablesExists() var ipt6 *iptables.IPTables @@ -73,20 +74,23 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) { if err != nil { return nil, err } - supportsV6 = checkSupportsV6Filter(ipt6, logf) - if supportsV6 { - supportsV6NAT = checkSupportsV6NAT(ipt6, logf) - } - logf("v6filter = %v, v6nat = %v", supportsV6, supportsV6NAT) + supportsV6Filter = checkSupportsV6Filter(ipt6, logf) + supportsV6NAT = checkSupportsV6NAT(ipt6, logf) + logf("v6 = %v, v6filter = %v, v6nat = %v", supportsV6, supportsV6Filter, supportsV6NAT) } - return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil + return &iptablesRunner{ + ipt4: ipt4, + ipt6: ipt6, + v6Available: supportsV6, + v6NATAvailable: supportsV6NAT, + v6FilterAvailable: supportsV6Filter}, nil } // checkSupportsV6Filter returns whether the system has a "filter" table in the // IPv6 tables. Some container environments such as GitHub codespaces have // limited local IPv6 support, and containers containing ip6tables, but do not // have kernel support for IPv6 filtering. -// We will not enable IPv6 in these instances. +// We will not set ip6tables rules in these instances. func checkSupportsV6Filter(ipt *iptables.IPTables, logf logger.Logf) bool { if ipt == nil { return false @@ -95,7 +99,7 @@ func checkSupportsV6Filter(ipt *iptables.IPTables, logf logger.Logf) bool { if filterListErr == nil { return true } - logf("ipv6 unavailable due to missing filter table: %s", filterListErr) + logf("ip6tables filtering is not supported on this host: %v", filterListErr) return false } @@ -142,6 +146,11 @@ func (i *iptablesRunner) HasIPV6() bool { return i.v6Available } +// HasIPV6Filter reports true if the system supports ip6tables filter table. +func (i *iptablesRunner) HasIPV6Filter() bool { + return i.v6FilterAvailable +} + // HasIPV6NAT reports true if the system supports IPv6 NAT. func (i *iptablesRunner) HasIPV6NAT() bool { return i.v6NATAvailable @@ -189,7 +198,7 @@ func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error { // getTables gets the available iptablesInterface in iptables runner. func (i *iptablesRunner) getTables() []iptablesInterface { - if i.HasIPV6() { + if i.HasIPV6Filter() { return []iptablesInterface{i.ipt4, i.ipt6} } return []iptablesInterface{i.ipt4} @@ -286,7 +295,7 @@ func (i *iptablesRunner) AddBase(tunname string) error { if err := i.addBase4(tunname); err != nil { return err } - if i.HasIPV6() { + if i.HasIPV6Filter() { if err := i.addBase6(tunname); err != nil { return err } diff --git a/util/linuxfw/nftables_runner.go b/util/linuxfw/nftables_runner.go index e3dc2ec0a..ec9ef1ae5 100644 --- a/util/linuxfw/nftables_runner.go +++ b/util/linuxfw/nftables_runner.go @@ -488,6 +488,12 @@ type NetfilterRunner interface { // HasIPV6NAT reports true if the system supports IPv6 NAT. HasIPV6NAT() bool + // HasIPV6Filter reports true if the system supports IPv6 filter tables + // This is only meaningful for iptables implementation, where hosts have + // partial ipables support (i.e missing filter table). For nftables + // implementation, this will default to the value of HasIPv6(). + HasIPV6Filter() bool + // AddDNATRule adds a rule to the nat/PREROUTING chain to DNAT traffic // destined for the given original destination to the given new destination. // This is used to forward all traffic destined for the Tailscale interface @@ -555,21 +561,16 @@ func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) { if supportsV6 { 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 return &nftablesRunner{ - conn: conn, - nft4: nft4, - nft6: nft6, - v6Available: supportsV6, - v6NATAvailable: supportsV6, // if nftables are supported, IPv6 NAT is supported + conn: conn, + nft4: nft4, + nft6: nft6, + v6Available: supportsV6, }, nil } @@ -612,9 +613,20 @@ func (n *nftablesRunner) HasIPV6() bool { return n.v6Available } -// HasIPV6NAT returns true if the system supports IPv6 NAT. +// HasIPV6NAT returns true if the system supports IPv6. +// Kernel support for nftables was added after support for IPv6 +// NAT, so no need for a separate IPv6 NAT support check like we do for iptables. +// https://tldp.org/HOWTO/Linux+IPv6-HOWTO/ch18s04.html +// https://wiki.nftables.org/wiki-nftables/index.php/Building_and_installing_nftables_from_sources func (n *nftablesRunner) HasIPV6NAT() bool { - return n.v6NATAvailable + return n.v6Available +} + +// HasIPV6Filter returns true if system supports IPv6. There are no known edge +// cases where nftables running on a host that supports IPv6 would not support +// filter table. +func (n *nftablesRunner) HasIPV6Filter() bool { + return n.v6Available } // findRule iterates through the rules to find the rule with matching expressions. diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index 2a1443108..ebc83e9ca 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -419,7 +419,12 @@ func (r *linuxRouter) UpdateMagicsockPort(port uint16, network string) error { case "udp4": magicsockPort = &r.magicsockPortV4 case "udp6": - if !r.getV6Available() { + // Skip setting up MagicSock port if the host does not support + // IPv6. MagicSock IPv6 port needs a filter rule to function. In + // some cases (hosts with partial iptables support) filter + // tables are not supported, so skip setting up the port for + // those hosts too. + if !r.getV6FilteringAvailable() { return nil } magicsockPort = &r.magicsockPortV6 @@ -526,7 +531,7 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error { return fmt.Errorf("could not add magicsock port rule v4: %w", err) } } - if r.magicsockPortV6 != 0 && r.getV6Available() { + if r.magicsockPortV6 != 0 && r.getV6FilteringAvailable() { if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV6, "udp6"); err != nil { return fmt.Errorf("could not add magicsock port rule v6: %w", err) } @@ -566,7 +571,7 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error { return fmt.Errorf("could not add magicsock port rule v4: %w", err) } } - if r.magicsockPortV6 != 0 && r.getV6Available() { + if r.magicsockPortV6 != 0 && r.getV6FilteringAvailable() { if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV6, "udp6"); err != nil { return fmt.Errorf("could not add magicsock port rule v6: %w", err) } @@ -597,17 +602,21 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error { for cidr := range r.addrs { if err := r.addLoopbackRule(cidr.Addr()); err != nil { - return err + return fmt.Errorf("error adding loopback rule: %w", err) } } return nil } +// getV6FilteringAvailable returns true if the router is able to setup the +// required tailscale filter rules for IPv6. +func (r *linuxRouter) getV6FilteringAvailable() bool { + return r.nfr.HasIPV6() && r.nfr.HasIPV6Filter() +} + +// getV6Available returns true if the host supports IPv6. func (r *linuxRouter) getV6Available() bool { - if r.netfilterMode == netfilterOff { - return r.v6Available - } return r.nfr.HasIPV6() } @@ -669,6 +678,9 @@ func (r *linuxRouter) addLoopbackRule(addr netip.Addr) error { if r.netfilterMode == netfilterOff { return nil } + if addr.Is6() && !r.nfr.HasIPV6Filter() { + return nil + } if err := r.nfr.AddLoopbackRule(addr); err != nil { return err @@ -682,6 +694,9 @@ func (r *linuxRouter) delLoopbackRule(addr netip.Addr) error { if r.netfilterMode == netfilterOff { return nil } + if addr.Is6() && !r.nfr.HasIPV6Filter() { + return nil + } if err := r.nfr.DelLoopbackRule(addr); err != nil { return err diff --git a/wgengine/router/router_linux_test.go b/wgengine/router/router_linux_test.go index b86469f6d..fcf12b03e 100644 --- a/wgengine/router/router_linux_test.go +++ b/wgengine/router/router_linux_test.go @@ -659,8 +659,9 @@ func (n *fakeIPTablesRunner) DelMagicsockPortRule(port uint16, network string) e return nil } -func (n *fakeIPTablesRunner) HasIPV6() bool { return true } -func (n *fakeIPTablesRunner) HasIPV6NAT() bool { return true } +func (n *fakeIPTablesRunner) HasIPV6() bool { return true } +func (n *fakeIPTablesRunner) HasIPV6NAT() bool { return true } +func (n *fakeIPTablesRunner) HasIPV6Filter() bool { return true } // fakeOS implements commandRunner and provides v4 and v6 // netfilterRunners, but captures changes without touching the OS.