diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index e0db5369d..f65d12742 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -6,6 +6,7 @@ package router import ( "bytes" + "errors" "fmt" "io/ioutil" "os" @@ -113,8 +114,15 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) ( return nil, err } - supportsV6 := supportsV6() + v6err := checkIPv6() + if v6err != nil { + logf("disabling IPv6 due to system IPv6 config: %v", v6err) + } + supportsV6 := v6err == nil supportsV6NAT := supportsV6 && supportsV6NAT() + if supportsV6 { + logf("v6nat = %v", supportsV6NAT) + } var ipt6 netfilterRunner if supportsV6 { @@ -1003,46 +1011,53 @@ 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 { +// checkIPv6 checks whether the system appears to have a working IPv6 +// network stack. It returns an error explaining what looks wrong or +// missing. It does not check that IPv6 is currently functional or +// that there's a global address, just that the system would support +// IPv6 if it were on an IPv6 network. +func checkIPv6() error { _, err := os.Stat("/proc/sys/net/ipv6") if os.IsNotExist(err) { - return false + return err } 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 + return err } disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs))) if err != nil { - return false + return errors.New("disable_ipv6 has invalid bool") } if disabled { - return false + return errors.New("disable_ipv6 is set") } // Older kernels don't support IPv6 policy routing. bs, err = ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy") if err != nil { // Absent knob means policy routing is unsupported. - return false + return err } disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs))) if err != nil { - return false + return errors.New("disable_policy has invalid bool") } if disabled { - return false + return errors.New("disable_policy is set") } // Some distros ship ip6tables separately from iptables. if _, err := exec.LookPath("ip6tables"); err != nil { - return false + return err } - return true + if err := checkIPRuleSupportsV6(); err != nil { + return err + } + + return nil } // supportsV6NAT returns whether the system has a "nat" table in the @@ -1060,3 +1075,22 @@ func supportsV6NAT() bool { return bytes.Contains(bs, []byte("nat\n")) } + +func checkIPRuleSupportsV6() error { + // First add a rule for "ip rule del" to delete. + // We ignore the "add" operation's error because it can also + // fail if the rule already exists. + exec.Command("ip", "-6", "rule", "add", + "pref", "123", "fwmark", tailscaleBypassMark, "table", fmt.Sprint(tailscaleRouteTable)).Run() + out, err := exec.Command("ip", "-6", "rule", "del", + "pref", "123", "fwmark", tailscaleBypassMark, "table", fmt.Sprint(tailscaleRouteTable)).CombinedOutput() + if err != nil { + out = bytes.TrimSpace(out) + var detail interface{} = out + if len(out) == 0 { + detail = err.Error() + } + return fmt.Errorf("ip -6 rule failed: %s", detail) + } + return nil +}