فهرست منبع

net/netutil: add function to check rp_filter value (#5703)

Updates #4432


Change-Id: Ifc332a5747fc1feffdbb87437308cf8ecb21b0b0

Signed-off-by: Andrew Dunham <[email protected]>
Andrew Dunham 2 سال پیش
والد
کامیت
09136e5995
2فایلهای تغییر یافته به همراه128 افزوده شده و 0 حذف شده
  1. 116 0
      net/netutil/ip_forward.go
  2. 12 0
      net/netutil/netutil_test.go

+ 116 - 0
net/netutil/ip_forward.go

@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"net/netip"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"runtime"
 	"strconv"
@@ -140,6 +141,76 @@ func CheckIPForwarding(routes []netip.Prefix, state *interfaces.State) (warn, er
 	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 state param can be nil, in which case interfaces.GetState is used.
+//
+// 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(routes []netip.Prefix, state *interfaces.State) (warn []string, err error) {
+	if runtime.GOOS != "linux" {
+		return nil, nil
+	}
+
+	if state == nil {
+		var err error
+		state, err = interfaces.GetState()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Reverse path filtering as a syscall is only implemented on Linux for IPv4.
+	wantV4, _ := protocolsRequiredForForwarding(routes, state)
+	if !wantV4 {
+		return nil, nil
+	}
+
+	// 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`
@@ -171,6 +242,25 @@ func ipForwardSysctlKey(format sysctlFormat, p protocol, iface string) string {
 	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 (
@@ -221,3 +311,29 @@ func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
 	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
+}

+ 12 - 0
net/netutil/netutil_test.go

@@ -6,6 +6,7 @@ package netutil
 import (
 	"io"
 	"net"
+	"net/netip"
 	"runtime"
 	"testing"
 )
@@ -65,3 +66,14 @@ func TestIPForwardingEnabledLinux(t *testing.T) {
 		t.Errorf("got true; want false")
 	}
 }
+
+func TestCheckReversePathFiltering(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("skipping on %s", runtime.GOOS)
+	}
+	warn, err := CheckReversePathFiltering([]netip.Prefix{
+		netip.MustParsePrefix("192.168.1.1/24"),
+	}, nil)
+	t.Logf("err: %v", err)
+	t.Logf("warnings: %v", warn)
+}