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 <irbe@tailscale.com>
This commit is contained in:
Irbe Krumina 2024-03-28 17:45:58 +00:00
parent 354cac74a9
commit c2526e2622
5 changed files with 71 additions and 34 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -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

View File

@ -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.