util/linuxfw,wgengine/router: skip IPv6 firewall configuration in partial iptables mode (#11546)

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 <irbe@tailscale.com>
pull/11560/head
Irbe Krumina 8 months ago committed by GitHub
parent af61179c2f
commit 5fb721d4ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -121,6 +121,6 @@ func NewFakeIPTablesRunner() *iptablesRunner {
ipt4 := newFakeIPTables() ipt4 := newFakeIPTables()
ipt6 := newFakeIPTables() ipt6 := newFakeIPTables()
iptr := &iptablesRunner{ipt4, ipt6, true, true} iptr := &iptablesRunner{ipt4, ipt6, true, true, true}
return iptr return iptr
} }

@ -38,6 +38,7 @@ type iptablesRunner struct {
v6Available bool v6Available bool
v6NATAvailable bool v6NATAvailable bool
v6FilterAvailable bool
} }
func checkIP6TablesExists() error { func checkIP6TablesExists() error {
@ -58,7 +59,7 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
return nil, err return nil, err
} }
supportsV6, supportsV6NAT := false, false supportsV6, supportsV6NAT, supportsV6Filter := false, false, false
v6err := CheckIPv6(logf) v6err := CheckIPv6(logf)
ip6terr := checkIP6TablesExists() ip6terr := checkIP6TablesExists()
var ipt6 *iptables.IPTables var ipt6 *iptables.IPTables
@ -73,20 +74,23 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
supportsV6 = checkSupportsV6Filter(ipt6, logf) supportsV6Filter = checkSupportsV6Filter(ipt6, logf)
if supportsV6 {
supportsV6NAT = checkSupportsV6NAT(ipt6, logf) supportsV6NAT = checkSupportsV6NAT(ipt6, logf)
logf("v6 = %v, v6filter = %v, v6nat = %v", supportsV6, supportsV6Filter, supportsV6NAT)
} }
logf("v6filter = %v, v6nat = %v", supportsV6, supportsV6NAT) return &iptablesRunner{
} ipt4: ipt4,
return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil ipt6: ipt6,
v6Available: supportsV6,
v6NATAvailable: supportsV6NAT,
v6FilterAvailable: supportsV6Filter}, nil
} }
// checkSupportsV6Filter returns whether the system has a "filter" table in the // checkSupportsV6Filter returns whether the system has a "filter" table in the
// IPv6 tables. Some container environments such as GitHub codespaces have // IPv6 tables. Some container environments such as GitHub codespaces have
// limited local IPv6 support, and containers containing ip6tables, but do not // limited local IPv6 support, and containers containing ip6tables, but do not
// have kernel support for IPv6 filtering. // 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 { func checkSupportsV6Filter(ipt *iptables.IPTables, logf logger.Logf) bool {
if ipt == nil { if ipt == nil {
return false return false
@ -95,7 +99,7 @@ func checkSupportsV6Filter(ipt *iptables.IPTables, logf logger.Logf) bool {
if filterListErr == nil { if filterListErr == nil {
return true 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 return false
} }
@ -142,6 +146,11 @@ func (i *iptablesRunner) HasIPV6() bool {
return i.v6Available 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. // HasIPV6NAT reports true if the system supports IPv6 NAT.
func (i *iptablesRunner) HasIPV6NAT() bool { func (i *iptablesRunner) HasIPV6NAT() bool {
return i.v6NATAvailable return i.v6NATAvailable
@ -189,7 +198,7 @@ func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error {
// getTables gets the available iptablesInterface in iptables runner. // getTables gets the available iptablesInterface in iptables runner.
func (i *iptablesRunner) getTables() []iptablesInterface { func (i *iptablesRunner) getTables() []iptablesInterface {
if i.HasIPV6() { if i.HasIPV6Filter() {
return []iptablesInterface{i.ipt4, i.ipt6} return []iptablesInterface{i.ipt4, i.ipt6}
} }
return []iptablesInterface{i.ipt4} return []iptablesInterface{i.ipt4}
@ -286,7 +295,7 @@ func (i *iptablesRunner) AddBase(tunname string) error {
if err := i.addBase4(tunname); err != nil { if err := i.addBase4(tunname); err != nil {
return err return err
} }
if i.HasIPV6() { if i.HasIPV6Filter() {
if err := i.addBase6(tunname); err != nil { if err := i.addBase6(tunname); err != nil {
return err return err
} }

@ -488,6 +488,12 @@ type NetfilterRunner interface {
// HasIPV6NAT reports true if the system supports IPv6 NAT. // HasIPV6NAT reports true if the system supports IPv6 NAT.
HasIPV6NAT() bool 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 // AddDNATRule adds a rule to the nat/PREROUTING chain to DNAT traffic
// destined for the given original destination to the given new destination. // destined for the given original destination to the given new destination.
// This is used to forward all traffic destined for the Tailscale interface // This is used to forward all traffic destined for the Tailscale interface
@ -555,10 +561,6 @@ func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
if supportsV6 { if supportsV6 {
nft6 = &nftable{Proto: nftables.TableFamilyIPv6} 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") logf("v6nat availability: true")
} }
@ -569,7 +571,6 @@ func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
nft4: nft4, nft4: nft4,
nft6: nft6, nft6: nft6,
v6Available: supportsV6, v6Available: supportsV6,
v6NATAvailable: supportsV6, // if nftables are supported, IPv6 NAT is supported
}, nil }, nil
} }
@ -612,9 +613,20 @@ func (n *nftablesRunner) HasIPV6() bool {
return n.v6Available 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 { 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. // findRule iterates through the rules to find the rule with matching expressions.

@ -419,7 +419,12 @@ func (r *linuxRouter) UpdateMagicsockPort(port uint16, network string) error {
case "udp4": case "udp4":
magicsockPort = &r.magicsockPortV4 magicsockPort = &r.magicsockPortV4
case "udp6": 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 return nil
} }
magicsockPort = &r.magicsockPortV6 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) 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 { if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV6, "udp6"); err != nil {
return fmt.Errorf("could not add magicsock port rule v6: %w", err) 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) 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 { if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV6, "udp6"); err != nil {
return fmt.Errorf("could not add magicsock port rule v6: %w", err) 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 { for cidr := range r.addrs {
if err := r.addLoopbackRule(cidr.Addr()); err != nil { if err := r.addLoopbackRule(cidr.Addr()); err != nil {
return err return fmt.Errorf("error adding loopback rule: %w", err)
} }
} }
return nil 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 { func (r *linuxRouter) getV6Available() bool {
if r.netfilterMode == netfilterOff {
return r.v6Available
}
return r.nfr.HasIPV6() return r.nfr.HasIPV6()
} }
@ -669,6 +678,9 @@ func (r *linuxRouter) addLoopbackRule(addr netip.Addr) error {
if r.netfilterMode == netfilterOff { if r.netfilterMode == netfilterOff {
return nil return nil
} }
if addr.Is6() && !r.nfr.HasIPV6Filter() {
return nil
}
if err := r.nfr.AddLoopbackRule(addr); err != nil { if err := r.nfr.AddLoopbackRule(addr); err != nil {
return err return err
@ -682,6 +694,9 @@ func (r *linuxRouter) delLoopbackRule(addr netip.Addr) error {
if r.netfilterMode == netfilterOff { if r.netfilterMode == netfilterOff {
return nil return nil
} }
if addr.Is6() && !r.nfr.HasIPV6Filter() {
return nil
}
if err := r.nfr.DelLoopbackRule(addr); err != nil { if err := r.nfr.DelLoopbackRule(addr); err != nil {
return err return err

@ -661,6 +661,7 @@ func (n *fakeIPTablesRunner) DelMagicsockPortRule(port uint16, network string) e
func (n *fakeIPTablesRunner) HasIPV6() bool { return true } func (n *fakeIPTablesRunner) HasIPV6() bool { return true }
func (n *fakeIPTablesRunner) HasIPV6NAT() 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 // fakeOS implements commandRunner and provides v4 and v6
// netfilterRunners, but captures changes without touching the OS. // netfilterRunners, but captures changes without touching the OS.

Loading…
Cancel
Save