Skip to content

fix: refresh routes when Tailscale account changes on same utun#16

Merged
GeiserX merged 1 commit intoGeiserX:mainfrom
karle0wne:fix/tailscale-multi-account-isolated
Mar 5, 2026
Merged

fix: refresh routes when Tailscale account changes on same utun#16
GeiserX merged 1 commit intoGeiserX:mainfrom
karle0wne:fix/tailscale-multi-account-isolated

Conversation

@karle0wne
Copy link
Contributor

@karle0wne karle0wne commented Mar 5, 2026

This patch fixes a practical Tailscale edge case: switching between Tailscale accounts/profiles while staying on the same utun interface.

In the current behavior, routes may stay tied to the previous profile because the app only reacts to connect/disconnect or interface changes. That means users can switch account, keep utun6, and still have stale bypass routes.

What this patch changes:

  • Adds a lightweight fingerprint of the active local Tailscale identity (ID/UserID/DNSName/TailscaleIPs).
  • During periodic VPN checks, compares previous and current fingerprint.
  • If fingerprint changes while VPN is still up on the same interface, it triggers a route refresh (removeAllRoutes + applyAllRoutes).
  • Keeps the existing cooldown and safety guards to avoid duplicate reroutes.
  • Improves Tailscale status reads by requesting only self data (tailscale status --json --self --peers=false) to avoid large JSON/timeouts on big tailnets.
  • Refactors duplicated Tailscale JSON reading into one helper (readTailscaleStatusJSON) so detection paths stay consistent.

Scope and impact:

  • File touched: Sources/RouteManager.swift only.
  • No UI changes.
  • No config format changes.
  • No broad VPN detection refactor.
  • Existing non-Tailscale VPN flow is intentionally left intact.

Why this is needed:

Without this, users can end up in a confusing state where Tailscale is connected, but the app reports no usable VPN route transition after account switch or keeps stale bypass behavior until a manual refresh.

@karle0wne karle0wne requested a review from GeiserX as a code owner March 5, 2026 07:09
Copy link
Owner

@GeiserX GeiserX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough review completed across 8 dimensions (security, concurrency, logic, performance, edge cases, Tailscale CLI compatibility, style, behavioral regression).

No blocking issues found. Key observations:

  • Security: Net improvement — --peers=false reduces unnecessary data exposure. No injection vectors, fingerprint stays in memory only.
  • Concurrency: @MainActor serialization + isLoading/isApplyingRoutes guards are sufficient.
  • Tailscale CLI: Verified against Tailscale source — --self --peers=false preserves ExitNodeStatus, Self, and TailscaleIPs fields.
  • Edge cases: All tested (Tailscale not installed, no exit node, rapid switching, VPN disconnect mid-refresh, non-Tailscale VPNs) — all handled correctly.
  • Performance: 3 CLI spawns per cycle vs 2 (no per-cycle cache), but negligible at 30s intervals with the lighter --peers=false payload.

Minor non-blocking observations for future consideration:

  • String(describing:) on NSNumber for UserID — works today but could use explicit NSNumber.stringValue for stability
  • Per-cycle caching of readTailscaleStatusJSON() result would eliminate redundant subprocess spawns

LGTM ✅

@GeiserX GeiserX merged commit f8768dc into GeiserX:main Mar 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants