diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index da371f19a..e19572187 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -5,7 +5,12 @@ package router import ( + "bytes" "fmt" + "io/ioutil" + "os" + "os/exec" + "strconv" "strings" "github.com/coreos/go-iptables/iptables" @@ -79,13 +84,17 @@ type netfilterRunner interface { type linuxRouter struct { logf func(fmt string, args ...interface{}) - ipRuleAvailable bool tunname string addrs map[netaddr.IPPrefix]bool routes map[netaddr.IPPrefix]bool snatSubnetRoutes bool netfilterMode NetfilterMode + // Various feature checks for the network stack. + ipRuleAvailable bool + v6Available bool + v6NATAvailable bool + dns *dns.Manager ipt4 netfilterRunner @@ -104,9 +113,14 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) ( return nil, err } - ipt6, err := iptables.NewWithProtocol(iptables.ProtocolIPv6) - if err != nil { - return nil, err + var ipt6 netfilterRunner + if supportsV6() { + // The iptables package probes for `ip6tables` and errors out + // if unavailable. We want that to be a non-fatal error. + ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6) + if err != nil { + return nil, err + } } return newUserspaceRouterAdvanced(logf, tunname, ipt4, ipt6, osCommandRunner{}) @@ -120,15 +134,21 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter4, ne InterfaceName: tunname, } + supportsV6 := supportsV6() + return &linuxRouter{ - logf: logf, + logf: logf, + tunname: tunname, + netfilterMode: NetfilterOff, + ipRuleAvailable: ipRuleAvailable, - tunname: tunname, - netfilterMode: NetfilterOff, - ipt4: netfilter4, - ipt6: netfilter6, - cmd: cmd, - dns: dns.NewManager(mconfig), + v6Available: supportsV6, + v6NATAvailable: supportsV6 && supportsV6NAT(), + + ipt4: netfilter4, + ipt6: netfilter6, + cmd: cmd, + dns: dns.NewManager(mconfig), }, nil } @@ -423,6 +443,13 @@ func (r *linuxRouter) downInterface() error { return r.cmd.run("ip", "link", "set", "dev", r.tunname, "down") } +func (r *linuxRouter) iprouteFamilies() []string { + if r.v6Available { + return []string{"-4", "-6"} + } + return []string{"-4"} +} + // addIPRules adds the policy routing rule that avoids tailscaled // routing loops. If the rule exists and appears to be a // tailscale-managed rule, it is gracefully replaced. @@ -439,7 +466,7 @@ func (r *linuxRouter) addIPRules() error { rg := newRunGroup(nil, r.cmd) - for _, family := range []string{"-4", "-6"} { + for _, family := range r.iprouteFamilies() { // NOTE(apenwarr): We leave spaces between each pref number. // This is so the sysadmin can override by inserting rules in // between if they want. @@ -512,7 +539,7 @@ func (r *linuxRouter) delIPRules() error { // unknown rules during deletion. rg := newRunGroup([]int{2, 254}, r.cmd) - for _, family := range []string{"-4", "-6"} { + for _, family := range r.iprouteFamilies() { // When deleting rules, we want to be a bit specific (mention which // table we were routing to) but not *too* specific (fwmarks, etc). // That leaves us some flexibility to change these values in later @@ -554,6 +581,13 @@ func (r *linuxRouter) delIPRules() error { return rg.ErrAcc } +func (r *linuxRouter) netfilterFamilies() []netfilterRunner { + if r.v6Available { + return []netfilterRunner{r.ipt4, r.ipt6} + } + return []netfilterRunner{r.ipt4} +} + // addNetfilterChains creates custom Tailscale chains in netfilter. func (r *linuxRouter) addNetfilterChains() error { create := func(ipt netfilterRunner, table, chain string) error { @@ -568,14 +602,19 @@ func (r *linuxRouter) addNetfilterChains() error { return nil } - for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} { + for _, ipt := range r.netfilterFamilies() { if err := create(ipt, "filter", "ts-input"); err != nil { return err } if err := create(ipt, "filter", "ts-forward"); err != nil { return err } - if err := create(ipt, "nat", "ts-postrouting"); err != nil { + } + if err := create(r.ipt4, "nat", "ts-postrouting"); err != nil { + return err + } + if r.v6NATAvailable { + if err := create(r.ipt6, "nat", "ts-postrouting"); err != nil { return err } } @@ -588,8 +627,10 @@ func (r *linuxRouter) addNetfilterBase() error { if err := r.addNetfilterBase4(); err != nil { return err } - if err := r.addNetfilterBase6(); err != nil { - return err + if r.v6Available { + if err := r.addNetfilterBase6(); err != nil { + return err + } } return nil } @@ -686,14 +727,19 @@ func (r *linuxRouter) delNetfilterChains() error { return nil } - for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} { + for _, ipt := range r.netfilterFamilies() { if err := del(ipt, "filter", "ts-input"); err != nil { return err } if err := del(ipt, "filter", "ts-forward"); err != nil { return err } - if err := del(ipt, "nat", "ts-postrouting"); err != nil { + } + if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil { + return err + } + if r.v6NATAvailable { + if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil { return err } } @@ -716,14 +762,19 @@ func (r *linuxRouter) delNetfilterBase() error { return nil } - for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} { + for _, ipt := range r.netfilterFamilies() { if err := del(ipt, "filter", "ts-input"); err != nil { return err } if err := del(ipt, "filter", "ts-forward"); err != nil { return err } - if err := del(ipt, "nat", "ts-postrouting"); err != nil { + } + if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil { + return err + } + if r.v6NATAvailable { + if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil { return err } } @@ -752,14 +803,19 @@ func (r *linuxRouter) addNetfilterHooks() error { return nil } - for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} { + for _, ipt := range r.netfilterFamilies() { if err := divert(ipt, "filter", "INPUT"); err != nil { return err } if err := divert(ipt, "filter", "FORWARD"); err != nil { return err } - if err := divert(ipt, "nat", "POSTROUTING"); err != nil { + } + if err := divert(r.ipt4, "nat", "POSTROUTING"); err != nil { + return err + } + if r.v6NATAvailable { + if err := divert(r.ipt6, "nat", "POSTROUTING"); err != nil { return err } } @@ -784,14 +840,19 @@ func (r *linuxRouter) delNetfilterHooks() error { return nil } - for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} { + for _, ipt := range r.netfilterFamilies() { if err := del(ipt, "filter", "INPUT"); err != nil { return err } if err := del(ipt, "filter", "FORWARD"); err != nil { return err } - if err := del(ipt, "nat", "POSTROUTING"); err != nil { + } + if err := del(r.ipt4, "nat", "POSTROUTING"); err != nil { + return err + } + if r.v6NATAvailable { + if err := del(r.ipt6, "nat", "POSTROUTING"); err != nil { return err } } @@ -809,8 +870,10 @@ func (r *linuxRouter) addSNATRule() error { if err := r.ipt4.Append("nat", "ts-postrouting", args...); err != nil { return fmt.Errorf("adding %v in v4/nat/ts-postrouting: %w", args, err) } - if err := r.ipt6.Append("nat", "ts-postrouting", args...); err != nil { - return fmt.Errorf("adding %v in v6/nat/ts-postrouting: %w", args, err) + if r.v6NATAvailable { + if err := r.ipt6.Append("nat", "ts-postrouting", args...); err != nil { + return fmt.Errorf("adding %v in v6/nat/ts-postrouting: %w", args, err) + } } return nil } @@ -826,8 +889,10 @@ func (r *linuxRouter) delSNATRule() error { if err := r.ipt4.Delete("nat", "ts-postrouting", args...); err != nil { return fmt.Errorf("deleting %v in v4/nat/ts-postrouting: %w", args, err) } - if err := r.ipt6.Delete("nat", "ts-postrouting", args...); err != nil { - return fmt.Errorf("deleting %v in v6/nat/ts-postrouting: %w", args, err) + if r.v6NATAvailable { + if err := r.ipt6.Delete("nat", "ts-postrouting", args...); err != nil { + return fmt.Errorf("deleting %v in v6/nat/ts-postrouting: %w", args, err) + } } return nil } @@ -915,3 +980,47 @@ func normalizeCIDR(cidr netaddr.IPPrefix) string { func cleanup(logf logger.Logf, interfaceName string) { // TODO(dmytro): clean up iptables. } + +// supportsV6 returns whether the system appears to have a working +// IPv6 network stack. +func supportsV6() bool { + _, err := os.Stat("/proc/sys/net/ipv6") + if os.IsNotExist(err) { + return false + } + bs, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6") + if err != nil { + // Be conservative if we can't find the ipv6 configuration knob. + return false + } + disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs))) + if err != nil { + return false + } + if disabled { + return false + } + + // Some distros ship ip6tables separately from iptables. + if _, err := exec.LookPath("ip6tables"); err != nil { + return false + } + + return true +} + +// supportsV6NAT returns whether the system has a "nat" table in the +// IPv6 netfilter stack. +// +// The nat table was added after the initial release of ipv6 +// netfilter, so some older distros ship a kernel that can't NAT IPv6 +// traffic. +func supportsV6NAT() bool { + bs, err := ioutil.ReadFile("/proc/net/ip6_tables_names") + if err != nil { + // Can't read the file. Assume SNAT works. + return true + } + + return bytes.Contains(bs, []byte("nat\n")) +}