mirror of
https://github.com/tailscale/tailscale.git
synced 2026-07-20 21:23:07 +08:00
wgengine/router/osrouter: remove orphaned tailnet addrs on cleanup (#20304)
* net/tsaddr: unmap IPv4-mapped IPv6 addrs in IsTailscaleIP IsTailscaleIP branched on ip.Is4() before checking the CGNAT range, so an IPv4-mapped IPv6 address (e.g. ::ffff:100.64.0.1) took the IPv6 path and was tested only against the ULA range, wrongly returning false for a Tailscale CGNAT address. Unmap at the top so both forms are classified identically; Unmap is cheap and IsTailscaleIPv4 stays IPv4-only for callers that need it. Signed-off-by: Brendan Creane <bcreane@gmail.com> * wgengine/router/osrouter: remove orphaned tailnet addrs on cleanup The orphan-address sweep added in #20199 ran only inside Router.Set, so the teardown path (tailscaled --cleanup, and the unconditional cleanup at daemon start) never removed stale Tailscale addresses a previous instance left on a persistent tailscale0 -- it only flushed iptables/nftables. Wire address removal into cleanUp: with no desired config, every Tailscale-range address on the interface is an orphan, so enumerate and delete them all (IPv4 and IPv6, best-effort) in removeOrphanedAddrsForCleanup. tailscaleInterfaceAddrs now yields the interface's addresses as an iter.Seq[netip.Prefix], and the filters compose lazily over it: tailscaleAddrs (every Tailscale-range address; used by cleanup), deletableAddrs (isDeletableAddr: Tailscale-range and deletable now, i.e. excluding v6 when v6 is unavailable; used by the live Set sweep), and orphanedAddrs (drops the desired addresses). The Set sweep ranges the composed iterator directly, so no throwaway slices are built. delAddress is made idempotent: it attempts both the loopback-rule teardown and the address delete and joins their errors, so a missing firewall rule can't leak the address, and it no longer no-ops on v6 (cleanup relies on that to remove v6 orphans even when this process never brought IPv6 up). The Set-time sweep is otherwise unchanged; re-running it on network changes (netmon) for late orphans remains a follow-up (tailscale/corp#43882). Updates #19974 Fixes tailscale/corp#44173 Signed-off-by: Brendan Creane <bcreane@gmail.com> --------- Signed-off-by: Brendan Creane <bcreane@gmail.com>
This commit is contained in:
parent
0433cc6929
commit
bab3f5fce7
@ -70,6 +70,7 @@ func TailscaleServiceIPv6() netip.Addr {
|
||||
// IsTailscaleIP reports whether IP is an IP address in a range that
|
||||
// Tailscale assigns from.
|
||||
func IsTailscaleIP(ip netip.Addr) bool {
|
||||
ip = ip.Unmap()
|
||||
if ip.Is4() {
|
||||
return IsTailscaleIPv4(ip)
|
||||
}
|
||||
@ -78,6 +79,7 @@ func IsTailscaleIP(ip netip.Addr) bool {
|
||||
|
||||
// IsTailscaleIPv4 reports whether an IPv4 IP is an IP address that
|
||||
// Tailscale assigns from.
|
||||
// It will always return false if ip is an "IPv4-mapped IPv6 address".
|
||||
func IsTailscaleIPv4(ip netip.Addr) bool {
|
||||
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
|
||||
}
|
||||
|
||||
@ -250,6 +250,12 @@ func TestIsTailscaleIPv4(t *testing.T) {
|
||||
in: netip.MustParseAddr("100.115.92.157"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
// IsTailscaleIPv4 does not unmap, so an IPv4-mapped IPv6 form of a
|
||||
// CGNAT address is not an IPv4 address and must return false.
|
||||
in: netip.AddrFrom16(netip.MustParseAddr("100.67.19.57").As16()),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := IsTailscaleIPv4(tt.in); got != tt.want {
|
||||
@ -284,6 +290,16 @@ func TestIsTailscaleIP(t *testing.T) {
|
||||
in: netip.MustParseAddr("100.115.92.157"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
// IPv4-mapped IPv6 form of a CGNAT address is still a Tailscale IP.
|
||||
in: netip.MustParseAddr("::ffff:100.67.19.57"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// IPv4-mapped IPv6 form of a non-Tailscale address.
|
||||
in: netip.MustParseAddr("::ffff:10.10.10.10"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := IsTailscaleIP(tt.in); got != tt.want {
|
||||
|
||||
@ -10,11 +10,13 @@
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -491,13 +493,13 @@ func (r *linuxRouter) Set(cfg *router.Config) error {
|
||||
// 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 {
|
||||
if ifaceAddrs, 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 {
|
||||
kernelAddrs := r.deletableAddrs(ifaceAddrs)
|
||||
var removed []netip.Prefix
|
||||
for p := range orphanedAddrs(kernelAddrs, cfg.LocalAddrs) {
|
||||
if prevAddrs[p] {
|
||||
continue // an address we were tracking; cidrDiff already handled it
|
||||
}
|
||||
@ -1012,37 +1014,45 @@ func (r *linuxRouter) addAddress(addr netip.Prefix) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// delAddress removes an IP/mask from the tunnel interface. Fails if
|
||||
// the address is not assigned to the interface, or if the removal
|
||||
// fails.
|
||||
// delAddress removes an IP/mask from the tunnel interface. It attempts both the
|
||||
// loopback-rule teardown and the address deletion even if the former fails, so a
|
||||
// missing firewall rule can't leak the address; errors from both are joined.
|
||||
func (r *linuxRouter) delAddress(addr netip.Prefix) error {
|
||||
if !r.getV6Available() && addr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
var errs []error
|
||||
if err := r.delLoopbackRule(addr.Addr()); err != nil {
|
||||
return err
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := r.delAddrRaw(addr); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// delAddrRaw deletes addr from the tunnel interface without the loopback-rule
|
||||
// teardown that [linuxRouter.delAddress] does.
|
||||
func (r *linuxRouter) delAddrRaw(addr netip.Prefix) error {
|
||||
if r.useIPCommand() {
|
||||
if err := r.cmd.run("ip", "addr", "del", addr.String(), "dev", r.tunname); err != nil {
|
||||
return fmt.Errorf("deleting address %q from tunnel interface: %w", addr, err)
|
||||
}
|
||||
} else {
|
||||
link, err := r.link()
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting address %v, %w", addr, err)
|
||||
}
|
||||
if err := netlink.AddrDel(link, nlAddrOfPrefix(addr)); err != nil {
|
||||
return fmt.Errorf("deleting address %v from tunnel interface: %w", addr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
link, err := r.link()
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting address %v, %w", addr, err)
|
||||
}
|
||||
if err := netlink.AddrDel(link, nlAddrOfPrefix(addr)); err != nil {
|
||||
return fmt.Errorf("deleting address %v from tunnel interface: %w", addr, err)
|
||||
}
|
||||
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 {
|
||||
// isDeletableAddr reports whether ip, found on the tunnel interface, is a
|
||||
// Tailscale-range address the live router should delete. It excludes IPv6 when
|
||||
// IPv6 is unavailable so the Set-time sweep doesn't churn on addresses this
|
||||
// instance couldn't have installed; the teardown path uses [tailscaleAddrs]
|
||||
// instead and removes every Tailscale-range address.
|
||||
func (r *linuxRouter) isDeletableAddr(ip netip.Addr) bool {
|
||||
ip = ip.Unmap()
|
||||
if !tsaddr.IsTailscaleIP(ip) {
|
||||
return false
|
||||
@ -1050,11 +1060,34 @@ func (r *linuxRouter) reconcilableTailscaleIP(ip netip.Addr) bool {
|
||||
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) {
|
||||
// tailscaleAddrs yields only the Tailscale-range addresses from addrs, per
|
||||
// [tsaddr.IsTailscaleIP].
|
||||
func tailscaleAddrs(addrs iter.Seq[netip.Prefix]) iter.Seq[netip.Prefix] {
|
||||
return func(yield func(netip.Prefix) bool) {
|
||||
for p := range addrs {
|
||||
if tsaddr.IsTailscaleIP(p.Addr()) && !yield(p) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deletableAddrs yields the addresses from addrs the live router should delete,
|
||||
// per [linuxRouter.isDeletableAddr].
|
||||
func (r *linuxRouter) deletableAddrs(addrs iter.Seq[netip.Prefix]) iter.Seq[netip.Prefix] {
|
||||
return func(yield func(netip.Prefix) bool) {
|
||||
for p := range addrs {
|
||||
if r.isDeletableAddr(p.Addr()) && !yield(p) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tailscaleInterfaceAddrs yields the addresses on the tunnel interface,
|
||||
// preserving each kernel prefix length so a later delete matches. It errors if
|
||||
// the interface can't be read.
|
||||
func (r *linuxRouter) tailscaleInterfaceAddrs() (iter.Seq[netip.Prefix], error) {
|
||||
if r.useIPCommand() {
|
||||
return r.tailscaleInterfaceAddrsIPCommand()
|
||||
}
|
||||
@ -1071,22 +1104,17 @@ func (r *linuxRouter) tailscaleInterfaceAddrs() ([]netip.Prefix, error) {
|
||||
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()) {
|
||||
if pfx, ok := netipx.FromStdIPNet(a.IPNet); ok {
|
||||
ret = append(ret, pfx)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
return slices.Values(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) {
|
||||
// [linuxRouter.tailscaleInterfaceAddrs], used in tests and when
|
||||
// TS_DEBUG_USE_IP_COMMAND is set.
|
||||
func (r *linuxRouter) tailscaleInterfaceAddrsIPCommand() (iter.Seq[netip.Prefix], error) {
|
||||
out, err := r.cmd.output("ip", "-oneline", "addr", "show", "dev", r.tunname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1104,24 +1132,24 @@ func (r *linuxRouter) tailscaleInterfaceAddrsIPCommand() ([]netip.Prefix, error)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if r.reconcilableTailscaleIP(p.Addr()) {
|
||||
ret = append(ret, p)
|
||||
}
|
||||
ret = append(ret, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
return slices.Values(ret), nil
|
||||
}
|
||||
|
||||
// orphanedAddrs returns the addresses in kernelAddrs that are not in desired,
|
||||
// orphanedAddrs yields 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
|
||||
func orphanedAddrs(kernelAddrs iter.Seq[netip.Prefix], desired []netip.Prefix) iter.Seq[netip.Prefix] {
|
||||
want := set.SetOf(desired)
|
||||
return func(yield func(netip.Prefix) bool) {
|
||||
for p := range kernelAddrs {
|
||||
if !want.Contains(p) && !yield(p) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
s := set.SetOf(kernelAddrs)
|
||||
s.DeleteSlice(desired)
|
||||
return s.Slice()
|
||||
}
|
||||
|
||||
// addLoopbackRule adds a firewall rule to permit loopback traffic to
|
||||
@ -1910,10 +1938,49 @@ func platformCanNetfilter() bool {
|
||||
// The function calls cleanUp for both iptables and nftables since which ever
|
||||
// netfilter runner is used, the cleanUp function for the other one doesn't do anything.
|
||||
func cleanUp(logf logger.Logf, interfaceName string) {
|
||||
if interfaceName != "userspace-networking" && platformCanNetfilter() {
|
||||
if interfaceName == "userspace-networking" {
|
||||
return
|
||||
}
|
||||
if platformCanNetfilter() {
|
||||
linuxfw.IPTablesCleanUp(logf)
|
||||
linuxfw.NfTablesCleanUp(logf)
|
||||
}
|
||||
removeOrphanedAddrsForCleanup(logf, osCommandRunner{ambientCapNetAdmin: useAmbientCaps()}, interfaceName)
|
||||
}
|
||||
|
||||
// removeOrphanedAddrsForCleanup removes every Tailscale-range address from
|
||||
// interfaceName. On the teardown path (tailscaled --cleanup, and the cleanup at
|
||||
// daemon start) there is no desired config or running router, so every such
|
||||
// address is an orphan. Best-effort: failures are logged, not returned.
|
||||
func removeOrphanedAddrsForCleanup(logf logger.Logf, cmd commandRunner, interfaceName string) {
|
||||
// A minimal router suffices: delAddress reaches delLoopbackRule, which
|
||||
// returns early because netfilterMode is netfilterOff (the zero value), so
|
||||
// the nil nfr/netMon/health are never dereferenced. logf is wrapped to match
|
||||
// the live router's "router: " prefix.
|
||||
r := &linuxRouter{
|
||||
logf: logger.WithPrefix(logf, "router: "),
|
||||
tunname: interfaceName,
|
||||
cmd: cmd,
|
||||
}
|
||||
ifaceAddrs, err := r.tailscaleInterfaceAddrs()
|
||||
if err != nil {
|
||||
r.logf("enumerating %s addresses for cleanup failed: %v", interfaceName, err)
|
||||
return
|
||||
}
|
||||
// Unlike the live sweep's deletableAddrs, tailscaleAddrs keeps IPv6 too: a
|
||||
// previous instance may have left a ULA orphan even though this process never
|
||||
// brought IPv6 up. delAddress is idempotent, so a no-op v6 delete is harmless.
|
||||
var removed []netip.Prefix
|
||||
for p := range tailscaleAddrs(ifaceAddrs) {
|
||||
if err := r.delAddress(p); err != nil {
|
||||
r.logf("removing stale address %v from %s during cleanup failed: %v", p, interfaceName, err)
|
||||
continue
|
||||
}
|
||||
removed = append(removed, p)
|
||||
}
|
||||
if len(removed) > 0 {
|
||||
r.logf("removed %d stale Tailscale address(es) from %s during cleanup: %v", len(removed), interfaceName, removed)
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the running openWRT system is using mwan3, based on the heuristic
|
||||
|
||||
@ -1177,13 +1177,18 @@ func (o *fakeOS) run(args ...string) error {
|
||||
func (o *fakeOS) output(args ...string) ([]byte, error) {
|
||||
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.
|
||||
if dev, ok := strings.CutPrefix(got, "ip -oneline addr show dev "); ok {
|
||||
// Render o.ips (entries look like "<cidr> dev <ifname>") in a simplified
|
||||
// `ip -oneline addr show` format that exposes the family token the parser
|
||||
// keys off of. Only addresses on the requested device are returned, so the
|
||||
// fake models a real multi-interface host: `show dev tailscale0` never
|
||||
// reports an address that lives on eth0.
|
||||
var ret []string
|
||||
for _, e := range o.ips {
|
||||
cidr, _, _ := strings.Cut(e, " ")
|
||||
cidr, rest, _ := strings.Cut(e, " ")
|
||||
if rest != "dev "+dev {
|
||||
continue // address is on a different interface
|
||||
}
|
||||
p, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
continue
|
||||
@ -1192,7 +1197,7 @@ func (o *fakeOS) output(args ...string) ([]byte, error) {
|
||||
if p.Addr().Is6() {
|
||||
fam = "inet6"
|
||||
}
|
||||
ret = append(ret, fmt.Sprintf("3: tailscale0 %s %s scope global tailscale0", fam, cidr))
|
||||
ret = append(ret, fmt.Sprintf("3: %s %s %s scope global %s", dev, fam, cidr, dev))
|
||||
}
|
||||
return []byte(strings.Join(ret, "\n")), nil
|
||||
}
|
||||
@ -1855,10 +1860,15 @@ func TestSetOrphanScanNotRetriedOnDuplicateLocalAddrs(t *testing.T) {
|
||||
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
|
||||
keep := []string{
|
||||
"192.168.1.5/24 dev tailscale0", // non-Tailscale v4
|
||||
"fe80::1/64 dev tailscale0", // link-local v6
|
||||
"100.115.92.5/32 dev tailscale0", // ChromeOS VM range: in CGNAT, not a Tailscale IP
|
||||
"100.63.0.1/32 dev tailscale0", // just below CGNAT 100.64.0.0/10
|
||||
"100.128.0.1/32 dev tailscale0", // just above CGNAT
|
||||
"fd7a:115c:a1e1::1/128 dev tailscale0", // adjacent to the Tailscale ULA /48, not in it
|
||||
}
|
||||
fake.ips = append([]string(nil), keep...)
|
||||
slices.Sort(fake.ips)
|
||||
|
||||
cfg := &Config{
|
||||
@ -1869,35 +1879,32 @@ func TestSetKeepsNonTailscaleAddrs(t *testing.T) {
|
||||
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)
|
||||
for _, k := range keep {
|
||||
if !slices.Contains(fake.ips, k) {
|
||||
t.Errorf("non-Tailscale addr %q was wrongly removed; ips=%q", k, 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")},
|
||||
// orphanedAddrs yields in kernelAddrs order, but compare as sets to be safe.
|
||||
got := set.SetOf(slices.Collect(orphanedAddrs(
|
||||
slices.Values([]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")
|
||||
if got := slices.Collect(orphanedAddrs(slices.Values([]netip.Prefix(nil)), []netip.Prefix{p("100.64.0.1/32")})); got != nil {
|
||||
t.Errorf("orphanedAddrs(nil, ...) = %v; want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// The orphan sweep reaches getV6Available via isDeletableAddr, so this
|
||||
// must not deref a nil runner.
|
||||
func TestGetV6AvailableNilNFR(t *testing.T) {
|
||||
r := &linuxRouter{} // nfr left nil
|
||||
@ -1907,8 +1914,8 @@ func TestGetV6AvailableNilNFR(t *testing.T) {
|
||||
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")
|
||||
if r.isDeletableAddr(netip.MustParseAddr("fd7a:115c:a1e0::99")) {
|
||||
t.Error("isDeletableAddr(v6) = true with nil nfr; want false")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1997,3 +2004,111 @@ func TestSetSnapshotsV6Usable(t *testing.T) {
|
||||
t.Errorf("interfaceV6Usable called %d times during one Set; want 1 (snapshotted)", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCleanUpRemovesAllTailscaleAddrs verifies the teardown path removes every
|
||||
// Tailscale-range address (IPv4 CGNAT and IPv6 ULA) while leaving non-Tailscale
|
||||
// addresses alone.
|
||||
func TestCleanUpRemovesAllTailscaleAddrs(t *testing.T) {
|
||||
fake := NewFakeOS(t)
|
||||
fake.ips = []string{
|
||||
"100.64.0.1/32 dev tailscale0", // CGNAT v4
|
||||
"fd7a:115c:a1e0::1/128 dev tailscale0", // ULA v6
|
||||
"192.168.1.5/24 dev tailscale0", // non-Tailscale, leave alone
|
||||
"fe80::1/64 dev tailscale0", // link-local v6, leave alone
|
||||
}
|
||||
slices.Sort(fake.ips)
|
||||
|
||||
removeOrphanedAddrsForCleanup(t.Logf, fake, "tailscale0")
|
||||
|
||||
for _, gone := range []string{
|
||||
"100.64.0.1/32 dev tailscale0",
|
||||
"fd7a:115c:a1e0::1/128 dev tailscale0",
|
||||
} {
|
||||
if slices.Contains(fake.ips, gone) {
|
||||
t.Errorf("Tailscale addr %q was not removed during cleanup; ips=%q", gone, fake.ips)
|
||||
}
|
||||
}
|
||||
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 during cleanup; ips=%q", keep, fake.ips)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCleanUpRemovesV6OrphanWithoutNetfilter is the teardown counterpart to
|
||||
// TestSetSkipsV6OrphansWhenV6Unavailable: with a nil netfilter runner (so
|
||||
// getV6Available is false), cleanup must still remove an IPv6 ULA orphan.
|
||||
func TestCleanUpRemovesV6OrphanWithoutNetfilter(t *testing.T) {
|
||||
fake := NewFakeOS(t)
|
||||
fake.ips = []string{
|
||||
"fd7a:115c:a1e0::99/128 dev tailscale0", // ULA v6 orphan
|
||||
}
|
||||
|
||||
removeOrphanedAddrsForCleanup(t.Logf, fake, "tailscale0")
|
||||
|
||||
if slices.Contains(fake.ips, "fd7a:115c:a1e0::99/128 dev tailscale0") {
|
||||
t.Errorf("v6 orphan was not removed during cleanup; ips=%q", fake.ips)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCleanUpKeepsNearRangeAddrs verifies the teardown sweep removes real
|
||||
// Tailscale orphans while leaving addresses at the edges of Tailscale's ranges
|
||||
// untouched. In particular the ChromeOS VM range is inside CGNAT 100.64.0.0/10
|
||||
// but excluded by tsaddr.IsTailscaleIP, so a naive CGNAT-prefix check would
|
||||
// wrongly delete it.
|
||||
func TestCleanUpKeepsNearRangeAddrs(t *testing.T) {
|
||||
fake := NewFakeOS(t)
|
||||
remove := []string{
|
||||
"100.64.0.99/32 dev tailscale0", // CGNAT v4 orphan
|
||||
"fd7a:115c:a1e0::99/128 dev tailscale0", // Tailscale ULA v6 orphan
|
||||
}
|
||||
keep := []string{
|
||||
"100.115.92.5/32 dev tailscale0", // ChromeOS VM range: in CGNAT, not a Tailscale IP
|
||||
"100.63.0.1/32 dev tailscale0", // just below CGNAT
|
||||
"100.128.0.1/32 dev tailscale0", // just above CGNAT
|
||||
"fd7a:115c:a1e1::1/128 dev tailscale0", // adjacent to the Tailscale ULA /48
|
||||
"192.168.1.5/24 dev tailscale0", // non-Tailscale v4
|
||||
"fe80::1/64 dev tailscale0", // link-local v6
|
||||
}
|
||||
fake.ips = append(append([]string(nil), remove...), keep...)
|
||||
slices.Sort(fake.ips)
|
||||
|
||||
removeOrphanedAddrsForCleanup(t.Logf, fake, "tailscale0")
|
||||
|
||||
for _, r := range remove {
|
||||
if slices.Contains(fake.ips, r) {
|
||||
t.Errorf("orphan %q was not removed during cleanup; ips=%q", r, fake.ips)
|
||||
}
|
||||
}
|
||||
for _, k := range keep {
|
||||
if !slices.Contains(fake.ips, k) {
|
||||
t.Errorf("near-range non-Tailscale addr %q was wrongly removed during cleanup; ips=%q", k, fake.ips)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCleanUpOnlyTouchesTunInterface guards that the sweep is scoped to the
|
||||
// tunnel interface. 100.64.0.0/10 is shared ISP CGNAT space, so a host may carry
|
||||
// a non-Tailscale CGNAT address on its WAN/other interface; the sweep enumerates
|
||||
// and deletes only on tailscale0, so such an address on eth0 must never be
|
||||
// touched even though it's in the CGNAT range.
|
||||
func TestCleanUpOnlyTouchesTunInterface(t *testing.T) {
|
||||
fake := NewFakeOS(t)
|
||||
fake.ips = []string{
|
||||
"100.64.0.99/32 dev tailscale0", // our orphan on the tun -> removed
|
||||
"100.64.0.5/32 dev eth0", // someone else's CGNAT on WAN -> must survive
|
||||
}
|
||||
slices.Sort(fake.ips)
|
||||
|
||||
removeOrphanedAddrsForCleanup(t.Logf, fake, "tailscale0")
|
||||
|
||||
if slices.Contains(fake.ips, "100.64.0.99/32 dev tailscale0") {
|
||||
t.Errorf("tailscale0 orphan was not removed; ips=%q", fake.ips)
|
||||
}
|
||||
if !slices.Contains(fake.ips, "100.64.0.5/32 dev eth0") {
|
||||
t.Errorf("non-Tailscale CGNAT addr on eth0 was wrongly removed; ips=%q", fake.ips)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user