ipn/ipnlocal: warn more precisely about IP forwarding issues on linux.

If IP forwarding is disabled globally, but enabled per-interface on all interfaces,
don't complain. If only some interfaces have forwarding enabled, warn that some
subnet routing/exit node traffic may not work.

Fixes #1586

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson 2021-11-25 16:12:08 -08:00 committed by Dave Anderson
parent db800ddeac
commit 097602b3ca

View File

@ -2909,35 +2909,97 @@ func (b *LocalBackend) CheckIPForwarding() error {
if wgengine.IsNetstackRouter(b.e) { if wgengine.IsNetstackRouter(b.e) {
return nil return nil
} }
if isBSD(runtime.GOOS) {
switch {
case isBSD(runtime.GOOS):
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS) return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS)
case runtime.GOOS == "linux":
return checkIPForwardingLinux()
default:
// TODO: subnet routing and exit nodes probably don't work
// correctly on non-linux, non-netstack OSes either. Warn
// instead of being silent?
return nil
}
}
// checkIPForwardingLinux checks if IP forwarding is enabled correctly
// for subnet routing and exit node functionality. Returns an error
// describing configuration issues if the configuration is not
// definitely good.
func checkIPForwardingLinux() error {
const kbLink = "\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
disabled, err := disabledSysctls("net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
if err != nil {
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
} }
var keys []string if len(disabled) == 0 {
// IP forwarding is enabled systemwide, all is well.
if runtime.GOOS == "linux" {
keys = append(keys, "net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
} else if isBSD(runtime.GOOS) {
keys = append(keys, "net.inet.ip.forwarding")
} else {
return nil return nil
} }
const suffix = "\nSubnet routes won't work without IP forwarding.\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/" // IP forwarding isn't enabled globally, but it might be enabled
for _, key := range keys { // on a per-interface basis. Check if it's on for all interfaces,
bs, err := exec.Command("sysctl", "-n", key).Output() // and warn appropriately if it's not.
ifaces, err := interfaces.GetList()
if err != nil { if err != nil {
return fmt.Errorf("couldn't check %s (%v)%s", key, err, suffix) return fmt.Errorf("Couldn't enumerate network interfaces, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
var (
warnings []string
anyEnabled bool
)
for _, iface := range ifaces {
if iface.Name == "lo" {
continue
}
disabled, err = disabledSysctls(fmt.Sprintf("net.ipv4.conf.%s.forwarding", iface.Name), fmt.Sprintf("net.ipv6.conf.%s.forwarding", iface.Name))
if err != nil {
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if len(disabled) > 0 {
warnings = append(warnings, fmt.Sprintf("Traffic received on %s won't be forwarded (%s disabled)", iface.Name, strings.Join(disabled, ", ")))
} else {
anyEnabled = true
}
}
if !anyEnabled {
// IP forwarding is compeltely disabled, just say that rather
// than enumerate all the interfaces on the system.
return fmt.Errorf("IP forwarding is disabled, subnet routing/exit nodes will not work.%s", kbLink)
}
if len(warnings) > 0 {
// If partially enabled, enumerate the bits that won't work.
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink)
}
return nil
}
// disabledSysctls checks if the given sysctl keys are off, according
// to strconv.ParseBool. Returns a list of keys that are disabled, or
// err if something went wrong which prevented the lookups from
// completing.
func disabledSysctls(sysctls ...string) (disabled []string, err error) {
for _, k := range sysctls {
// TODO: on linux, we can get at these values via /proc/sys,
// rather than fork subcommands that may not be installed.
bs, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
return nil, fmt.Errorf("couldn't check %s (%v)", k, err)
} }
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs))) on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
if err != nil { if err != nil {
return fmt.Errorf("couldn't parse %s (%v)%s.", key, err, suffix) return nil, fmt.Errorf("couldn't parse %s (%v)", k, err)
} }
if !on { if !on {
return fmt.Errorf("%s is disabled.%s", key, suffix) disabled = append(disabled, k)
} }
} }
return nil return disabled, nil
} }
// peerDialControlFunc is non-nil on platforms that require a way to // peerDialControlFunc is non-nil on platforms that require a way to