From d2480fd508fb9ffb5582141a24cb14cc19388e2f Mon Sep 17 00:00:00 2001 From: Denton Gentry Date: Tue, 29 Jun 2021 21:13:00 -0700 Subject: [PATCH] net/netns: support !CAP_NET_ADMIN netns_linux checked whether "ip rule" could run to determine whether to use SO_MARK for network namespacing. However in Linux environments which lack CAP_NET_ADMIN, such as various container runtimes, the "ip rule" command succeeds but SO_MARK fails due to lack of permission. SO_BINDTODEVICE would work in these environments, but isn't tried. In addition to running "ip rule" check directly whether SO_MARK works or not. Among others, this allows Microsoft Azure App Service and AWS App Runner to work. Signed-off-by: Denton Gentry --- cmd/tailscale/depaware.txt | 2 +- net/netns/netns_linux.go | 53 +++++++++++++++++++++++++++-------- net/netns/netns_linux_test.go | 6 ++++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index dfc160e37..3adfe7c62 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -22,7 +22,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/derp from tailscale.com/derp/derphttp tailscale.com/derp/derphttp from tailscale.com/net/netcheck tailscale.com/disco from tailscale.com/derp - tailscale.com/hostinfo from tailscale.com/net/interfaces + tailscale.com/hostinfo from tailscale.com/net/interfaces+ tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+ tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+ tailscale.com/metrics from tailscale.com/derp diff --git a/net/netns/netns_linux.go b/net/netns/netns_linux.go index e3d2c03da..42834338c 100644 --- a/net/netns/netns_linux.go +++ b/net/netns/netns_linux.go @@ -7,14 +7,15 @@ package netns import ( - "flag" "fmt" + "net" "os" "os/exec" "sync" "syscall" "golang.org/x/sys/unix" + "tailscale.com/hostinfo" "tailscale.com/net/interfaces" ) @@ -26,19 +27,49 @@ import ( // wgengine/router/router_linux.go. const tailscaleBypassMark = 0x80000 -// ipRuleOnce is the sync.Once & cached value for ipRuleAvailable. -var ipRuleOnce struct { +// socketMarkWorksOnce is the sync.Once & cached value for useSocketMark. +var socketMarkWorksOnce struct { sync.Once v bool } -// ipRuleAvailable reports whether the 'ip rule' command works. +// socketMarkWorks returns whether SO_MARK works. +func socketMarkWorks() bool { + addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:1") + if err != nil { + return true // unsure, returning true does the least harm. + } + + sConn, err := net.DialUDP("udp", nil, addr) + if err != nil { + return true // unsure, return true + } + defer sConn.Close() + + rConn, err := sConn.SyscallConn() + if err != nil { + return true // unsure, return true + } + + var sockErr error + err = rConn.Control(func(fd uintptr) { + sockErr = setBypassMark(fd) + }) + if err != nil || sockErr != nil { + return false + } + + return true +} + +// useSocketMark reports whether SO_MARK works. // If it doesn't, we have to use SO_BINDTODEVICE on our sockets instead. -func ipRuleAvailable() bool { - ipRuleOnce.Do(func() { - ipRuleOnce.v = exec.Command("ip", "rule").Run() == nil +func useSocketMark() bool { + socketMarkWorksOnce.Do(func() { + ipRuleWorks := exec.Command("ip", "rule").Run() == nil + socketMarkWorksOnce.v = ipRuleWorks && socketMarkWorks() }) - return ipRuleOnce.v + return socketMarkWorksOnce.v } // ignoreErrors returns true if we should ignore setsocketopt errors in @@ -49,7 +80,7 @@ func ignoreErrors() bool { // checks if it's setting up a world that needs netns to work. // But by default, assume that tests don't need netns and it's // harmless to ignore the sockopts failing. - if flag.CommandLine.Lookup("test.v") != nil { + if hostinfo.GetEnvType() == hostinfo.TestCase { return true } if os.Getuid() != 0 { @@ -67,14 +98,14 @@ func control(network, address string, c syscall.RawConn) error { if hostinfo.GetEnvType() == hostinfo.TestCase { return nil } - if IsLocalhost(address) { + if isLocalhost(address) { // Don't bind to an interface for localhost connections. return nil } var sockErr error err := c.Control(func(fd uintptr) { - if ipRuleAvailable() { + if useSocketMark() { sockErr = setBypassMark(fd) } else { sockErr = bindToDevice(fd) diff --git a/net/netns/netns_linux_test.go b/net/netns/netns_linux_test.go index 8b050f7f5..38f989cea 100644 --- a/net/netns/netns_linux_test.go +++ b/net/netns/netns_linux_test.go @@ -49,3 +49,9 @@ func TestBypassMarkInSync(t *testing.T) { } t.Errorf("tailscaleBypassMark not found in router_linux.go") } + +func TestSocketMarkWorks(t *testing.T) { + _ = socketMarkWorks() + // we cannot actually assert whether the test runner has SO_MARK available + // or not, as we don't know. We're just checking that it doesn't panic. +}