From c2526e262232ce668ff52a24735324711850301c Mon Sep 17 00:00:00 2001 From: Irbe Krumina Date: Thu, 28 Mar 2024 17:45:58 +0000 Subject: [PATCH] util/linuxfw,wgengine/router: skip IPv6 firewall configuration in partial iptables mode We have hosts that support IPv6, but not IPv6 firewall configuration in iptables mode. We also have hosts that have some support for IPv6 firewall configuration in iptables mode, but do not have iptables filter table. We should: - configure ip rules for all hosts that support IPv6 - only configure firewall rules in iptables mode if the host has iptables filter table. Updates tailscale/tailscale#11540 Signed-off-by: Irbe Krumina --- util/linuxfw/fake.go | 2 +- util/linuxfw/iptables_runner.go | 35 +++++++++++++++++----------- util/linuxfw/nftables_runner.go | 34 ++++++++++++++++++--------- wgengine/router/router_linux.go | 29 +++++++++++++++++------ wgengine/router/router_linux_test.go | 5 ++-- 5 files changed, 71 insertions(+), 34 deletions(-) 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.