Skip to content

[Bug]: Proton VPN address is advertised as Tailscale IP and breaks phone pairing #15750

Description

@arthurkatcher

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

  1. Run T3 Code Desktop on Linux with Proton VPN's kill-switch interface and Tailscale present. On the affected host, pvpnksintrf1 has 100.85.0.1 and tailscale0 has 100.74.126.34.
  2. Open Settings → Connections and enable Network access.
  3. Select the advertised Tailscale IP option using 100.85.0.1, create a pairing link, and open it on a phone connected to the same tailnet from another network.
  4. Compare the advertised address with tailscale ip -4 on the host.

The failure also appears with a freshly computed endpoint list: the current provider emits the Proton address before the actual Tailscale address.

Expected behavior

The Tailscale pairing option should advertise the host's actual Tailscale address. An unrelated VPN interface should not be labeled as reachable through the tailnet.

Actual behavior

T3 advertises http://100.85.0.1:3773/ as Tailscale IP and uses it in the pairing link. The phone's request to /.well-known/t3-environment times out after 10 seconds. Replacing only the host in the link with 100.74.126.34 makes pairing succeed.

Impact

Major degradation or frequent failure: the generated remote-pairing link is unusable unless the address is manually corrected.

Version or commit

Observed on desktop 0.0.45. Source reproduction confirmed on main at 4ee6bfd50ef4a089440d5c3662db2298da9cc50e.

Environment

Ubuntu 24.04.4 LTS; Linux x86_64 AppImage; Proton VPN; Tailscale 1.102.4; iPhone connected to the same tailnet.

Relevant evidence

Local interfaces:
  pvpnksintrf1  100.85.0.1
  tailscale0   100.74.126.34

tailscale ip -4:
  100.74.126.34

Current endpoint provider:
  http://100.85.0.1:3773/
  http://100.74.126.34:3773/

The provider scans every interface and accepts anything matching isTailscaleIpv4Address, which only tests the shared 100.64.0.0/10 range. That range alone does not establish ownership by Tailscale.

The saved Tailscale default is keyed by endpoint type, so the first incorrectly admitted candidate can remain selected. Filtering out the unrelated candidate lets the existing selection logic resolve to the real address.

Related but different: #9882 separates LAN and Tailscale entries; #11920 proposed broader discovery changes for macOS permission prompts and was closed without merging during the V2 transition.

Workaround

Use tailscale ip -4 and replace the address in the generated pairing URL while preserving the port, path, and token. This worked on the affected phone. Reloading alone does not change the range-based classification.

Activity

  1. juliusmarminge commented on Oct 4, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for the clear report and the working workaround, @arthurkatcher!

    What I found

    Confirmed on main at 4ee6bfd. This is separate from #15751, where the Connections list stays cached after the network changes.

    Why the wrong address gets picked:

    • isTailscaleIpv4Address (packages/tailscale/src/tailscale.ts) accepts any address in 100.64.0.0/10. That's shared CGNAT space, not Tailscale's alone.
    • Proton's Linux kill switch adds a dummy interface (pvpnksintrf0/pvpnksintrf1) with 100.85.0.1/24, which falls in that range.
    • resolveTailscaleIpAdvertisedEndpoints (apps/desktop/src/backend/tailscaleEndpointProvider.ts) scans Object.values(networkInterfaces), so interface names are dropped and listing order decides which address comes first.
    • Every tailscale-ip: endpoint shares the saved preference key tailscale:ip:http (endpointDefaultPreferenceKey in ConnectionsSettings.tsx), and pairing takes the first match. So the Proton address stays selected, and the phone's /.well-known/t3-environment request times out.

    The same range check also affects:

    • LAN endpoint selection (isUsableLanIpv4Address).
    • The exposure state (resolveRuntimeState in apps/desktop/src/backend/DesktopServerExposure.ts).

    Related, not duplicates:

    Likely fix area

    Some options:

    • Use addresses Tailscale actually owns. readTailscaleStatus already returns Self.TailscaleIPs as tailnetIpv4Addresses, but the desktop path only keeps the MagicDNS name. Its cached result could be used to filter the interface candidates. Spawning tailscale status on every settings read should be avoided, since that brings back the macOS "Other apps" prompt.
    • Keep interface names during the scan and prefer tailscale*/utun*-style interfaces when several CGNAT addresses exist.
    • Revisit the shared tailscale:ip:http preference key, so a stale first candidate doesn't stay selected.

    Until then, your workaround works: keep the port, path, and token, and replace only the host with the address from tailscale ip -4.

    A maintainer will decide on the fix direction.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 4, 2026
  3. xixixao commented on Oct 9, 2026

    @xixixao

    Ran into this with Cloudflare WARP as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions