diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index a950f7dec..a2ede1d98 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -60,6 +60,7 @@ var upFlagSet = (func() *flag.FlagSet { upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel") upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes") upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic") + upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node") upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections") upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")") upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key") @@ -81,21 +82,22 @@ func defaultNetfilterMode() string { } var upArgs struct { - reset bool - server string - acceptRoutes bool - acceptDNS bool - singleRoutes bool - exitNodeIP string - shieldsUp bool - forceReauth bool - advertiseRoutes string - advertiseDefaultRoute bool - advertiseTags string - snat bool - netfilterMode string - authKey string - hostname string + reset bool + server string + acceptRoutes bool + acceptDNS bool + singleRoutes bool + exitNodeIP string + exitNodeAllowLANAccess bool + shieldsUp bool + forceReauth bool + advertiseRoutes string + advertiseDefaultRoute bool + advertiseTags string + snat bool + netfilterMode string + authKey string + hostname string } func warnf(format string, args ...interface{}) { @@ -182,6 +184,8 @@ func runUp(ctx context.Context, args []string) error { if err != nil { fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err) } + } else if upArgs.exitNodeAllowLANAccess { + fatalf("--exit-node-allow-lan-access can only be used with --exit-node") } if !exitNodeIP.IsZero() { @@ -212,6 +216,7 @@ func runUp(ctx context.Context, args []string) error { prefs.WantRunning = true prefs.RouteAll = upArgs.acceptRoutes prefs.ExitNodeIP = exitNodeIP + prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess prefs.CorpDNS = upArgs.acceptDNS prefs.AllowSingleHosts = upArgs.singleRoutes prefs.ShieldsUp = upArgs.shieldsUp @@ -413,6 +418,7 @@ func init() { addPrefFlagMapping("shields-up", "ShieldsUp") addPrefFlagMapping("snat-subnet-routes", "NoSNAT") addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeIP") + addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess") } func addPrefFlagMapping(flagName string, prefNames ...string) { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 4b6e3b7d9..abb3aa866 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -821,13 +821,9 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{ tsaddr.TailscaleULARange(), } -// shrinkDefaultRoute returns an IPSet representing the IPs in route, -// minus those in removeFromDefaultRoute and local interface subnets. -func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) { +func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) { var b netaddr.IPSetBuilder - b.AddPrefix(route) - var hostIPs []netaddr.IP - err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) { + if err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) { if tsaddr.IsTailscaleIP(pfx.IP) { return } @@ -835,11 +831,26 @@ func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) { return } hostIPs = append(hostIPs, pfx.IP) - b.RemovePrefix(pfx) - }) + b.AddPrefix(pfx) + }); err != nil { + return nil, nil, err + } + + return b.IPSet(), hostIPs, nil +} + +// shrinkDefaultRoute returns an IPSet representing the IPs in route, +// minus those in removeFromDefaultRoute and local interface subnets. +func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) { + interfaceRoutes, hostIPs, err := interfaceRoutes() if err != nil { return nil, err } + var b netaddr.IPSetBuilder + // Add the default route. + b.AddPrefix(route) + // Remove the local interface routes. + b.RemoveSet(interfaceRoutes) // Having removed all the LAN subnets, re-add the hosts's own // IPs. It's fine for clients to connect to an exit node's public @@ -1542,7 +1553,7 @@ func (b *LocalBackend) authReconfig() { return } - rcfg := routerConfig(cfg, uc) + rcfg := b.routerConfig(cfg, uc) var dcfg dns.Config @@ -1794,7 +1805,7 @@ func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netaddr.IPPref } // routerConfig produces a router.Config from a wireguard config and IPN prefs. -func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { +func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { rs := &router.Config{ LocalAddrs: unmapIPPrefixes(cfg.Addresses), SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes), @@ -1812,9 +1823,10 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { if prefs.ExitNodeID != "" || !prefs.ExitNodeIP.IsZero() { var default4, default6 bool for _, route := range rs.Routes { - if route == ipv4Default { + switch route { + case ipv4Default: default4 = true - } else if route == ipv6Default { + case ipv6Default: default6 = true } if default4 && default6 { @@ -1827,6 +1839,17 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { if !default6 { rs.Routes = append(rs.Routes, ipv6Default) } + ips, _, err := interfaceRoutes() + if err != nil { + b.logf("failed to discover interface ips: %v", err) + } + if prefs.ExitNodeAllowLANAccess { + rs.LocalRoutes = ips.Prefixes() + } else { + // Explicitly add routes to the local network so that we do not + // leak any traffic. + rs.Routes = append(rs.Routes, ips.Prefixes()...) + } } rs.Routes = append(rs.Routes, netaddr.IPPrefix{ diff --git a/ipn/prefs.go b/ipn/prefs.go index 6fc4ce69a..530dca59f 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -66,6 +66,10 @@ type Prefs struct { ExitNodeID tailcfg.StableNodeID ExitNodeIP netaddr.IP + // ExitNodeAllowLANAccess indicates whether locally accessible subnets should be + // routed directly or via the exit node. + ExitNodeAllowLANAccess bool + // CorpDNS specifies whether to install the Tailscale network's // DNS configuration, if it exists. CorpDNS bool @@ -152,23 +156,24 @@ type Prefs struct { type MaskedPrefs struct { Prefs - ControlURLSet bool `json:",omitempty"` - RouteAllSet bool `json:",omitempty"` - AllowSingleHostsSet bool `json:",omitempty"` - ExitNodeIDSet bool `json:",omitempty"` - ExitNodeIPSet bool `json:",omitempty"` - CorpDNSSet bool `json:",omitempty"` - WantRunningSet bool `json:",omitempty"` - ShieldsUpSet bool `json:",omitempty"` - AdvertiseTagsSet bool `json:",omitempty"` - HostnameSet bool `json:",omitempty"` - OSVersionSet bool `json:",omitempty"` - DeviceModelSet bool `json:",omitempty"` - NotepadURLsSet bool `json:",omitempty"` - ForceDaemonSet bool `json:",omitempty"` - AdvertiseRoutesSet bool `json:",omitempty"` - NoSNATSet bool `json:",omitempty"` - NetfilterModeSet bool `json:",omitempty"` + ControlURLSet bool `json:",omitempty"` + RouteAllSet bool `json:",omitempty"` + AllowSingleHostsSet bool `json:",omitempty"` + ExitNodeIDSet bool `json:",omitempty"` + ExitNodeIPSet bool `json:",omitempty"` + ExitNodeAllowLANAccessSet bool `json:",omitempty"` + CorpDNSSet bool `json:",omitempty"` + WantRunningSet bool `json:",omitempty"` + ShieldsUpSet bool `json:",omitempty"` + AdvertiseTagsSet bool `json:",omitempty"` + HostnameSet bool `json:",omitempty"` + OSVersionSet bool `json:",omitempty"` + DeviceModelSet bool `json:",omitempty"` + NotepadURLsSet bool `json:",omitempty"` + ForceDaemonSet bool `json:",omitempty"` + AdvertiseRoutesSet bool `json:",omitempty"` + NoSNATSet bool `json:",omitempty"` + NetfilterModeSet bool `json:",omitempty"` } // ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs @@ -237,9 +242,9 @@ func (p *Prefs) pretty(goos string) string { sb.WriteString("shields=true ") } if !p.ExitNodeIP.IsZero() { - fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeIP) + fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess) } else if !p.ExitNodeID.IsZero() { - fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeID) + fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeID, p.ExitNodeAllowLANAccess) } if len(p.AdvertiseRoutes) > 0 || goos == "linux" { fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes) @@ -290,6 +295,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool { p.AllowSingleHosts == p2.AllowSingleHosts && p.ExitNodeID == p2.ExitNodeID && p.ExitNodeIP == p2.ExitNodeIP && + p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess && p.CorpDNS == p2.CorpDNS && p.WantRunning == p2.WantRunning && p.NotepadURLs == p2.NotepadURLs && diff --git a/ipn/prefs_clone.go b/ipn/prefs_clone.go index 9e426a4fa..14e811ade 100644 --- a/ipn/prefs_clone.go +++ b/ipn/prefs_clone.go @@ -33,22 +33,23 @@ func (src *Prefs) Clone() *Prefs { // A compilation failure here means this code must be regenerated, with command: // tailscale.com/cmd/cloner -type Prefs var _PrefsNeedsRegeneration = Prefs(struct { - ControlURL string - RouteAll bool - AllowSingleHosts bool - ExitNodeID tailcfg.StableNodeID - ExitNodeIP netaddr.IP - CorpDNS bool - WantRunning bool - ShieldsUp bool - AdvertiseTags []string - Hostname string - OSVersion string - DeviceModel string - NotepadURLs bool - ForceDaemon bool - AdvertiseRoutes []netaddr.IPPrefix - NoSNAT bool - NetfilterMode preftype.NetfilterMode - Persist *persist.Persist + ControlURL string + RouteAll bool + AllowSingleHosts bool + ExitNodeID tailcfg.StableNodeID + ExitNodeIP netaddr.IP + ExitNodeAllowLANAccess bool + CorpDNS bool + WantRunning bool + ShieldsUp bool + AdvertiseTags []string + Hostname string + OSVersion string + DeviceModel string + NotepadURLs bool + ForceDaemon bool + AdvertiseRoutes []netaddr.IPPrefix + NoSNAT bool + NetfilterMode preftype.NetfilterMode + Persist *persist.Persist }{}) diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index 251bf9c13..d50c11ad0 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -33,7 +33,7 @@ func fieldsOf(t reflect.Type) (fields []string) { func TestPrefsEqual(t *testing.T) { tstest.PanicOnLog() - prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"} + prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "ExitNodeAllowLANAccess", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"} if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) { t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n", have, prefsHandles) @@ -124,6 +124,17 @@ func TestPrefsEqual(t *testing.T) { true, }, + { + &Prefs{}, + &Prefs{ExitNodeAllowLANAccess: true}, + false, + }, + { + &Prefs{ExitNodeAllowLANAccess: true}, + &Prefs{ExitNodeAllowLANAccess: true}, + true, + }, + { &Prefs{CorpDNS: true}, &Prefs{CorpDNS: false}, @@ -384,14 +395,29 @@ func TestPrefsPretty(t *testing.T) { ExitNodeIP: netaddr.MustParseIP("1.2.3.4"), }, "linux", - `Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 routes=[] nf=off Persist=nil}`, + `Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off Persist=nil}`, }, { Prefs{ ExitNodeID: tailcfg.StableNodeID("myNodeABC"), }, "linux", - `Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC routes=[] nf=off Persist=nil}`, + `Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off Persist=nil}`, + }, + { + Prefs{ + ExitNodeID: tailcfg.StableNodeID("myNodeABC"), + ExitNodeAllowLANAccess: true, + }, + "linux", + `Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off Persist=nil}`, + }, + { + Prefs{ + ExitNodeAllowLANAccess: true, + }, + "linux", + `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}`, }, { Prefs{ diff --git a/wgengine/router/router.go b/wgengine/router/router.go index 11fce3302..647aec07b 100644 --- a/wgengine/router/router.go +++ b/wgengine/router/router.go @@ -51,12 +51,17 @@ type Config struct { // IPv6/128 (Tailscale ULA). LocalAddrs []netaddr.IPPrefix - // Routes are the routes that point in to the Tailscale + // Routes are the routes that point into the Tailscale // interface. These are the /32 and /128 routes to peers, as // well as any other subnets that peers are advertising and // this node has chosen to use. Routes []netaddr.IPPrefix + // LocalRoutes are the routes that should not be routed through Tailscale. + // There are no priorities set in how these routes are added, normal + // routing rules apply. + LocalRoutes []netaddr.IPPrefix + // Linux-only things below, ignored on other platforms. SubnetRoutes []netaddr.IPPrefix // subnets being advertised to other Tailscale nodes SNATSubnetRoutes bool // SNAT traffic to local subnets diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index 334f3fd57..60012ce9f 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -59,21 +59,26 @@ const ( tailscaleBypassMark = "0x80000" ) -// tailscaleRouteTable is the routing table number for Tailscale -// network routes. See addIPRules for the detailed policy routing -// logic that ends up doing lookups within that table. -// -// NOTE(danderson): We chose 52 because those are the digits above the -// letters "TS" on a qwerty keyboard, and 52 is sufficiently unlikely -// to be picked by other software. -// -// NOTE(danderson): You might wonder why we didn't pick some high -// table number like 5252, to further avoid the potential for -// collisions with other software. Unfortunately, Busybox's `ip` -// implementation believes that table numbers are 8-bit integers, so -// for maximum compatibility we have to stay in the 0-255 range even -// though linux itself supports larger numbers. -const tailscaleRouteTable = "52" +const ( + defaultRouteTable = "default" + mainRouteTable = "main" + + // tailscaleRouteTable is the routing table number for Tailscale + // network routes. See addIPRules for the detailed policy routing + // logic that ends up doing lookups within that table. + // + // NOTE(danderson): We chose 52 because those are the digits above the + // letters "TS" on a qwerty keyboard, and 52 is sufficiently unlikely + // to be picked by other software. + // + // NOTE(danderson): You might wonder why we didn't pick some high + // table number like 5252, to further avoid the potential for + // collisions with other software. Unfortunately, Busybox's `ip` + // implementation believes that table numbers are 8-bit integers, so + // for maximum compatibility we have to stay in the 0-255 range even + // though linux itself supports larger numbers. + tailscaleRouteTable = "52" +) // netfilterRunner abstracts helpers to run netfilter commands. It // exists purely to swap out go-iptables for a fake implementation in @@ -93,6 +98,7 @@ type linuxRouter struct { tunname string addrs map[netaddr.IPPrefix]bool routes map[netaddr.IPPrefix]bool + localRoutes map[netaddr.IPPrefix]bool snatSubnetRoutes bool netfilterMode preftype.NetfilterMode @@ -185,9 +191,13 @@ func (r *linuxRouter) Close() error { if err := r.setNetfilterMode(netfilterOff); err != nil { return err } + if err := r.delRoutes(); err != nil { + return err + } r.addrs = nil r.routes = nil + r.localRoutes = nil return nil } @@ -203,6 +213,12 @@ func (r *linuxRouter) Set(cfg *Config) error { errs = append(errs, err) } + newLocalRoutes, err := cidrDiff("localRoute", r.localRoutes, cfg.LocalRoutes, r.addThrowRoute, r.delThrowRoute, r.logf) + if err != nil { + errs = append(errs, err) + } + r.localRoutes = newLocalRoutes + newRoutes, err := cidrDiff("route", r.routes, cfg.Routes, r.addRoute, r.delRoute, r.logf) if err != nil { errs = append(errs, err) @@ -432,14 +448,25 @@ func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error { // interface. Fails if the route already exists, or if adding the // route fails. func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error { - if !r.v6Available && cidr.IP.Is6() { + return r.addRouteDef([]string{normalizeCIDR(cidr), "dev", r.tunname}, cidr) +} + +// addThrowRoute adds a throw route for the provided cidr. +// This has the effect that lookup in the routing table is terminated +// pretending that no route was found. Fails if the route already exists, +// or if adding the route fails. +func (r *linuxRouter) addThrowRoute(cidr netaddr.IPPrefix) error { + if !r.ipRuleAvailable { return nil } - args := []string{ - "ip", "route", "add", - normalizeCIDR(cidr), - "dev", r.tunname, + return r.addRouteDef([]string{"throw", normalizeCIDR(cidr)}, cidr) +} + +func (r *linuxRouter) addRouteDef(routeDef []string, cidr netaddr.IPPrefix) error { + if !r.v6Available && cidr.IP.Is6() { + return nil } + args := append([]string{"ip", "route", "add"}, routeDef...) if r.ipRuleAvailable { args = append(args, "table", tailscaleRouteTable) } @@ -450,20 +477,29 @@ func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error { // interface. Fails if the route doesn't exist, or if removing the // route fails. func (r *linuxRouter) delRoute(cidr netaddr.IPPrefix) error { - if !r.v6Available && cidr.IP.Is6() { + return r.delRouteDef([]string{normalizeCIDR(cidr), "dev", r.tunname}, cidr) +} + +// delThrowRoute removes the throw route for the cidr. Fails if the route +// doesn't exist, or if removing the route fails. +func (r *linuxRouter) delThrowRoute(cidr netaddr.IPPrefix) error { + if !r.ipRuleAvailable { return nil } - args := []string{ - "ip", "route", "del", - normalizeCIDR(cidr), - "dev", r.tunname, + return r.delRouteDef([]string{"throw", normalizeCIDR(cidr)}, cidr) +} + +func (r *linuxRouter) delRouteDef(routeDef []string, cidr netaddr.IPPrefix) error { + if !r.v6Available && cidr.IP.Is6() { + return nil } + args := append([]string{"ip", "route", "del"}, routeDef...) if r.ipRuleAvailable { args = append(args, "table", tailscaleRouteTable) } err := r.cmd.run(args...) if err != nil { - ok, err := r.hasRoute(cidr) + ok, err := r.hasRoute(routeDef, cidr) if err != nil { r.logf("warning: error checking whether %v even exists after error deleting it: %v", err) } else { @@ -483,12 +519,8 @@ func dashFam(ip netaddr.IP) string { return "-4" } -func (r *linuxRouter) hasRoute(cidr netaddr.IPPrefix) (bool, error) { - args := []string{ - "ip", dashFam(cidr.IP), "route", "show", - normalizeCIDR(cidr), - "dev", r.tunname, - } +func (r *linuxRouter) hasRoute(routeDef []string, cidr netaddr.IPPrefix) (bool, error) { + args := append([]string{"ip", dashFam(cidr.IP), "route", "show"}, routeDef...) if r.ipRuleAvailable { args = append(args, "table", tailscaleRouteTable) } @@ -551,7 +583,7 @@ func (r *linuxRouter) addIPRules() error { "ip", family, "rule", "add", "pref", tailscaleRouteTable+"10", "fwmark", tailscaleBypassMark, - "table", "main", + "table", mainRouteTable, ) // ...and then we try the 'default' table, for correctness, // even though it's been empty on every Linux system I've ever seen. @@ -559,7 +591,7 @@ func (r *linuxRouter) addIPRules() error { "ip", family, "rule", "add", "pref", tailscaleRouteTable+"30", "fwmark", tailscaleBypassMark, - "table", "default", + "table", defaultRouteTable, ) // If neither of those matched (no default route on this system?) // then packets from us should be aborted rather than falling through @@ -590,7 +622,18 @@ func (r *linuxRouter) addIPRules() error { return rg.ErrAcc } -// delBypassrule removes the policy routing rules that avoid +// delRoutes removes any local routes that we added that would not be +// cleaned up on interface down. +func (r *linuxRouter) delRoutes() error { + for rt := range r.localRoutes { + if err := r.delThrowRoute(rt); err != nil { + r.logf("failed to delete throw route(%q): %v", rt, err) + } + } + return nil +} + +// delIPRules removes the policy routing rules that avoid // tailscaled routing loops, if it exists. func (r *linuxRouter) delIPRules() error { if !r.ipRuleAvailable { diff --git a/wgengine/router/router_linux_test.go b/wgengine/router/router_linux_test.go index 45109d35c..d4a12ac3e 100644 --- a/wgengine/router/router_linux_test.go +++ b/wgengine/router/router_linux_test.go @@ -280,6 +280,54 @@ v6/filter/ts-forward -o tailscale0 -j ACCEPT v6/nat/POSTROUTING -j ts-postrouting `, }, + { + name: "addr, routes, and local routes with netfilter", + in: &Config{ + LocalAddrs: mustCIDRs("100.101.102.104/10"), + Routes: mustCIDRs("100.100.100.100/32", "0.0.0.0/0"), + LocalRoutes: mustCIDRs("10.0.0.0/8"), + NetfilterMode: netfilterOn, + }, + want: ` +up +ip addr add 100.101.102.104/10 dev tailscale0 +ip route add 0.0.0.0/0 dev tailscale0 table 52 +ip route add 100.100.100.100/32 dev tailscale0 table 52 +ip route add throw 10.0.0.0/8 table 52` + basic + + `v4/filter/FORWARD -j ts-forward +v4/filter/INPUT -j ts-input +v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 +v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT +v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP +v4/filter/ts-forward -o tailscale0 -j ACCEPT +v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT +v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN +v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP +v4/nat/POSTROUTING -j ts-postrouting +v6/filter/FORWARD -j ts-forward +v6/filter/INPUT -j ts-input +v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 +v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT +v6/filter/ts-forward -o tailscale0 -j ACCEPT +v6/nat/POSTROUTING -j ts-postrouting +`, + }, + { + name: "addr, routes, and local routes with no netfilter", + in: &Config{ + LocalAddrs: mustCIDRs("100.101.102.104/10"), + Routes: mustCIDRs("100.100.100.100/32", "0.0.0.0/0"), + LocalRoutes: mustCIDRs("10.0.0.0/8", "192.168.0.0/24"), + NetfilterMode: netfilterOff, + }, + want: ` +up +ip addr add 100.101.102.104/10 dev tailscale0 +ip route add 0.0.0.0/0 dev tailscale0 table 52 +ip route add 100.100.100.100/32 dev tailscale0 table 52 +ip route add throw 10.0.0.0/8 table 52 +ip route add throw 192.168.0.0/24 table 52` + basic, + }, } fake := NewFakeOS(t)