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
@@ -5601,7 +5601,7 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView
|
||||
b.logf("failed to discover interface ips: %v", err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux", "windows", "darwin", "ios", "android":
|
||||
case "linux", "windows", "darwin", "ios", "android", "openbsd":
|
||||
rs.LocalRoutes = internalIPs // unconditionally allow access to guest VM networks
|
||||
if prefs.ExitNodeAllowLANAccess() {
|
||||
rs.LocalRoutes = append(rs.LocalRoutes, externalIPs...)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/set"
|
||||
@@ -32,12 +33,13 @@ func init() {
|
||||
// https://git.zx2c4.com/wireguard-openbsd.
|
||||
|
||||
type openbsdRouter struct {
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor
|
||||
tunname string
|
||||
local4 netip.Prefix
|
||||
local6 netip.Prefix
|
||||
routes set.Set[netip.Prefix]
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor
|
||||
tunname string
|
||||
local4 netip.Prefix
|
||||
local6 netip.Prefix
|
||||
routes set.Set[netip.Prefix]
|
||||
areDefaultRoute bool
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker, bus *eventbus.Bus) (router.Router, error) {
|
||||
@@ -76,6 +78,10 @@ func inet(p netip.Prefix) string {
|
||||
return "inet"
|
||||
}
|
||||
|
||||
func isDefaultRoute(p netip.Prefix) bool {
|
||||
return p.Bits() == 0
|
||||
}
|
||||
|
||||
func (r *openbsdRouter) Set(cfg *router.Config) error {
|
||||
if cfg == nil {
|
||||
cfg = &shutdownConfig
|
||||
@@ -219,8 +225,12 @@ func (r *openbsdRouter) Set(cfg *router.Config) error {
|
||||
dst = localAddr6.Addr().String()
|
||||
}
|
||||
routeadd := []string{"route", "-q", "-n",
|
||||
"add", "-" + inet(route), nstr,
|
||||
"-iface", dst}
|
||||
"add", "-" + inet(route), nstr}
|
||||
if isDefaultRoute(route) {
|
||||
// 1 is reserved for kernel
|
||||
routeadd = append(routeadd, "-priority", "2")
|
||||
}
|
||||
routeadd = append(routeadd, "-iface", dst)
|
||||
out, err := cmd(routeadd...).CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("addr add failed: %v: %v\n%s", routeadd, err, out)
|
||||
@@ -235,10 +245,33 @@ func (r *openbsdRouter) Set(cfg *router.Config) error {
|
||||
r.local6 = localAddr6
|
||||
r.routes = newRoutes
|
||||
|
||||
areDefault := false
|
||||
for route := range newRoutes {
|
||||
if isDefaultRoute(route) {
|
||||
areDefault = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Set up or tear down the bypass rtable as needed
|
||||
if areDefault && !r.areDefaultRoute {
|
||||
if _, err := netns.SetupBypassRtable(r.logf); err != nil {
|
||||
r.logf("router: failed to set up bypass rtable: %v", err)
|
||||
}
|
||||
r.areDefaultRoute = true
|
||||
} else if !areDefault && r.areDefaultRoute {
|
||||
netns.CleanupBypassRtable(r.logf)
|
||||
r.areDefaultRoute = false
|
||||
}
|
||||
|
||||
return errq
|
||||
}
|
||||
|
||||
func (r *openbsdRouter) Close() error {
|
||||
if r.areDefaultRoute {
|
||||
netns.CleanupBypassRtable(r.logf)
|
||||
r.areDefaultRoute = false
|
||||
}
|
||||
cleanUp(r.logf, r.tunname)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user