wgengine/router/osrouter: add native FreeBSD routing, no-snat support

And add site-to-site natlab vmtests for Linux + FreeBSD.

Fixes #5573
Updates #18897

Change-Id: I9629f46fdd99137f0ae96431487d3a7410e2a4cc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2026-04-09 14:33:57 +00:00
parent 0e8ae9d60c
commit c02a32029e
13 changed files with 400 additions and 37 deletions

View File

@ -115,6 +115,8 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
setf.BoolVar(&setArgs.statefulFiltering, "stateful-filtering", false, "apply stateful filtering to forwarded packets (subnet routers, exit nodes, and so on)")
setf.StringVar(&setArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
case "freebsd":
setf.BoolVar(&setArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
case "windows":
setf.BoolVar(&setArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}

View File

@ -128,6 +128,8 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
upf.BoolVar(&upArgs.statefulFiltering, "stateful-filtering", false, "apply stateful filtering to forwarded packets (subnet routers, exit nodes, and so on)")
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
case "freebsd":
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
case "windows":
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}
@ -355,14 +357,15 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
prefs.AppConnector.Advertise = upArgs.advertiseConnector
prefs.PostureChecking = upArgs.postureChecking
if goos == "linux" {
if goos == "linux" || goos == "freebsd" {
prefs.NoSNAT = !upArgs.snat
// We want to make sure user is aware setting --snat-subnet-routes=false with --advertise-exit-node would break exitnode,
// but we won't prevent them from doing it since there are current dependencies on that combination. (as of 2026-03-25)
if prefs.NoSNAT && prefs.AdvertisesExitNode() {
warnf("--snat-subnet-routes=false is set with --advertise-exit-node; internet traffic through this exit node may not work as expected")
}
}
if goos == "linux" {
// Backfills for NoStatefulFiltering occur when loading a profile; just set it explicitly here.
prefs.NoStatefulFiltering.Set(!upArgs.statefulFiltering)
v, warning, err := netfilterModeFromFlag(upArgs.netfilterMode)
@ -1106,8 +1109,10 @@ func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, env upCheckEnv) {
func flagAppliesToOS(flag, goos string) bool {
switch flag {
case "netfilter-mode", "snat-subnet-routes", "stateful-filtering":
case "netfilter-mode", "stateful-filtering":
return goos == "linux"
case "snat-subnet-routes":
return goos == "linux" || goos == "freebsd"
case "unattended":
return goos == "windows"
}

View File

@ -729,9 +729,9 @@ func handleSubnetsInNetstack() bool {
return true
}
switch runtime.GOOS {
case "windows", "darwin", "freebsd", "openbsd", "solaris", "illumos":
case "windows", "darwin", "openbsd", "solaris", "illumos":
// Enable on Windows and tailscaled-on-macOS (this doesn't
// affect the GUI clients), and on FreeBSD.
// affect the GUI clients).
return true
}
return false

View File

@ -202,6 +202,9 @@ func main() {
if routes := r.URL.Query().Get("advertise-routes"); routes != "" {
args = append(args, "--advertise-routes="+routes)
}
if snat := r.URL.Query().Get("snat-subnet-routes"); snat != "" {
args = append(args, "--snat-subnet-routes="+snat)
}
serveCmd(w, "tailscale", args...)
})
ttaMux.HandleFunc("/ip", func(w http.ResponseWriter, r *http.Request) {
@ -236,7 +239,8 @@ func main() {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello world I am %s", name)
host, _, _ := net.SplitHostPort(r.RemoteAddr)
fmt.Fprintf(w, "Hello world I am %s from %s", name, host)
})
if err := http.ListenAndServe(":"+port, mux); err != nil {
log.Printf("webserver on :%s failed: %v", port, err)

View File

@ -378,7 +378,7 @@ func (v PrefsView) Sync() opt.Bool { return v.ж.Sync }
// network to route Tailscale traffic back to the subnet relay
// machine.
//
// Linux-only.
// Linux and FreeBSD only.
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
// NoStatefulFiltering specifies whether to apply stateful filtering when

View File

@ -222,7 +222,7 @@ type Prefs struct {
// network to route Tailscale traffic back to the subnet relay
// machine.
//
// Linux-only.
// Linux and FreeBSD only.
NoSNAT bool
// NoStatefulFiltering specifies whether to apply stateful filtering when

View File

@ -162,9 +162,6 @@ func (e *Env) generateFreeBSDUserData(n *Node) string {
ud.WriteString(" - \"chmod +x /usr/local/bin/tailscaled /usr/local/bin/tailscale /usr/local/bin/tta\"\n")
// Enable IP forwarding for subnet routers.
// This is currently a noop as of 2026-04-08 because FreeBSD uses
// gvisor netstack for subnet routing until
// https://github.com/tailscale/tailscale/issues/5573 etc are fixed.
if n.advertiseRoutes != "" {
ud.WriteString(" - \"sysctl net.inet.ip.forwarding=1\"\n")
ud.WriteString(" - \"sysctl net.inet6.ip6.forwarding=1\"\n")

View File

@ -27,6 +27,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -95,14 +96,15 @@ type Node struct {
name string
num int // assigned during AddNode
os OSImage
nets []*vnet.Network
vnetNode *vnet.Node // primary vnet node (set during Start)
agent *vnet.NodeAgentClient
joinTailnet bool
advertiseRoutes string
webServerPort int
sshPort int // host port for SSH debug access (cloud VMs only)
os OSImage
nets []*vnet.Network
vnetNode *vnet.Node // primary vnet node (set during Start)
agent *vnet.NodeAgentClient
joinTailnet bool
advertiseRoutes string
snatSubnetRoutes *bool // nil means default (true)
webServerPort int
sshPort int // host port for SSH debug access (cloud VMs only)
}
// AddNode creates a new VM node. The name is used for identification and as the
@ -130,6 +132,9 @@ func (e *Env) AddNode(name string, opts ...any) *Node {
vnetOpts = append(vnetOpts, vnet.DontJoinTailnet)
case nodeOptAdvertiseRoutes:
n.advertiseRoutes = string(o)
case nodeOptSNATSubnetRoutes:
v := bool(o)
n.snatSubnetRoutes = &v
case nodeOptWebServer:
n.webServerPort = int(o)
default:
@ -154,6 +159,7 @@ func (n *Node) LanIP(net *vnet.Network) netip.Addr {
type nodeOptOS OSImage
type nodeOptNoTailscale struct{}
type nodeOptAdvertiseRoutes string
type nodeOptSNATSubnetRoutes bool
type nodeOptWebServer int
// OS returns a NodeOption that sets the node's operating system image.
@ -168,8 +174,14 @@ func AdvertiseRoutes(routes string) nodeOptAdvertiseRoutes {
return nodeOptAdvertiseRoutes(routes)
}
// SNATSubnetRoutes returns a NodeOption that sets whether the node should
// source NAT traffic to advertised subnet routes. The default is true.
// Setting this to false preserves original source IPs, which is needed
// for site-to-site configurations.
func SNATSubnetRoutes(v bool) nodeOptSNATSubnetRoutes { return nodeOptSNATSubnetRoutes(v) }
// WebServer returns a NodeOption that starts a webserver on the given port.
// The webserver responds with "Hello world I am <nodename>" on all requests.
// The webserver responds with "Hello world I am <nodename> from <sourceIP>" on all requests.
func WebServer(port int) nodeOptWebServer { return nodeOptWebServer(port) }
// Start initializes the virtual network, builds/downloads images, compiles
@ -332,6 +344,13 @@ func (e *Env) tailscaleUp(ctx context.Context, n *Node) error {
if n.advertiseRoutes != "" {
url += "&advertise-routes=" + n.advertiseRoutes
}
if n.snatSubnetRoutes != nil {
if *n.snatSubnetRoutes {
url += "&snat-subnet-routes=true"
} else {
url += "&snat-subnet-routes=false"
}
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
@ -575,7 +594,11 @@ func (e *Env) HTTPGet(from *Node, targetURL string) string {
return ""
}
// ensureGokrazy finds or builds the gokrazy base image and kernel.
var buildGokrazy sync.Once
// ensureGokrazy builds the gokrazy base image (once per test process) and
// locates the kernel. The build is fast (~4s) so we always rebuild to ensure
// the baked-in binaries (tta, tailscale, tailscaled) match the current source.
func (e *Env) ensureGokrazy(ctx context.Context) error {
if e.gokrazyBase != "" {
return nil // already found
@ -586,21 +609,23 @@ func (e *Env) ensureGokrazy(ctx context.Context) error {
return err
}
e.gokrazyBase = filepath.Join(modRoot, "gokrazy/natlabapp.qcow2")
if _, err := os.Stat(e.gokrazyBase); err != nil {
if !os.IsNotExist(err) {
return err
}
var buildErr error
buildGokrazy.Do(func() {
e.t.Logf("building gokrazy natlab image...")
cmd := exec.CommandContext(ctx, "make", "natlab")
cmd.Dir = filepath.Join(modRoot, "gokrazy")
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("make natlab: %w", err)
buildErr = fmt.Errorf("make natlab: %w", err)
}
})
if buildErr != nil {
return buildErr
}
e.gokrazyBase = filepath.Join(modRoot, "gokrazy/natlabapp.qcow2")
kernel, err := findKernelPath(filepath.Join(modRoot, "go.mod"))
if err != nil {
return fmt.Errorf("finding kernel: %w", err)

View File

@ -45,3 +45,93 @@ func testSubnetRouterForOS(t testing.TB, srOS vmtest.OSImage) {
t.Fatalf("got %q", body)
}
}
func TestSiteToSite(t *testing.T) {
testSiteToSite(t, vmtest.Ubuntu2404)
}
func TestSiteToSiteFreeBSD(t *testing.T) {
testSiteToSite(t, vmtest.FreeBSD150)
}
// testSiteToSite runs a site-to-site subnet routing test with
// --snat-subnet-routes=false, verifying that original source IPs are preserved
// across Tailscale subnet routes.
//
// Topology:
//
// Site A: backend-a (10.1.0.0/24) ← → sr-a (WAN + LAN-A)
// Site B: backend-b (10.2.0.0/24) ← → sr-b (WAN + LAN-B)
//
// Both subnet routers are on Tailscale with --snat-subnet-routes=false.
// The test sends HTTP from backend-a to backend-b through the subnet routers
// and verifies that backend-b sees backend-a's LAN IP (not the subnet router's).
func testSiteToSite(t *testing.T, srOS vmtest.OSImage) {
env := vmtest.New(t)
// WAN networks for each site (each behind NAT).
wanA := env.AddNetwork("2.1.1.1", "192.168.1.1/24", vnet.EasyNAT)
wanB := env.AddNetwork("3.1.1.1", "192.168.2.1/24", vnet.EasyNAT)
// Internal LAN for each site.
lanA := env.AddNetwork("10.1.0.1/24")
lanB := env.AddNetwork("10.2.0.1/24")
// Subnet routers: each on its WAN + LAN, advertising the local LAN,
// with SNAT disabled to preserve source IPs.
srA := env.AddNode("sr-a", wanA, lanA,
vmtest.OS(srOS),
vmtest.AdvertiseRoutes("10.1.0.0/24"),
vmtest.SNATSubnetRoutes(false))
srB := env.AddNode("sr-b", wanB, lanB,
vmtest.OS(srOS),
vmtest.AdvertiseRoutes("10.2.0.0/24"),
vmtest.SNATSubnetRoutes(false))
// Backend servers on each site's LAN (not on Tailscale).
// Use Ubuntu so we can SSH in to add static routes.
backendA := env.AddNode("backend-a", lanA,
vmtest.OS(vmtest.Ubuntu2404),
vmtest.DontJoinTailnet(),
vmtest.WebServer(8080))
backendB := env.AddNode("backend-b", lanB,
vmtest.OS(vmtest.Ubuntu2404),
vmtest.DontJoinTailnet(),
vmtest.WebServer(8080))
env.Start()
env.ApproveRoutes(srA, "10.1.0.0/24")
env.ApproveRoutes(srB, "10.2.0.0/24")
// Add static routes on the backends so that traffic to the remote site's
// subnet goes through the local subnet router. This mirrors how a real
// site-to-site deployment is configured.
srALanIP := srA.LanIP(lanA)
srBLanIP := srB.LanIP(lanB)
t.Logf("sr-a LAN IP: %s, sr-b LAN IP: %s", srALanIP, srBLanIP)
t.Logf("backend-a LAN IP: %s, backend-b LAN IP: %s", backendA.LanIP(lanA), backendB.LanIP(lanB))
if out, err := env.SSHExec(backendA, fmt.Sprintf("ip route add 10.2.0.0/24 via %s", srALanIP)); err != nil {
t.Fatalf("adding route on backend-a: %v\n%s", err, out)
}
if out, err := env.SSHExec(backendB, fmt.Sprintf("ip route add 10.1.0.0/24 via %s", srBLanIP)); err != nil {
t.Fatalf("adding route on backend-b: %v\n%s", err, out)
}
// Make an HTTP request from backend-a to backend-b through the subnet routers.
// TTA's /http-get falls back to direct dial on non-Tailscale nodes.
backendBIP := backendB.LanIP(lanB)
body := env.HTTPGet(backendA, fmt.Sprintf("http://%s:8080/", backendBIP))
t.Logf("response: %s", body)
if !strings.Contains(body, "Hello world I am backend-b") {
t.Fatalf("expected response from backend-b, got %q", body)
}
// Verify the source IP was preserved. With --snat-subnet-routes=false,
// backend-b should see backend-a's LAN IP as the source, not sr-b's LAN IP.
backendAIP := backendA.LanIP(lanA).String()
if !strings.Contains(body, "from "+backendAIP) {
t.Fatalf("source IP not preserved: expected %q in response, got %q", backendAIP, body)
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package osrouter
import "tailscale.com/wgengine/router"
func init() {
router.HookNewUserspaceRouter.Set(func(opts router.NewOpts) (router.Router, error) {
return newUserspaceBSDRouter(opts.Logf, opts.Tun, opts.NetMon, opts.Health)
})
}

View File

@ -4,24 +4,255 @@
package osrouter
import (
"fmt"
"os/exec"
"strings"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router"
)
func init() {
router.HookNewUserspaceRouter.Set(func(opts router.NewOpts) (router.Router, error) {
return newFreeBSDRouter(opts.Logf, opts.Tun, opts.NetMon, opts.Health)
})
router.HookCleanUp.Set(func(logf logger.Logf, netMon *netmon.Monitor, ifName string) {
cleanUp(logf, ifName)
})
}
// freebsdRouter extends the shared BSD userspace router with FreeBSD-specific
// IP forwarding and PF-based NAT for native subnet routing.
type freebsdRouter struct {
*userspaceBSDRouter
snatSubnetRoutes bool
}
func newFreeBSDRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (router.Router, error) {
bsd, err := newUserspaceBSDRouter(logf, tundev, netMon, health)
if err != nil {
return nil, err
}
return &freebsdRouter{userspaceBSDRouter: bsd}, nil
}
func (r *freebsdRouter) Set(cfg *router.Config) (reterr error) {
if cfg == nil {
cfg = &shutdownConfig
}
setErr := func(err error) {
if reterr == nil {
reterr = err
}
}
// Base address and route management.
if err := r.userspaceBSDRouter.Set(cfg); err != nil {
setErr(err)
}
// Enable IP forwarding when advertising subnet routes.
if len(cfg.SubnetRoutes) > 0 {
r.enableIPForwarding()
}
// Manage PF NAT rules for subnet routing.
switch {
case cfg.SNATSubnetRoutes == r.snatSubnetRoutes:
// No change needed.
case cfg.SNATSubnetRoutes:
if err := r.addPFNATRules(); err != nil {
r.logf("adding PF NAT rules: %v", err)
setErr(err)
}
default:
if err := r.delPFNATRules(); err != nil {
r.logf("removing PF NAT rules: %v", err)
setErr(err)
}
}
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
return reterr
}
func (r *freebsdRouter) enableIPForwarding() {
for _, kv := range []string{
"net.inet.ip.forwarding=1",
"net.inet6.ip6.forwarding=1",
} {
if out, err := cmd("sysctl", kv).CombinedOutput(); err != nil {
r.logf("warning: sysctl %s: %v (%s)", kv, err, strings.TrimSpace(string(out)))
}
}
}
// pfAnchorName is the PF anchor used for all Tailscale NAT/filter rules.
// Using an anchor keeps our rules isolated from the user's existing PF
// configuration; we only ever flush or modify rules inside this anchor.
const pfAnchorName = "tailscale"
// addPFNATRules configures PF to masquerade traffic from Tailscale addresses
// leaving via non-Tailscale interfaces. This is the FreeBSD equivalent of the
// Linux iptables MASQUERADE rule used for subnet routing.
//
// Rules are loaded into the "tailscale" PF anchor so that any pre-existing
// user rules in the main ruleset are left untouched.
func (r *freebsdRouter) addPFNATRules() error {
// Ensure the PF kernel module is loaded.
cmd("kldload", "pf").CombinedOutput() // may already be loaded
// Enable PF (idempotent; returns error if already enabled).
cmd("pfctl", "-e").CombinedOutput()
// Ensure the main ruleset references our anchor so PF evaluates it.
// We add both a nat-anchor (for NAT rules) and an anchor (for filter
// rules, currently just "pass" to avoid blocking) if not already present.
if err := ensurePFAnchorRef(); err != nil {
return fmt.Errorf("ensuring PF anchor reference: %w", err)
}
// Load rules into the tailscale anchor.
// Traffic from Tailscale CGNAT (100.64.0.0/10) or ULA (fd7a:115c:a1e0::/48)
// addresses exiting any non-Tailscale interface is source-NATed to the
// outgoing interface's address so that return traffic routes back through
// this node.
rules := fmt.Sprintf(
"nat on ! %s inet from 100.64.0.0/10 to any -> (self)\n"+
"nat on ! %s inet6 from fd7a:115c:a1e0::/48 to any -> (self)\n",
r.tunname, r.tunname,
)
pfctl := exec.Command("pfctl", "-a", pfAnchorName, "-f", "-")
pfctl.Stdin = strings.NewReader(rules)
out, err := pfctl.CombinedOutput()
if err != nil {
return fmt.Errorf("pfctl -a %s -f: %v (%s)", pfAnchorName, err, strings.TrimSpace(string(out)))
}
return nil
}
// getPFMainRuleset reads the current main PF filter and NAT rules.
func getPFMainRuleset() (filterRules, natRules string) {
if out, err := cmd("pfctl", "-s", "rules").CombinedOutput(); err == nil {
filterRules = string(out)
}
if out, err := cmd("pfctl", "-s", "nat").CombinedOutput(); err == nil {
natRules = string(out)
}
return
}
// loadPFMainRuleset replaces the main PF ruleset with the given combined
// NAT + filter rules.
func loadPFMainRuleset(rules string) error {
pfctl := exec.Command("pfctl", "-f", "-")
pfctl.Stdin = strings.NewReader(rules)
out, err := pfctl.CombinedOutput()
if err != nil {
return fmt.Errorf("pfctl -f: %v (%s)", err, strings.TrimSpace(string(out)))
}
return nil
}
// ensurePFAnchorRef makes sure the main PF ruleset contains nat-anchor and
// anchor references for "tailscale". Without these, PF won't evaluate our
// anchor even if it has rules loaded.
//
// We read the current main ruleset and prepend the references only if they're
// not already present, then reload the combined ruleset.
func ensurePFAnchorRef() error {
filterRules, natRules := getPFMainRuleset()
var additions string
natAnchorRef := fmt.Sprintf("nat-anchor \"%s\"", pfAnchorName)
anchorRef := fmt.Sprintf("anchor \"%s\"", pfAnchorName)
if !strings.Contains(natRules, natAnchorRef) {
additions += natAnchorRef + "\n"
}
if !strings.Contains(filterRules, anchorRef) {
additions += anchorRef + "\n"
}
if additions == "" {
return nil // already present
}
// Prepend our anchor references so they're evaluated, then include
// all existing rules so we don't disrupt the user's configuration.
return loadPFMainRuleset(additions + natRules + filterRules)
}
// removePFAnchorRef removes the nat-anchor and anchor references for
// "tailscale" from the main PF ruleset via read-modify-write, leaving
// all other rules intact.
func removePFAnchorRef() error {
filterRules, natRules := getPFMainRuleset()
natAnchorRef := fmt.Sprintf("nat-anchor \"%s\"", pfAnchorName)
anchorRef := fmt.Sprintf("anchor \"%s\"", pfAnchorName)
newNat := removeLines(natRules, natAnchorRef)
newFilter := removeLines(filterRules, anchorRef)
if newNat == natRules && newFilter == filterRules {
return nil // nothing to remove
}
return loadPFMainRuleset(newNat + newFilter)
}
// removeLines removes all lines from s that contain substr.
func removeLines(s, substr string) string {
var b strings.Builder
for line := range strings.SplitSeq(s, "\n") {
if strings.Contains(line, substr) {
continue
}
b.WriteString(line)
b.WriteByte('\n')
}
return b.String()
}
// delPFNATRules flushes rules inside the tailscale PF anchor and removes
// the anchor references from the main ruleset.
func (r *freebsdRouter) delPFNATRules() error {
// Flush rules inside the anchor.
if out, err := cmd("pfctl", "-a", pfAnchorName, "-F", "all").CombinedOutput(); err != nil {
return fmt.Errorf("pfctl -a %s -F all: %v (%s)", pfAnchorName, err, strings.TrimSpace(string(out)))
}
// Remove the anchor references from the main ruleset.
if err := removePFAnchorRef(); err != nil {
return fmt.Errorf("removing PF anchor reference: %w", err)
}
return nil
}
func (r *freebsdRouter) Close() error {
cleanUp(r.logf, r.tunname)
return nil
}
func cleanUp(logf logger.Logf, interfaceName string) {
// Flush only the tailscale PF anchor, leaving user rules intact.
if out, err := cmd("pfctl", "-a", pfAnchorName, "-F", "all").CombinedOutput(); err != nil {
logf("pfctl flush anchor %s: %v (%s)", pfAnchorName, err, strings.TrimSpace(string(out)))
}
// Remove the anchor references from the main ruleset.
if err := removePFAnchorRef(); err != nil {
logf("removing PF anchor ref: %v", err)
}
// If the interface was left behind, ifconfig down will not remove it.
// In fact, this will leave a system in a tainted state where starting tailscaled
// will result in "interface tailscale0 already exists"
// until the defunct interface is ifconfig-destroyed.
ifup := []string{"ifconfig", interfaceName, "destroy"}
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
if out, err := cmd("ifconfig", interfaceName, "destroy").CombinedOutput(); err != nil {
logf("ifconfig destroy: %v\n%s", err, out)
}
}

View File

@ -22,12 +22,6 @@ import (
"tailscale.com/wgengine/router"
)
func init() {
router.HookNewUserspaceRouter.Set(func(opts router.NewOpts) (router.Router, error) {
return newUserspaceBSDRouter(opts.Logf, opts.Tun, opts.NetMon, opts.Health)
})
}
type userspaceBSDRouter struct {
logf logger.Logf
netMon *netmon.Monitor
@ -37,7 +31,7 @@ type userspaceBSDRouter struct {
routes map[netip.Prefix]bool
}
func newUserspaceBSDRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (router.Router, error) {
func newUserspaceBSDRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (*userspaceBSDRouter, error) {
tunname, err := tundev.Name()
if err != nil {
return nil, err

View File

@ -131,8 +131,11 @@ type Config struct {
// flow logging and is otherwise ignored.
SubnetRoutes []netip.Prefix
// SNATSubnetRoutes enables SNAT for traffic to local subnets.
// Implemented on Linux (iptables/nftables) and FreeBSD (PF).
SNATSubnetRoutes bool
// Linux-only things below, ignored on other platforms.
SNATSubnetRoutes bool // SNAT traffic to local subnets
StatefulFiltering bool // Apply stateful filtering to inbound connections
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
NetfilterKind string // what kind of netfilter to use ("nftables", "iptables", or "" to auto-detect)