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>
main
joshua stein 2 months ago committed by Brad Fitzpatrick
parent 7370c24eb4
commit 518d241700
  1. 2
      ipn/ipnlocal/local.go
  2. 5
      net/netmon/defaultroute_bsd.go
  3. 2
      net/netmon/interfaces_bsd.go
  4. 4
      net/netmon/interfaces_bsdroute.go
  5. 2
      net/netmon/interfaces_defaultrouteif_todo.go
  6. 2
      net/netns/netns_default.go
  7. 178
      net/netns/netns_openbsd.go
  8. 2
      net/routetable/routetable_bsd.go
  9. 3
      net/routetable/routetable_bsdconst.go
  10. 2
      net/routetable/routetable_other.go
  11. 49
      wgengine/router/osrouter/router_openbsd.go

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

Loading…
Cancel
Save