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