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 <dgentry@tailscale.com>
pull/2429/head
Denton Gentry 3 years ago committed by Denton Gentry
parent 1896bf99d9
commit d2480fd508

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

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

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

Loading…
Cancel
Save