WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -95,9 +95,21 @@ func tsChain(chain string) string {
|
||||
}
|
||||
|
||||
// DelLoopbackRule removes the iptables rule permitting loopback
|
||||
// traffic to a Tailscale IP.
|
||||
// traffic to a Tailscale IP. A missing rule is not an error: an address
|
||||
// left on the interface by a previous tailscaled instance never went
|
||||
// through AddLoopbackRule in this one, so removing it must not be
|
||||
// blocked by the absence of its loopback rule.
|
||||
func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
ipt := i.getIPTByAddr(addr)
|
||||
args := []string{"-i", "lo", "-s", addr.String(), "-j", "ACCEPT"}
|
||||
exists, err := ipt.Exists("filter", "ts-input", args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
if err := ipt.Delete("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -557,3 +557,36 @@ func TestAddAndDelCGNATRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDelLoopbackRuleMissing verifies DelLoopbackRule is a no-op (not an error)
|
||||
// when the rule is absent, so removing an address whose loopback rule was never
|
||||
// added in this instance -- e.g. one left on the interface by a previous
|
||||
// tailscaled -- isn't blocked. See tailscale/tailscale#19974.
|
||||
func TestDelLoopbackRuleMissing(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner()
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer iptr.DelChains()
|
||||
|
||||
addr := netip.MustParseAddr("100.64.0.99")
|
||||
rule := []string{"-i", "lo", "-s", addr.String(), "-j", "ACCEPT"}
|
||||
|
||||
// No AddLoopbackRule for addr, so its rule is absent. Delete must not error.
|
||||
if err := iptr.DelLoopbackRule(addr); err != nil {
|
||||
t.Fatalf("DelLoopbackRule with no rule present: %v", err)
|
||||
}
|
||||
|
||||
// And it still deletes the rule when present.
|
||||
if err := iptr.AddLoopbackRule(addr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := iptr.DelLoopbackRule(addr); err != nil {
|
||||
t.Fatalf("DelLoopbackRule with rule present: %v", err)
|
||||
}
|
||||
if exists, err := iptr.ipt4.Exists("filter", "ts-input", rule...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exists {
|
||||
t.Error("loopback rule still present after DelLoopbackRule")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package osrouter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -27,12 +28,14 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tsconst"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/linuxfw"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -82,6 +85,7 @@ type linuxRouter struct {
|
||||
|
||||
mu sync.Mutex
|
||||
addrs map[netip.Prefix]bool
|
||||
lastScanAddrs set.Set[netip.Prefix] // desired addrs at the last successful orphan scan; nil until the first scan
|
||||
routes map[netip.Prefix]bool
|
||||
localRoutes map[netip.Prefix]bool
|
||||
snatSubnetRoutes bool
|
||||
@@ -450,12 +454,48 @@ func (r *linuxRouter) Set(cfg *router.Config) error {
|
||||
}
|
||||
r.routes = newRoutes
|
||||
|
||||
prevAddrs := r.addrs
|
||||
newAddrs, err := cidrDiff("addr", r.addrs, cfg.LocalAddrs, r.addAddress, r.delAddress, r.logf)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
r.addrs = newAddrs
|
||||
|
||||
// r.addrs only tracks what this instance configured, so it misses
|
||||
// Tailscale addresses a previous instance left on a persistent tailscale0.
|
||||
// After the reconcile above (so our own addresses and their loopback rules
|
||||
// are installed first), sweep any Tailscale-range interface address that
|
||||
// isn't desired and that we don't already track (prevAddrs). Like cidrDiff,
|
||||
// this trusts cfg.LocalAddrs to be authoritative. See #19974.
|
||||
//
|
||||
// TODO(bcreane): a late orphan with no config change -- IPv6 becoming
|
||||
// available, or an external re-add -- isn't caught here; re-run this sweep on
|
||||
// netmon.ChangeDelta to handle it. See tailscale/corp#43882.
|
||||
wantAddrs := set.SetOf(cfg.LocalAddrs)
|
||||
if r.lastScanAddrs == nil || !r.lastScanAddrs.Equal(wantAddrs) {
|
||||
if kernelAddrs, err := r.tailscaleInterfaceAddrs(); err != nil {
|
||||
r.logf("router: enumerating interface addresses failed, skipping orphan cleanup: %v", err)
|
||||
} else {
|
||||
r.lastScanAddrs = wantAddrs
|
||||
orphaned := orphanedAddrs(kernelAddrs, cfg.LocalAddrs)
|
||||
removed := make([]netip.Prefix, 0, len(orphaned))
|
||||
for _, p := range orphaned {
|
||||
if prevAddrs[p] {
|
||||
continue // an address we were tracking; cidrDiff already handled it
|
||||
}
|
||||
if err := r.delAddress(p); err != nil {
|
||||
r.logf("router: removing stale address %v from %s failed: %v", p, r.tunname, err)
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
removed = append(removed, p)
|
||||
}
|
||||
if len(removed) > 0 {
|
||||
r.logf("router: removed %d stale Tailscale address(es) from %s left by a previous instance: %v", len(removed), r.tunname, removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the SNAT rule is added or removed as needed.
|
||||
switch {
|
||||
case cfg.SNATSubnetRoutes == r.snatSubnetRoutes:
|
||||
@@ -844,11 +884,19 @@ func (r *linuxRouter) setNetfilterModeLocked(mode preftype.NetfilterMode) error
|
||||
// getV6FilteringAvailable returns true if the router is able to setup the
|
||||
// required tailscale filter rules for IPv6.
|
||||
func (r *linuxRouter) getV6FilteringAvailable() bool {
|
||||
if r.nfr == nil {
|
||||
return false
|
||||
}
|
||||
return r.nfr.HasIPV6() && r.nfr.HasIPV6Filter()
|
||||
}
|
||||
|
||||
// getV6Available returns true if the host supports IPv6.
|
||||
// getV6Available reports whether the router can manage IPv6. r.nfr can be nil if
|
||||
// setupNetfilterLocked failed earlier in Set (which continues on error), so
|
||||
// treat a nil runner as no IPv6 rather than dereferencing it.
|
||||
func (r *linuxRouter) getV6Available() bool {
|
||||
if r.nfr == nil {
|
||||
return false
|
||||
}
|
||||
return r.nfr.HasIPV6()
|
||||
}
|
||||
|
||||
@@ -904,6 +952,92 @@ func (r *linuxRouter) delAddress(addr netip.Prefix) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcilableTailscaleIP reports whether ip, found on the tunnel interface, is
|
||||
// a Tailscale-range address the router can actually delete. delAddress no-ops on
|
||||
// IPv6 when IPv6 is unavailable, so excluding those avoids treating a no-op
|
||||
// "deletion" as success.
|
||||
func (r *linuxRouter) reconcilableTailscaleIP(ip netip.Addr) bool {
|
||||
ip = ip.Unmap()
|
||||
if !tsaddr.IsTailscaleIP(ip) {
|
||||
return false
|
||||
}
|
||||
return !ip.Is6() || r.getV6Available()
|
||||
}
|
||||
|
||||
// tailscaleInterfaceAddrs returns the addresses on the tunnel interface that are
|
||||
// within Tailscale's ranges and that the router can act on (see
|
||||
// reconcilableTailscaleIP); all others are excluded so they're never candidates
|
||||
// for removal. It's a filtered subset, and errors if the interface can't be read.
|
||||
func (r *linuxRouter) tailscaleInterfaceAddrs() ([]netip.Prefix, error) {
|
||||
if r.useIPCommand() {
|
||||
return r.tailscaleInterfaceAddrsIPCommand()
|
||||
}
|
||||
link, err := r.link()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret []netip.Prefix
|
||||
for _, a := range addrs {
|
||||
if a.IPNet == nil {
|
||||
continue
|
||||
}
|
||||
pfx, ok := netipx.FromStdIPNet(a.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Preserve the kernel's prefix length so a later delete matches.
|
||||
if r.reconcilableTailscaleIP(pfx.Addr()) {
|
||||
ret = append(ret, pfx)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// tailscaleInterfaceAddrsIPCommand is the "ip" command implementation of
|
||||
// tailscaleInterfaceAddrs, used in tests and when TS_DEBUG_USE_IP_COMMAND is
|
||||
// set.
|
||||
func (r *linuxRouter) tailscaleInterfaceAddrsIPCommand() ([]netip.Prefix, error) {
|
||||
out, err := r.cmd.output("ip", "-oneline", "addr", "show", "dev", r.tunname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret []netip.Prefix
|
||||
for line := range bytes.Lines(out) {
|
||||
// `ip -oneline addr show` puts each address on one line as
|
||||
// "inet <cidr>" or "inet6 <cidr>".
|
||||
fields := strings.Fields(string(line))
|
||||
for i := 0; i+1 < len(fields); i++ {
|
||||
if fields[i] != "inet" && fields[i] != "inet6" {
|
||||
continue
|
||||
}
|
||||
p, err := netip.ParsePrefix(fields[i+1])
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if r.reconcilableTailscaleIP(p.Addr()) {
|
||||
ret = append(ret, p)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// orphanedAddrs returns the addresses in kernelAddrs that are not in desired,
|
||||
// i.e. the stale addresses left on the interface that the sweep should remove.
|
||||
func orphanedAddrs(kernelAddrs, desired []netip.Prefix) []netip.Prefix {
|
||||
if len(kernelAddrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
s := set.SetOf(kernelAddrs)
|
||||
s.DeleteSlice(desired)
|
||||
return s.Slice()
|
||||
}
|
||||
|
||||
// addLoopbackRule adds a firewall rule to permit loopback traffic to
|
||||
// a local Tailscale IP.
|
||||
func (r *linuxRouter) addLoopbackRule(addr netip.Addr) error {
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
"tailscale.com/util/linuxfw"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -561,7 +562,9 @@ type fakeIPTablesRunner struct {
|
||||
t *testing.T
|
||||
ipt4 map[string][]string
|
||||
ipt6 map[string][]string
|
||||
// we always assume ipv6 and ipv6 nat are enabled when testing
|
||||
// we always assume ipv6 and ipv6 nat are enabled when testing, unless
|
||||
// noV6 is set.
|
||||
noV6 bool
|
||||
|
||||
addChainsErr error // if non-nil, AddChains returns it instead of setting up chains
|
||||
addConnmarkSaveCalls int
|
||||
@@ -1004,7 +1007,7 @@ func (n *fakeIPTablesRunner) DelExternalCGNATRules(mode linuxfw.CGNATMode, tunna
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) HasIPV6() bool { return true }
|
||||
func (n *fakeIPTablesRunner) HasIPV6() bool { return !n.noV6 }
|
||||
func (n *fakeIPTablesRunner) HasIPV6NAT() bool { return true }
|
||||
func (n *fakeIPTablesRunner) HasIPV6Filter() bool { return true }
|
||||
|
||||
@@ -1125,6 +1128,12 @@ func (o *fakeOS) run(args ...string) error {
|
||||
switch args[2] {
|
||||
case "add":
|
||||
if slices.Contains(*ls, rest) {
|
||||
// addAddress uses netlink AddrReplace in production, which is
|
||||
// idempotent; model that for addresses rather than erroring.
|
||||
// Routes/rules keep strict add semantics.
|
||||
if args[1] == "addr" {
|
||||
return nil
|
||||
}
|
||||
o.t.Errorf("can't add %q, already present", rest)
|
||||
return errors.New("already exists")
|
||||
}
|
||||
@@ -1162,8 +1171,29 @@ func (o *fakeOS) run(args ...string) error {
|
||||
}
|
||||
|
||||
func (o *fakeOS) output(args ...string) ([]byte, error) {
|
||||
want := "ip rule list priority 10000"
|
||||
got := strings.Join(args, " ")
|
||||
|
||||
if got == "ip -oneline addr show dev tailscale0" {
|
||||
// Render o.ips (entries look like "<cidr> dev tailscale0") in a
|
||||
// simplified `ip -oneline addr show` format that exposes the family token
|
||||
// the parser keys off of.
|
||||
var ret []string
|
||||
for _, e := range o.ips {
|
||||
cidr, _, _ := strings.Cut(e, " ")
|
||||
p, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fam := "inet"
|
||||
if p.Addr().Is6() {
|
||||
fam = "inet6"
|
||||
}
|
||||
ret = append(ret, fmt.Sprintf("3: tailscale0 %s %s scope global tailscale0", fam, cidr))
|
||||
}
|
||||
return []byte(strings.Join(ret, "\n")), nil
|
||||
}
|
||||
|
||||
want := "ip rule list priority 10000"
|
||||
if got != want {
|
||||
o.t.Errorf("unexpected command that wants output: %v", got)
|
||||
return nil, errExec
|
||||
@@ -1609,3 +1639,297 @@ func TestSetSkipsNetfilterAddonsWhenSetupFails(t *testing.T) {
|
||||
nfr.addExternalCGNATCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// newTestLinuxRouter builds a linuxRouter backed by a fakeOS, brought up and
|
||||
// ready for Set, mirroring TestSetSkipsNetfilterAddonsWhenSetupFails.
|
||||
func newTestLinuxRouter(t *testing.T) (*linuxRouter, *fakeOS) {
|
||||
t.Helper()
|
||||
bus := eventbus.New()
|
||||
t.Cleanup(bus.Close)
|
||||
mon, err := netmon.New(bus, logger.Discard)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mon.Start()
|
||||
t.Cleanup(func() { mon.Close() })
|
||||
|
||||
fake := NewFakeOS(t)
|
||||
ht := health.NewTracker(bus)
|
||||
r, err := newUserspaceRouterAdvanced(logger.Discard, "tailscale0", mon, fake, ht, bus)
|
||||
if err != nil {
|
||||
t.Fatalf("newUserspaceRouterAdvanced: %v", err)
|
||||
}
|
||||
lr := r.(*linuxRouter)
|
||||
lr.nfr = fake.nfr
|
||||
if err := lr.Up(); err != nil {
|
||||
t.Fatalf("Up: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { lr.Close() })
|
||||
return lr, fake
|
||||
}
|
||||
|
||||
// TestSetRemovesOrphanedTailscaleAddrs verifies that Set removes Tailscale-range
|
||||
// addresses left on the interface by a previous instance (issue 19974), even
|
||||
// though they're absent from the in-memory r.addrs map.
|
||||
func TestSetRemovesOrphanedTailscaleAddrs(t *testing.T) {
|
||||
lr, fake := newTestLinuxRouter(t)
|
||||
|
||||
// Simulate a tailscale0 that survived a restart still carrying a prior
|
||||
// profile's CGNAT v4 and Tailscale ULA v6 addresses, plus a non-Tailscale
|
||||
// address that must be left alone.
|
||||
fake.ips = []string{
|
||||
"100.64.0.99/32 dev tailscale0", // CGNAT v4 orphan
|
||||
"fd7a:115c:a1e0::99/128 dev tailscale0", // ULA v6 orphan
|
||||
"192.168.1.5/24 dev tailscale0", // non-Tailscale, leave alone
|
||||
}
|
||||
slices.Sort(fake.ips)
|
||||
|
||||
cfg := &Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32"),
|
||||
NetfilterMode: netfilterOff,
|
||||
}
|
||||
if err := lr.Set(cfg); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(fake.ips, "100.64.0.1/32 dev tailscale0") {
|
||||
t.Errorf("desired addr 100.64.0.1/32 not present; ips=%q", fake.ips)
|
||||
}
|
||||
for _, gone := range []string{
|
||||
"100.64.0.99/32 dev tailscale0",
|
||||
"fd7a:115c:a1e0::99/128 dev tailscale0",
|
||||
} {
|
||||
if slices.Contains(fake.ips, gone) {
|
||||
t.Errorf("orphaned Tailscale addr %q was not removed; ips=%q", gone, fake.ips)
|
||||
}
|
||||
}
|
||||
// Non-Tailscale address must be untouched.
|
||||
if !slices.Contains(fake.ips, "192.168.1.5/24 dev tailscale0") {
|
||||
t.Errorf("non-Tailscale addr 192.168.1.5/24 was wrongly removed; ips=%q", fake.ips)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetRemovesOrphanWithNetfilter verifies orphan removal also works with
|
||||
// netfilter enabled, where delAddress additionally tears down the address's
|
||||
// loopback rule. An orphan from a previous instance has no such rule, so this
|
||||
// guards that removing it isn't blocked by the loopback-rule deletion (see the
|
||||
// iptables DelLoopbackRule DeleteIfExists handling).
|
||||
func TestSetRemovesOrphanWithNetfilter(t *testing.T) {
|
||||
lr, fake := newTestLinuxRouter(t)
|
||||
|
||||
fake.ips = []string{
|
||||
"100.64.0.99/32 dev tailscale0", // CGNAT v4 orphan, no loopback rule
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32"),
|
||||
NetfilterMode: netfilterOn,
|
||||
}
|
||||
if err := lr.Set(cfg); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
|
||||
if slices.Contains(fake.ips, "100.64.0.99/32 dev tailscale0") {
|
||||
t.Errorf("orphan was not removed with netfilter on; ips=%q", fake.ips)
|
||||
}
|
||||
if !slices.Contains(fake.ips, "100.64.0.1/32 dev tailscale0") {
|
||||
t.Errorf("desired addr 100.64.0.1/32 not present; ips=%q", fake.ips)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetInstallsLoopbackRuleForExistingAddr covers the persisted-interface
|
||||
// restart case: the node's own address is already on tailscale0 (so the kernel
|
||||
// reports it), but its per-address loopback rule was flushed when the netfilter
|
||||
// chains were rebuilt. Set must still install the loopback rule, i.e. it must
|
||||
// not skip addAddress just because the address is already present -- which it
|
||||
// would if a desired address were folded into the orphan set. See #19974.
|
||||
func TestSetInstallsLoopbackRuleForExistingAddr(t *testing.T) {
|
||||
lr, fake := newTestLinuxRouter(t)
|
||||
|
||||
// The node's own address persisted on the interface across the restart.
|
||||
fake.ips = []string{
|
||||
"100.64.0.1/32 dev tailscale0",
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32"),
|
||||
NetfilterMode: netfilterOn,
|
||||
}
|
||||
if err := lr.Set(cfg); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
|
||||
nfr := fake.nfr.(*fakeIPTablesRunner)
|
||||
wantRule := "-i lo -s 100.64.0.1 -j ACCEPT"
|
||||
if !slices.Contains(nfr.ipt4["filter/ts-input"], wantRule) {
|
||||
t.Errorf("loopback rule %q not installed for already-present addr; ts-input=%q",
|
||||
wantRule, nfr.ipt4["filter/ts-input"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetOrphanScanGatedByAddrChange verifies the interface is scanned for
|
||||
// orphans on the first Set and when LocalAddrs changes, but not for route-only
|
||||
// updates -- so a subnet router churning routes doesn't pay for a scan on every
|
||||
// Set.
|
||||
func TestSetOrphanScanGatedByAddrChange(t *testing.T) {
|
||||
lr, fake := newTestLinuxRouter(t)
|
||||
|
||||
// First Set establishes steady state (scans, finds nothing to remove).
|
||||
if err := lr.Set(&Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32"),
|
||||
NetfilterMode: netfilterOff,
|
||||
}); err != nil {
|
||||
t.Fatalf("Set 1: %v", err)
|
||||
}
|
||||
|
||||
// An orphan appears afterward. A route-only update leaves LocalAddrs
|
||||
// unchanged, so the scan is skipped and the orphan stays in place.
|
||||
fake.ips = append(fake.ips, "100.64.0.99/32 dev tailscale0")
|
||||
slices.Sort(fake.ips)
|
||||
if err := lr.Set(&Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32"),
|
||||
Routes: mustCIDRs("10.0.0.0/24"),
|
||||
NetfilterMode: netfilterOff,
|
||||
}); err != nil {
|
||||
t.Fatalf("Set 2 (route-only): %v", err)
|
||||
}
|
||||
if !slices.Contains(fake.ips, "100.64.0.99/32 dev tailscale0") {
|
||||
t.Errorf("orphan removed on a route-only update; scan should have been skipped; ips=%q", fake.ips)
|
||||
}
|
||||
|
||||
// When LocalAddrs changes, the scan runs again and the orphan is removed.
|
||||
if err := lr.Set(&Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.2/32"),
|
||||
Routes: mustCIDRs("10.0.0.0/24"),
|
||||
NetfilterMode: netfilterOff,
|
||||
}); err != nil {
|
||||
t.Fatalf("Set 3 (addr change): %v", err)
|
||||
}
|
||||
if slices.Contains(fake.ips, "100.64.0.99/32 dev tailscale0") {
|
||||
t.Errorf("orphan not removed after LocalAddrs change; ips=%q", fake.ips)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetOrphanScanNotRetriedOnDuplicateLocalAddrs verifies the scan gate
|
||||
// compares the desired address set rather than its length against r.addrs. A
|
||||
// duplicate in LocalAddrs (or, equivalently, an address that fails to install)
|
||||
// leaves r.addrs smaller than len(LocalAddrs); a length-based gate would then
|
||||
// rescan on every Set. The set comparison treats the desired set as unchanged.
|
||||
func TestSetOrphanScanNotRetriedOnDuplicateLocalAddrs(t *testing.T) {
|
||||
lr, fake := newTestLinuxRouter(t)
|
||||
|
||||
// LocalAddrs with a duplicate: cidrDiff dedups, so r.addrs ends up smaller
|
||||
// than len(LocalAddrs).
|
||||
dupCfg := &Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32", "100.64.0.1/32"),
|
||||
NetfilterMode: netfilterOff,
|
||||
}
|
||||
if err := lr.Set(dupCfg); err != nil {
|
||||
t.Fatalf("Set 1: %v", err)
|
||||
}
|
||||
|
||||
// An orphan appears; re-applying the same (duplicated) LocalAddrs must not
|
||||
// trigger a rescan, so the orphan stays.
|
||||
fake.ips = append(fake.ips, "100.64.0.99/32 dev tailscale0")
|
||||
slices.Sort(fake.ips)
|
||||
if err := lr.Set(dupCfg); err != nil {
|
||||
t.Fatalf("Set 2: %v", err)
|
||||
}
|
||||
if !slices.Contains(fake.ips, "100.64.0.99/32 dev tailscale0") {
|
||||
t.Errorf("rescan ran for an unchanged (duplicated) LocalAddrs; ips=%q", fake.ips)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetKeepsNonTailscaleAddrs is the safety check: Set must never remove
|
||||
// addresses outside Tailscale's ranges, even when they're not in the config.
|
||||
func TestSetKeepsNonTailscaleAddrs(t *testing.T) {
|
||||
lr, fake := newTestLinuxRouter(t)
|
||||
|
||||
fake.ips = []string{
|
||||
"192.168.1.5/24 dev tailscale0", // non-Tailscale v4
|
||||
"fe80::1/64 dev tailscale0", // link-local v6
|
||||
}
|
||||
slices.Sort(fake.ips)
|
||||
|
||||
cfg := &Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32"),
|
||||
NetfilterMode: netfilterOff,
|
||||
}
|
||||
if err := lr.Set(cfg); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
|
||||
for _, keep := range []string{
|
||||
"192.168.1.5/24 dev tailscale0",
|
||||
"fe80::1/64 dev tailscale0",
|
||||
} {
|
||||
if !slices.Contains(fake.ips, keep) {
|
||||
t.Errorf("non-Tailscale addr %q was wrongly removed; ips=%q", keep, fake.ips)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrphanedAddrs(t *testing.T) {
|
||||
p := netip.MustParsePrefix
|
||||
// orphanedAddrs returns set-iteration order, so compare as sets.
|
||||
got := set.SetOf(orphanedAddrs(
|
||||
[]netip.Prefix{p("100.64.0.1/32"), p("100.64.0.99/32"), p("fd7a:115c:a1e0::99/128")},
|
||||
[]netip.Prefix{p("100.64.0.1/32")},
|
||||
))
|
||||
want := set.SetOf([]netip.Prefix{p("100.64.0.99/32"), p("fd7a:115c:a1e0::99/128")})
|
||||
if !got.Equal(want) {
|
||||
t.Errorf("orphanedAddrs = %v; want %v", got.Slice(), want.Slice())
|
||||
}
|
||||
if orphanedAddrs(nil, []netip.Prefix{p("100.64.0.1/32")}) != nil {
|
||||
t.Error("orphanedAddrs(nil, ...) should be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetV6AvailableNilNFR verifies the v6-availability checks don't panic when
|
||||
// r.nfr is nil, which happens if setupNetfilterLocked failed earlier in Set.
|
||||
// The orphan sweep reaches getV6Available via reconcilableTailscaleIP, so this
|
||||
// must not deref a nil runner.
|
||||
func TestGetV6AvailableNilNFR(t *testing.T) {
|
||||
r := &linuxRouter{} // nfr left nil
|
||||
if r.getV6Available() {
|
||||
t.Error("getV6Available() = true with nil nfr; want false")
|
||||
}
|
||||
if r.getV6FilteringAvailable() {
|
||||
t.Error("getV6FilteringAvailable() = true with nil nfr; want false")
|
||||
}
|
||||
if r.reconcilableTailscaleIP(netip.MustParseAddr("fd7a:115c:a1e0::99")) {
|
||||
t.Error("reconcilableTailscaleIP(v6) = true with nil nfr; want false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetSkipsV6OrphansWhenV6Unavailable verifies that when IPv6 is
|
||||
// unavailable, Set does not enumerate v6 orphans for removal. delAddress
|
||||
// no-ops on v6 in that state, so enumerating them would let cidrDiff record a
|
||||
// delete that never happened; instead we leave them out of the reconcile.
|
||||
func TestSetSkipsV6OrphansWhenV6Unavailable(t *testing.T) {
|
||||
lr, fake := newTestLinuxRouter(t)
|
||||
fake.nfr.(*fakeIPTablesRunner).noV6 = true
|
||||
|
||||
fake.ips = []string{
|
||||
"100.64.0.99/32 dev tailscale0", // CGNAT v4 orphan, removable
|
||||
"fd7a:115c:a1e0::99/128 dev tailscale0", // ULA v6 orphan, not removable without v6
|
||||
}
|
||||
slices.Sort(fake.ips)
|
||||
|
||||
cfg := &Config{
|
||||
LocalAddrs: mustCIDRs("100.64.0.1/32"),
|
||||
NetfilterMode: netfilterOff,
|
||||
}
|
||||
if err := lr.Set(cfg); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
|
||||
// The v4 orphan is removed; the v6 orphan is left in place rather than
|
||||
// being treated as removed via a no-op delete.
|
||||
if slices.Contains(fake.ips, "100.64.0.99/32 dev tailscale0") {
|
||||
t.Errorf("v4 orphan was not removed; ips=%q", fake.ips)
|
||||
}
|
||||
if !slices.Contains(fake.ips, "fd7a:115c:a1e0::99/128 dev tailscale0") {
|
||||
t.Errorf("v6 orphan should be left alone when v6 is unavailable; ips=%q", fake.ips)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user