Skip to content

Commit

Permalink
util/linuxfw,wgengine/router: skip IPv6 firewall configuration in par…
Browse files Browse the repository at this point in the history
…tial iptables mode (tailscale#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#11540

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
  • Loading branch information
irbekrm committed Mar 29, 2024
1 parent af61179 commit 5fb721d
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 34 deletions.
2 changes: 1 addition & 1 deletion util/linuxfw/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
35 changes: 22 additions & 13 deletions util/linuxfw/iptables_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ type iptablesRunner struct {
ipt4 iptablesInterface
ipt6 iptablesInterface

v6Available bool
v6NATAvailable bool
v6Available bool
v6NATAvailable bool
v6FilterAvailable bool
}

func checkIP6TablesExists() error {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
}
Expand Down
34 changes: 23 additions & 11 deletions util/linuxfw/nftables_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
Expand Down
29 changes: 22 additions & 7 deletions wgengine/router/router_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions wgengine/router/router_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 5fb721d

Please sign in to comment.