| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- // 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
- }
|