netns,wgengine: add OpenBSD support to netns via an rtable
When an exit node has been set and a new default route is added, create a new rtable in the default rdomain and add the current default route via its physical interface. When control() is requesting a connection not go through the exit-node default route, we can use the SO_RTABLE socket option to force it through the new rtable we created. Updates #17321 Signed-off-by: joshua stein <jcs@jcs.org>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
7370c24eb4
commit
518d241700
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Common code for FreeBSD. This might also work on other
|
||||
// BSD systems (e.g. OpenBSD) but has not been tested.
|
||||
// Common code for FreeBSD and OpenBSD.
|
||||
// Not used on iOS or macOS. See defaultroute_darwin.go.
|
||||
|
||||
//go:build freebsd
|
||||
//go:build freebsd || openbsd
|
||||
|
||||
package netmon
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Common code for FreeBSD and Darwin. This might also work on other
|
||||
// BSD systems (e.g. OpenBSD) but has not been tested.
|
||||
|
||||
//go:build darwin || freebsd
|
||||
//go:build darwin || freebsd || openbsd
|
||||
|
||||
package netmon
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
// FreeBSD and OpenBSD routing table functions.
|
||||
|
||||
//go:build freebsd
|
||||
//go:build freebsd || openbsd
|
||||
|
||||
package netmon
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux && !windows && !darwin && !freebsd && !android
|
||||
//go:build !linux && !windows && !darwin && !freebsd && !android && !openbsd
|
||||
|
||||
package netmon
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux && !windows && !darwin
|
||||
//go:build !linux && !windows && !darwin && !openbsd
|
||||
|
||||
package netns
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build openbsd
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
bypassMu sync.Mutex
|
||||
bypassRtable int
|
||||
)
|
||||
|
||||
// Called by the router when exit node routes are configured.
|
||||
func SetBypassRtable(rtable int) {
|
||||
bypassMu.Lock()
|
||||
defer bypassMu.Unlock()
|
||||
bypassRtable = rtable
|
||||
}
|
||||
|
||||
func GetBypassRtable() int {
|
||||
bypassMu.Lock()
|
||||
defer bypassMu.Unlock()
|
||||
return bypassRtable
|
||||
}
|
||||
|
||||
func control(logf logger.Logf, _ *netmon.Monitor) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
return controlC(logf, network, address, c)
|
||||
}
|
||||
}
|
||||
|
||||
func controlC(logf logger.Logf, _, address string, c syscall.RawConn) error {
|
||||
if isLocalhost(address) {
|
||||
return nil
|
||||
}
|
||||
|
||||
rtable := GetBypassRtable()
|
||||
if rtable == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bindToRtable(c, rtable, logf)
|
||||
}
|
||||
|
||||
func bindToRtable(c syscall.RawConn, rtable int, logf logger.Logf) error {
|
||||
var sockErr error
|
||||
err := c.Control(func(fd uintptr) {
|
||||
sockErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RTABLE, rtable)
|
||||
})
|
||||
if sockErr != nil {
|
||||
logf("netns: SO_RTABLE(%d): %v", rtable, sockErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("RawConn.Control: %w", err)
|
||||
}
|
||||
return sockErr
|
||||
}
|
||||
|
||||
// SetupBypassRtable creates a bypass rtable with the existing default route
|
||||
// in it routing through its existing physical interface. It should be called
|
||||
// by the router when exit node routes are being added.
|
||||
// Returns the rtable number.
|
||||
func SetupBypassRtable(logf logger.Logf) (int, error) {
|
||||
bypassMu.Lock()
|
||||
defer bypassMu.Unlock()
|
||||
|
||||
if bypassRtable != 0 {
|
||||
return bypassRtable, nil
|
||||
}
|
||||
|
||||
gw, err := getPhysicalGateway()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("getPhysicalGateway: %w", err)
|
||||
}
|
||||
|
||||
rtable, err := findAvailableRtable()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("findAvailableRtable: %w", err)
|
||||
}
|
||||
|
||||
// Add the existing default route interface to the new bypass rtable
|
||||
out, err := exec.Command("route", "-T", strconv.Itoa(rtable), "-qn", "add", "default", gw).CombinedOutput()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route -T%d add default %s: %w\n%s", rtable, gw, err, out)
|
||||
}
|
||||
|
||||
bypassRtable = rtable
|
||||
logf("netns: created bypass rtable %d with default route via %s", rtable, gw)
|
||||
return rtable, nil
|
||||
}
|
||||
|
||||
func CleanupBypassRtable(logf logger.Logf) {
|
||||
bypassMu.Lock()
|
||||
defer bypassMu.Unlock()
|
||||
|
||||
if bypassRtable == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the default route from the bypass rtable which should clear it
|
||||
out, err := exec.Command("route", "-T", strconv.Itoa(bypassRtable), "-qn", "delete", "default").CombinedOutput()
|
||||
if err != nil {
|
||||
logf("netns: failed to clear bypass route: %v\n%s", err, out)
|
||||
} else {
|
||||
logf("netns: cleared bypass rtable %d", bypassRtable)
|
||||
}
|
||||
|
||||
bypassRtable = 0
|
||||
}
|
||||
|
||||
// getPhysicalGateway returns the default gateway IP that goes through a
|
||||
// physical interface (not tun).
|
||||
func getPhysicalGateway() (string, error) {
|
||||
out, err := exec.Command("route", "-n", "show", "-inet").CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("route show: %w", err)
|
||||
}
|
||||
|
||||
// Parse the routing table looking for default routes not via tun
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 8 {
|
||||
continue
|
||||
}
|
||||
// Format: Destination Gateway Flags Refs Use Mtu Prio Iface
|
||||
dest := fields[0]
|
||||
gateway := fields[1]
|
||||
iface := fields[7]
|
||||
|
||||
if dest == "default" && !strings.HasPrefix(iface, "tun") {
|
||||
return gateway, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no physical default gateway found")
|
||||
}
|
||||
|
||||
func findAvailableRtable() (int, error) {
|
||||
for i := 1; i <= 255; i++ {
|
||||
out, err := exec.Command("route", "-T", strconv.Itoa(i), "-n", "show", "-inet").CombinedOutput()
|
||||
if err != nil {
|
||||
// rtable doesn't exist, consider it available
|
||||
return i, nil
|
||||
}
|
||||
// Check if the output only contains the header (no actual routes)
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
hasRoutes := false
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "Routing") || strings.HasPrefix(line, "Destination") {
|
||||
continue
|
||||
}
|
||||
hasRoutes = true
|
||||
break
|
||||
}
|
||||
if !hasRoutes {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no available rtable")
|
||||
}
|
||||
|
||||
func UseSocketMark() bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build darwin || freebsd
|
||||
//go:build darwin || freebsd || openbsd
|
||||
|
||||
package routetable
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd
|
||||
//go:build freebsd || openbsd
|
||||
|
||||
package routetable
|
||||
|
||||
@@ -21,6 +21,7 @@ var flags = map[int]string{
|
||||
unix.RTF_BROADCAST: "broadcast",
|
||||
unix.RTF_GATEWAY: "gateway",
|
||||
unix.RTF_HOST: "host",
|
||||
unix.RTF_LOCAL: "local",
|
||||
unix.RTF_MULTICAST: "multicast",
|
||||
unix.RTF_REJECT: "reject",
|
||||
unix.RTF_STATIC: "static",
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build android || (!linux && !darwin && !freebsd)
|
||||
//go:build android || (!linux && !darwin && !freebsd && !openbsd)
|
||||
|
||||
package routetable
|
||||
|
||||
|
||||
Reference in New Issue
Block a user