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

Updates #4432


Change-Id: Ifc332a5747fc1feffdbb87437308cf8ecb21b0b0

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
This commit is contained in:
Andrew Dunham 2023-12-20 00:02:37 -05:00 committed by GitHub
parent 65f2d32300
commit 09136e5995
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 128 additions and 0 deletions

View File

@ -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
}

View File

@ -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)
}