mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	Updates #14565 Change-Id: I743148144938794db0a224873ce76c10dbe6fa5f Signed-off-by: Nahum Shalman <nahamu@gmail.com>
		
			
				
	
	
		
			354 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			354 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package netutil
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/netip"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"tailscale.com/net/netmon"
 | |
| )
 | |
| 
 | |
| // protocolsRequiredForForwarding reports whether IPv4 and/or IPv6 protocols are
 | |
| // required to forward the specified routes.
 | |
| // The state param must be specified.
 | |
| func protocolsRequiredForForwarding(routes []netip.Prefix, state *netmon.State) (v4, v6 bool) {
 | |
| 	if len(routes) == 0 {
 | |
| 		// Nothing to route, so no need to warn.
 | |
| 		return false, false
 | |
| 	}
 | |
| 
 | |
| 	localIPs := make(map[netip.Addr]bool)
 | |
| 	for _, addrs := range state.InterfaceIPs {
 | |
| 		for _, pfx := range addrs {
 | |
| 			localIPs[pfx.Addr()] = true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, r := range routes {
 | |
| 		// It's possible to advertise a route to one of the local
 | |
| 		// machine's local IPs. IP forwarding isn't required for this
 | |
| 		// to work, so we shouldn't warn for such exports.
 | |
| 		if r.IsSingleIP() && localIPs[r.Addr()] {
 | |
| 			continue
 | |
| 		}
 | |
| 		if r.Addr().Is4() {
 | |
| 			v4 = true
 | |
| 		} else {
 | |
| 			v6 = true
 | |
| 		}
 | |
| 	}
 | |
| 	return v4, v6
 | |
| }
 | |
| 
 | |
| // CheckIPForwarding reports whether IP forwarding is enabled correctly
 | |
| // for subnet routing and exit node functionality on any interface.
 | |
| // The state param must not be nil.
 | |
| // The routes should only be advertised routes, and should not contain the
 | |
| // nodes Tailscale IPs.
 | |
| // It returns an error if it is unable to determine if IP forwarding is enabled.
 | |
| // It returns a warning describing configuration issues if IP forwarding is
 | |
| // non-functional or partly functional.
 | |
| func CheckIPForwarding(routes []netip.Prefix, state *netmon.State) (warn, err error) {
 | |
| 	if runtime.GOOS != "linux" {
 | |
| 		switch runtime.GOOS {
 | |
| 		case "dragonfly", "freebsd", "netbsd", "openbsd":
 | |
| 			return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS), nil
 | |
| 		case "illumos", "solaris":
 | |
| 			_, err := ipForwardingEnabledSunOS(ipv4, "")
 | |
| 			if err != nil {
 | |
| 				return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, "")
 | |
| 			}
 | |
| 		}
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	if state == nil {
 | |
| 		return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration; no link state")
 | |
| 	}
 | |
| 	const kbLink = "\nSee https://tailscale.com/s/ip-forwarding"
 | |
| 	wantV4, wantV6 := protocolsRequiredForForwarding(routes, state)
 | |
| 	if !wantV4 && !wantV6 {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	v4e, err := ipForwardingEnabledLinux(ipv4, "")
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
 | |
| 	}
 | |
| 	v6e, err := ipForwardingEnabledLinux(ipv6, "")
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
 | |
| 	}
 | |
| 
 | |
| 	if v4e && v6e {
 | |
| 		// IP forwarding is enabled systemwide, all is well.
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	if !wantV4 {
 | |
| 		if !v6e {
 | |
| 			return nil, fmt.Errorf("IPv6 forwarding is disabled, subnet routing/exit nodes may not work.%s", kbLink)
 | |
| 		}
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	// IP forwarding isn't enabled globally, but it might be enabled
 | |
| 	// on a per-interface basis. Check if it's on for all interfaces,
 | |
| 	// and warn appropriately if it's not.
 | |
| 	// Note: you might be wondering why we check only the state of
 | |
| 	// ipv6.conf.all.forwarding, rather than per-interface forwarding
 | |
| 	// configuration. According to kernel documentation, it seems
 | |
| 	// that to actually forward packets, you need to enable
 | |
| 	// forwarding globally, and the per-interface forwarding
 | |
| 	// setting only alters other things such as how router
 | |
| 	// advertisements are handled. The kernel itself warns that
 | |
| 	// enabling forwarding per-interface and not globally will
 | |
| 	// probably not work, so I feel okay calling those configs
 | |
| 	// broken until we have proof otherwise.
 | |
| 	var (
 | |
| 		anyEnabled bool
 | |
| 		warnings   []string
 | |
| 	)
 | |
| 	if wantV6 && !v6e {
 | |
| 		warnings = append(warnings, "IPv6 forwarding is disabled.")
 | |
| 	}
 | |
| 	for _, iface := range state.Interface {
 | |
| 		if iface.Name == "lo" {
 | |
| 			continue
 | |
| 		}
 | |
| 		v4e, err := ipForwardingEnabledLinux(ipv4, iface.Name)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
 | |
| 		} else if !v4e {
 | |
| 			warnings = append(warnings, fmt.Sprintf("Traffic received on %s won't be forwarded (%s disabled)", iface.Name, ipForwardSysctlKey(dotFormat, ipv4, iface.Name)))
 | |
| 		} else {
 | |
| 			anyEnabled = true
 | |
| 		}
 | |
| 	}
 | |
| 	if !anyEnabled {
 | |
| 		// IP forwarding is completely 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), nil
 | |
| 	}
 | |
| 	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), nil
 | |
| 	}
 | |
| 
 | |
| 	return nil, nil
 | |
| }
 | |
| 
 | |
| // CheckReversePathFiltering reports whether reverse path filtering is either
 | |
| // disabled or set to 'loose' mode for exit node functionality on any
 | |
| // interface.
 | |
| //
 | |
| // The routes should only be advertised routes, and should not contain the
 | |
| // node's Tailscale IPs.
 | |
| //
 | |
| // This function returns an error if it is unable to determine whether reverse
 | |
| // path filtering is enabled, or a warning describing configuration issues if
 | |
| // reverse path fitering is non-functional or partly functional.
 | |
| func CheckReversePathFiltering(state *netmon.State) (warn []string, err error) {
 | |
| 	if runtime.GOOS != "linux" {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	if state == nil {
 | |
| 		return nil, errors.New("no link state")
 | |
| 	}
 | |
| 
 | |
| 	// The kernel uses the maximum value for rp_filter between the 'all'
 | |
| 	// setting and each per-interface config, so we need to fetch both.
 | |
| 	allSetting, err := reversePathFilterValueLinux("all")
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("reading global rp_filter value: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	const (
 | |
| 		filtOff    = 0
 | |
| 		filtStrict = 1
 | |
| 		filtLoose  = 2
 | |
| 	)
 | |
| 
 | |
| 	// Because the kernel use the max rp_filter value, each interface will use 'loose', so we
 | |
| 	// can abort early.
 | |
| 	if allSetting == filtLoose {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	for _, iface := range state.Interface {
 | |
| 		if iface.IsLoopback() {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		iSetting, err := reversePathFilterValueLinux(iface.Name)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("reading interface rp_filter value for %q: %w", iface.Name, err)
 | |
| 		}
 | |
| 		// Perform the same max() that the kernel does
 | |
| 		if allSetting > iSetting {
 | |
| 			iSetting = allSetting
 | |
| 		}
 | |
| 		if iSetting == filtStrict {
 | |
| 			warn = append(warn, fmt.Sprintf("interface %q has strict reverse-path filtering enabled", iface.Name))
 | |
| 		}
 | |
| 	}
 | |
| 	return warn, nil
 | |
| }
 | |
| 
 | |
| // ipForwardSysctlKey returns the sysctl key for the given protocol and iface.
 | |
| // When the dotFormat parameter is true the output is formatted as `net.ipv4.ip_forward`,
 | |
| // else it is `net/ipv4/ip_forward`
 | |
| func ipForwardSysctlKey(format sysctlFormat, p protocol, iface string) string {
 | |
| 	if iface == "" {
 | |
| 		if format == dotFormat {
 | |
| 			if p == ipv4 {
 | |
| 				return "net.ipv4.ip_forward"
 | |
| 			}
 | |
| 			return "net.ipv6.conf.all.forwarding"
 | |
| 		}
 | |
| 		if p == ipv4 {
 | |
| 			return "net/ipv4/ip_forward"
 | |
| 		}
 | |
| 		return "net/ipv6/conf/all/forwarding"
 | |
| 	}
 | |
| 
 | |
| 	var k string
 | |
| 	if p == ipv4 {
 | |
| 		k = "net/ipv4/conf/%s/forwarding"
 | |
| 	} else {
 | |
| 		k = "net/ipv6/conf/%s/forwarding"
 | |
| 	}
 | |
| 	if format == dotFormat {
 | |
| 		// Swap the delimiters.
 | |
| 		iface = strings.ReplaceAll(iface, ".", "/")
 | |
| 		k = strings.ReplaceAll(k, "/", ".")
 | |
| 	}
 | |
| 	return fmt.Sprintf(k, iface)
 | |
| }
 | |
| 
 | |
| // rpFilterSysctlKey returns the sysctl key for the given iface.
 | |
| //
 | |
| // Format controls whether the output is formatted as
 | |
| // `net.ipv4.conf.iface.rp_filter` or `net/ipv4/conf/iface/rp_filter`.
 | |
| func rpFilterSysctlKey(format sysctlFormat, iface string) string {
 | |
| 	// No iface means all interfaces
 | |
| 	if iface == "" {
 | |
| 		iface = "all"
 | |
| 	}
 | |
| 
 | |
| 	k := "net/ipv4/conf/%s/rp_filter"
 | |
| 	if format == dotFormat {
 | |
| 		// Swap the delimiters.
 | |
| 		iface = strings.ReplaceAll(iface, ".", "/")
 | |
| 		k = strings.ReplaceAll(k, "/", ".")
 | |
| 	}
 | |
| 	return fmt.Sprintf(k, iface)
 | |
| }
 | |
| 
 | |
| type sysctlFormat int
 | |
| 
 | |
| const (
 | |
| 	dotFormat sysctlFormat = iota
 | |
| 	slashFormat
 | |
| )
 | |
| 
 | |
| type protocol int
 | |
| 
 | |
| const (
 | |
| 	ipv4 protocol = iota
 | |
| 	ipv6
 | |
| )
 | |
| 
 | |
| // ipForwardingEnabledLinux reports whether the IP Forwarding is enabled for the
 | |
| // given interface.
 | |
| // The iface param determines which interface to check against, "" means to check
 | |
| // global config.
 | |
| // This is Linux-specific: it only reads from /proc/sys and doesn't shell out to
 | |
| // sysctl (which on Linux just reads from /proc/sys anyway).
 | |
| func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
 | |
| 	k := ipForwardSysctlKey(slashFormat, p, iface)
 | |
| 	bs, err := os.ReadFile(filepath.Join("/proc/sys", k))
 | |
| 	if err != nil {
 | |
| 		if os.IsNotExist(err) {
 | |
| 			// If IPv6 is disabled, sysctl keys like "net.ipv6.conf.all.forwarding" just don't
 | |
| 			// exist on disk. But first diagnose whether procfs is even mounted before assuming
 | |
| 			// absence means false.
 | |
| 			if fi, err := os.Stat("/proc/sys"); err != nil {
 | |
| 				return false, fmt.Errorf("failed to check sysctl %v; no procfs? %w", k, err)
 | |
| 			} else if !fi.IsDir() {
 | |
| 				return false, fmt.Errorf("failed to check sysctl %v; /proc/sys isn't a directory, is %v", k, fi.Mode())
 | |
| 			}
 | |
| 			return false, nil
 | |
| 		}
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
 | |
| 	if err != nil {
 | |
| 		return false, fmt.Errorf("couldn't parse %s: %w", k, err)
 | |
| 	}
 | |
| 	// 0 = disabled, 1 = enabled, 2 = enabled (but uncommon)
 | |
| 	// https://github.com/tailscale/tailscale/issues/8375
 | |
| 	if val < 0 || val > 2 {
 | |
| 		return false, fmt.Errorf("unexpected value %d for %s", val, k)
 | |
| 	}
 | |
| 	on := val == 1 || val == 2
 | |
| 	return on, nil
 | |
| }
 | |
| 
 | |
| // reversePathFilterValueLinux reports the reverse path filter setting on Linux
 | |
| // for the given interface.
 | |
| //
 | |
| // The iface param determines which interface to check against; the empty
 | |
| // string means to check the global config.
 | |
| //
 | |
| // This function tries to look up the value directly from `/proc/sys`, and
 | |
| // falls back to using the `sysctl` command on failure.
 | |
| func reversePathFilterValueLinux(iface string) (int, error) {
 | |
| 	k := rpFilterSysctlKey(slashFormat, iface)
 | |
| 	bs, err := os.ReadFile(filepath.Join("/proc/sys", k))
 | |
| 	if err != nil {
 | |
| 		// Fall back to the sysctl command
 | |
| 		k := rpFilterSysctlKey(dotFormat, iface)
 | |
| 		bs, err = exec.Command("sysctl", "-n", k).Output()
 | |
| 		if err != nil {
 | |
| 			return -1, fmt.Errorf("couldn't check %s (%v)", k, err)
 | |
| 		}
 | |
| 	}
 | |
| 	v, err := strconv.Atoi(string(bytes.TrimSpace(bs)))
 | |
| 	if err != nil {
 | |
| 		return -1, fmt.Errorf("couldn't parse %s (%v)", k, err)
 | |
| 	}
 | |
| 	return v, nil
 | |
| }
 | |
| 
 | |
| func ipForwardingEnabledSunOS(p protocol, iface string) (bool, error) {
 | |
| 	var proto string
 | |
| 	if p == ipv4 {
 | |
| 		proto = "ipv4"
 | |
| 	} else if p == ipv6 {
 | |
| 		proto = "ipv6"
 | |
| 	} else {
 | |
| 		return false, fmt.Errorf("unknown protocol")
 | |
| 	}
 | |
| 
 | |
| 	ipadmCmd := "\"ipadm show-prop " + proto + " -p forwarding -o CURRENT -c\""
 | |
| 	bs, err := exec.Command("ipadm", "show-prop", proto, "-p", "forwarding", "-o", "CURRENT", "-c").Output()
 | |
| 	if err != nil {
 | |
| 		return false, fmt.Errorf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", ipadmCmd, err)
 | |
| 	}
 | |
| 	if string(bs) != "on\n" {
 | |
| 		return false, fmt.Errorf("IP forwarding is set to off. Subnet routes won't work. Try 'routeadm -u -e %s-forwarding'", proto)
 | |
| 	}
 | |
| 	return true, nil
 | |
| }
 |