2023-01-27 21:37:20 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2022-03-28 17:24:11 +00:00
// Package netutil contains misc shared networking code & types.
package netutil
import (
"bytes"
"fmt"
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
"net/netip"
2022-03-28 17:24:11 +00:00
"os"
2023-12-20 05:02:37 +00:00
"os/exec"
2022-03-28 17:24:11 +00:00
"path/filepath"
"runtime"
"strconv"
"strings"
"tailscale.com/net/interfaces"
)
// protocolsRequiredForForwarding reports whether IPv4 and/or IPv6 protocols are
// required to forward the specified routes.
// The state param must be specified.
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func protocolsRequiredForForwarding ( routes [ ] netip . Prefix , state * interfaces . State ) ( v4 , v6 bool ) {
2022-03-28 17:24:11 +00:00
if len ( routes ) == 0 {
// Nothing to route, so no need to warn.
return false , false
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
localIPs := make ( map [ netip . Addr ] bool )
2022-03-28 17:24:11 +00:00
for _ , addrs := range state . InterfaceIPs {
for _ , pfx := range addrs {
2022-07-25 03:08:42 +00:00
localIPs [ pfx . Addr ( ) ] = true
2022-03-28 17:24:11 +00:00
}
}
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.
2022-07-25 03:08:42 +00:00
if r . IsSingleIP ( ) && localIPs [ r . Addr ( ) ] {
2022-03-28 17:24:11 +00:00
continue
}
2022-07-25 03:08:42 +00:00
if r . Addr ( ) . Is4 ( ) {
2022-03-28 17:24:11 +00:00
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.
2023-08-01 22:47:54 +00:00
// The state param must not be nil.
2022-03-28 17:24:11 +00:00
// 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.
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func CheckIPForwarding ( routes [ ] netip . Prefix , state * interfaces . State ) ( warn , err error ) {
2022-03-28 17:24:11 +00:00
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
}
return nil , nil
}
if state == nil {
2023-08-01 22:47:54 +00:00
return nil , fmt . Errorf ( "Couldn't check system's IP forwarding configuration; no link state" )
2022-03-28 17:24:11 +00:00
}
2023-08-01 22:47:54 +00:00
const kbLink = "\nSee https://tailscale.com/s/ip-forwarding"
2022-03-28 17:24:11 +00:00
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
}
2023-12-20 05:02:37 +00:00
// 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
}
2022-03-28 17:24:11 +00:00
// 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 )
}
2023-12-20 05:02:37 +00:00
// 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 )
}
2022-03-28 17:24:11 +00:00
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.
2023-02-08 20:11:25 +00:00
// 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).
2022-03-28 17:24:11 +00:00
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 {
2023-02-08 20:11:25 +00:00
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 ( ) )
2022-03-28 17:24:11 +00:00
}
2023-02-08 20:11:25 +00:00
return false , nil
2022-03-28 17:24:11 +00:00
}
2023-02-08 20:11:25 +00:00
return false , err
2022-03-28 17:24:11 +00:00
}
2023-06-27 19:00:42 +00:00
val , err := strconv . ParseInt ( string ( bytes . TrimSpace ( bs ) ) , 10 , 32 )
2022-03-28 17:24:11 +00:00
if err != nil {
2023-02-08 20:11:25 +00:00
return false , fmt . Errorf ( "couldn't parse %s: %w" , k , err )
2022-03-28 17:24:11 +00:00
}
2023-06-27 19:00:42 +00:00
// 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
2022-03-28 17:24:11 +00:00
return on , nil
}
2023-12-20 05:02:37 +00:00
// 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
}