Tailscale parity with T3 Connect
On your personal tailnet, Tailscale already knows which devices are yours. T3 Code trusts that. Your own devices find every box and connect to it with no pairing code, no account, and no cloud in the path.
This is research only. No code changed. It is based on commit a4e1986585 (2026-10-28), 7 research agents, and a review council. The decisions are at the end.
Do this first
- Fix 3 gates with PRs that already exist: route flapping, the Serve switch, and Serve recovery.
- Ship honest status.
- Ship Tailscale sign-in, discovery, and
npx t3 up.
Releases 2 and 3 do not depend on each other. Both can start after Release 1.
Your setup, step by step
The setup is 1 laptop with the app, 5 or more headless Linux servers, and a phone. All of them are on your personal tailnet.
| Step | What you do | What happens |
|---|---|---|
| 1. Each server | npx t3 up | Only the tailnet can reach the box. T3 runs as a background service, and Tailscale sign-in is on. The command prints a QR code. You type no code. |
| 2. Laptop | Nothing, or turn on "Tailscale sign-in" in Settings if you also want to reach the laptop from the phone | The desktop app finds every machine on your tailnet that runs T3 and connects to it. The switch also opens the laptop to the tailnet on port 3773. |
| 3. Phone | Scan any QR code once, or type one machine name. Tailscale must be on. | The phone connects to that machine and gets the list of the others from it, then connects to them too. |
| 4. Laptop goes offline | Nothing | The phone stays connected. Each server checks with Tailscale itself. No laptop and no cloud are in the path. |
| 5. Change things from the phone | Nothing extra | The phone does not need the laptop. A new box appears through any machine the phone is connected to. |
How sign-in works
- Your device connects to the box at its direct tailnet address,
http://<100.x>:<port>. It sends a DPoP proof to/api/auth/tailnet. - The box asks Tailscale who owns the connecting address (
tailscale whois). WireGuard makes that address hard to fake. If the device is an untagged device that you own, the box issues a 2-minute grant bound to the device's key. - The device exchanges the grant at
/oauth/tokenfor a 1-hour session. It renews the same way, always at the direct tailnet address, and every check runs again.
The grant and the session use the same code as T3 Connect: createPairingLink and the DPoP exchange. No new session store, scopes, or token format.
This works on the native desktop and mobile apps. Browser clients, hosted or self-hosted, keep pairing codes. The guard refuses web origins, and a page on plain http://100.x cannot make a DPoP key.
Your 3 asks
| Ask | Answer | Release |
|---|---|---|
| Discovery and no pairing codes | Tailscale sign-in for your own devices in the desktop and mobile apps. Desktop reads the tailnet peer list. Phones get the list from any connected machine. | 3 |
| Clear machine status | The host reports its Tailscale state. Each failed route gets its own message. Status dots appear in the composer picker and the sidebar. | 2 |
| Off-tailnet states | Desktop reads its own Tailscale state. When every tailnet route fails but other routes work, mobile and web show a hint. | 2 |
Terms
- DPoP: a token bound to a key that only the client holds. A copied token does not work without the key.
- whois: asks the local Tailscale daemon which device and which user own a tailnet address.
- Tagged device: a device owned by a tag such as
tag:serverinstead of a person. Tailscale gives it no user identity. - Serve: gives a machine an HTTPS URL on the tailnet. Funnel opens Serve to the public internet.
Where we are
Works today
- Tailscale HTTPS through Serve: the desktop switch,
t3 serve --tailscale-serve, andt3 pair --tailscale. - Learned routes. A T3 Connect user already gets Tailscale routes with no pairing code.
- The background service (
t3 service install) listens on private and tailnet addresses and turns on systemd lingering. - The SSH tab in Add environment already lists hosts found on the desktop. The new tailnet list copies it.
Broken or missing (checked in code)
- Without T3 Connect, every device needs a pairing code. Direct sessions last 30 days, then the device must pair again (discussion #18003).
- Nothing reads the peer list or calls whois. The status parser ignores login state, health, key expiry, HTTPS certificates, and peers (tailscale.ts:132).
- Serve setup runs once. It does not retry and does not check who owns the mapping (#16238, #10363). The probe accepts any 2xx response (tailscale.ts:453).
- The Serve switch shows probe health, not your choice (#14746, #26136).
- A failed tailnet route says only "did not answer". The relay's "host is online" status is lost when a machine has more than one route.
- The picker and the sidebar show no machine status. Healthy sessions flap between routes (#24516).
Release 1: reliable connections
Done when:
- A Serve failure is visible and is retried.
- The switch shows the saved choice.
- No healthy session closes because of a route check.
- The startup QR shows the HTTPS URL.
- Route rule: rebase #25529, and close #24849, which has the same diff (decision 5).
- The switch follows the saved choice: #15027, then #26170. Fold in the copy from #24638.
- Serve retry and ownership: combine #23605 and #24943, with the rule from decision 4.
- The Serve probe checks the environment ID. About 10 lines.
- The startup QR uses the Serve URL: #25351 (clean).
Release 2: useful status
- Host Tailscale facts (300 to 450 lines). The host reads login state, key expiry, health, HTTPS, and MagicDNS. Clients show facts such as "Tailscale on studio is signed out" over any connected route. Add
t3 tailscale status. - A diagnosis for each route (about 250 lines). Tailscale failures get their own message and a help link.
- "This device is off the tailnet" (about 200 lines). Desktop reads its own login state. Mobile and web show a hint when every tailnet route fails but other routes work.
- Status dots in the composer picker and the sidebar (60 to 100 lines). The dots are static. The banner says "offline" only for a machine that is really offline.
Release 3: Tailscale sign-in and discovery
One command on each box
studio-vps $ npx t3 up
Tailscale signed in (studio-vps.tail1234.ts.net)
Owner theo@ (the owner of this box)
Network Tailscale only, LAN closed
Sign-in on for your own Tailscale devices
Service installed, starts on boot
Scan to add this box on your phone (http://100.88.1.2:3773):
[QR code]
Your other devices find it on their own.
Tip: this box is not tagged. If agents run here, tag it so they
cannot sign in to your other machines. Add to your ACL:
"tagOwners": { "tag:server": ["autogroup:admin"] }
then run: sudo tailscale set --advertise-tags=tag:server
- Order:
t3 upchecks Tailscale, writes its settings, then installs and starts the service. - User: run it as the user who should own T3. As root, it stops and prints
sudo -iu <user> npx t3 up, because the service usessystemctl --user. - Userspace networking (common in LXC and some containers) hands tailnet connections to
127.0.0.1, so whois cannot work.t3 updetects this and says so. - Owner:
- On an untagged box, the owner is the box's own Tailscale user.
- On a tagged box,
t3 upproposes the user of the device you SSH from, but only whenSSH_CONNECTIONholds a tailnet address that whois can resolve. Otherwise it asks for the login and offers no default.--owner <login>sets it in scripts. - It stores the user ID, not the email.
- The QR code holds the direct tailnet address, never the Serve URL, because sign-in through Serve arrives from loopback and is refused. The QR holds no secret. This is not the pairing QR from Release 1, which shows the HTTPS URL for pairing codes.
- Options:
--httpsalso sets up Serve, as an extra route for data. Runningt3 upis the opt-in. A switch in Settings turns sign-in on or off later. - "Tailscale only" mode accepts only loopback and tailnet peers, and it reports no LAN routes. Today's private-network mode also accepts LAN peers.
Discovery
- Desktop: the desktop app reads
tailscale status. It keeps online peers that you own or that are tagged, and skips nodes shared in from other tailnets. It probes each peer athttp://<100.x>:3773, a few at a time with short timeouts. It adds the machines it finds and shows a notice. - Phone:
- When asked, a connected box probes its own peers the same way and caches the result for a few minutes.
- It answers only sessions with
access:read, so narrow tokens such as MCP get no map of your tailnet. - It sends only the name, MagicDNS name, tailnet address, and environment ID.
- The phone checks each machine itself and adds it.
- A discovered address never joins the routes of a machine you already have. If a known environment ID shows up at a new address, the app warns you, because the public descriptor does not prove identity.
- Removing a machine sticks. The app remembers it and does not add it again (decision 8).
- The laptop: when Tailscale sign-in is on, the desktop listens on the tailnet at port 3773. If that port is taken, Settings says so and shows the port in use, and you add the laptop by name and port.
What we build (rough: 2,300 to 3,300 lines with tests)
| Part | Work | Lines |
|---|---|---|
| Server | The sign-in endpoint with every check in the security model. whois through LocalAPI or the CLI, with single-flight and a rate limit | 300 to 400 |
| Server | Block and Unblock, revoke by subject, and one transaction for the grant, the session, and the block check | 150 to 250 |
| Client runtime | A new tailnet DPoP profile kind that pins the direct address (today only t3-connect exists, catalog.ts:26). Renewal without a cloud session. Paired tailnet devices switch to Tailscale sign-in when their old session ends | 350 to 550 |
| Desktop | The "On your tailnet" list, adding machines automatically, remembering removals, and the address-conflict warning | 250 to 350 |
| Server and mobile | The peer list from connected machines, so phones find new boxes | 300 to 400 |
| CLI | t3 up: the Tailscale and userspace checks, the owner, settings, the service, the QR code, the tagging tip, and --https | 250 to 350 |
| UI | QR scan and add by name on mobile, sign-in states, Block in Sharing & access, and the desktop port warning | 200 to 300 |
| Tests | 500 to 700 |
Security model
Trust comes from Tailscale. Any untagged device you own on your tailnet gets full control of your T3 boxes: terminal, files, and access. On a shared tailnet, the box still accepts only your own devices.
- DPoP: required and verified with
verifyRequestDpopProof. The thumbprint comes from the proof, never from the body. - Address: the remote address must be in
100.64/10(normalize::ffff:). The local address must be this box's tailnet address. Loopback is refused, and that also refuses Serve and Funnel traffic. - Refused nodes:
- tagged nodes
- nodes shared in from another tailnet
- other users
- the box's own node
- nodes that route for others, meaning
AllowedIPsholds more than the node's own addresses (a subnet router or an exit node)
- whois fields:
StableID,User,Addresses, andAllowedIPsare required. Tailscale leaves out empty fields, so a missingTags,PrimaryRoutes, orSharermeans empty. Require a minimum Tailscale version. - whois cache: sign-in and renewal share one in-flight lookup and keep a result for a few seconds at most. So every check really runs again.
- Browser guard: a hard 403 in the handler. The Host must match exactly. The Origin must be absent (native apps) or
t3code://apport3code-dev://app.Origin: nullis rejected. Without this guard, any web page in your browser could sign in as your device. Production CORS allows any origin, so CORS cannot do this job. - Identity: every grant and session for a device has the subject
tailscale:<StableID>. - Routes:
- A tailnet session is used only on that box's tailnet and Serve routes, never on a learned plain-HTTP LAN route.
- Renewal always uses the direct tailnet address, because Serve and LAN routes hide who the device is.
- When the phone leaves Tailscale, it keeps its machines and renews when Tailscale is back. It never switches to pairing on its own.
- Renewal runs every check again. A device that you remove from your tailnet, or tag, loses access within 1 hour on every box. Open sockets close when the session ends.
- Ways out:
- Remove the device from your tailnet. This is the main way, and it covers every box.
- Block it on one box (decision 10), and Unblock it to undo. Block is keyed by
StableIDand ends the device's live sessions. It shares one transaction with the whole exchange, including session creation. Today grant use and session creation are separate steps (EnvironmentAuth.ts:935). A device that rejoins Tailscale gets a newStableID, so you block it again or remove it from the tailnet. - Turn off Tailscale sign-in on the box.
- The tradeoff: an agent on one of your untagged machines can sign in to your other boxes. That also bypasses the fence that linked environments put on agents. To stop it, tag the servers that run agents.
t3 upprints the tip (decision 9). Tagged boxes still host T3, but they can never sign in anywhere.
Parity with T3 Connect
| Capability | T3 Connect | Tailscale today | After this plan |
|---|---|---|---|
| Connect with no code | Yes | No | Yes, on your own devices |
| No 30-day lockout | Yes | No | Yes, renewal is automatic |
| Machine list | Account list | No | Desktop: automatic. Phone: one QR scan, then automatic |
| Works with no internet service up | No, needs the relay | Yes | Yes, no cloud in the path |
| Status with causes and fixes | About 15 causes | Generic | Diagnosis for each route, plus host facts |
| Browser clients (hosted or self-hosted) | Yes | Need a code | Still need a code. Later (Serve identity) |
| Push notifications | Yes | No | Later (optional T3 Connect link) |
| Public webhooks, hosted outside agents | Yes | No | Cannot match privately. We document it |
Later
- Browser clients (app.t3.codes and self-hosted web). Serve hides the device from whois. On a personal tailnet, Serve's user header is always you, so trust it only on a listener that only Serve uses. This is a separate design task.
- Account fleet add-on. This links boxes to your T3 account with no tunnel. It adds a list on a brand-new phone with no QR code, push notifications, and the hosted web app. The design is in version 5 of this page.
- Also later:
- IPv6 tailnet addresses
watch-ipn-businstead of polling- Windows Serve for WSL backends (#4028)
Do not build
| Idea | Reason |
|---|---|
| Tailscale API key or OAuth client | It only lists devices, which tailscale status already does for free, and it authorizes nothing. Tailscale's new OAuth apps can only add devices |
| Approvals for each device | On a personal tailnet, Tailscale already says which devices are yours. Copying approvals between boxes had holes |
| tsnet / libtailscale | A second node for each machine, node keys in T3 state, and no Node binding |
| Tailscale Services, mDNS, port-range scans | Services need admin setup and tags. Tailscale has no mDNS. Scans miss custom ports |
Test first on real devices
- whois through the macOS App Store CLI and the Windows CLI.
- iOS Local Network permission for 100.x addresses. Android cleartext to
http://100.x. - whois from a tagged server, which must be refused.
- Sign-in on a box with userspace networking, which
t3 upmust report.
PR queue (states on 2026-10-28)
"Conflicting" means the PR needs a rebase.
| PR | Topic | State |
|---|---|---|
| #25529 | Keep healthy sessions on their route (gate) | Conflicting |
| #24849 | Same diff as #25529. Close it | Conflicting |
| #15027 | The Serve switch follows the saved choice (gate) | Conflicting |
| #26170 | Enable Serve while Network access is off | CI fails "Agent context budget" |
| #23605 | Serve retry (gate, combine with #24943) | Conflicting |
| #24943 | Serve ownership check | Clean |
| #25351 | The startup QR uses the Serve URL | Clean |
| #25174 | Port conflict on update. Pick it and close #25171 | Clean |
| #25217 | Windows host-only adapters. Pick it and close #21056 | Clean |
| #25991 | Hotspot and VPN addresses are not LAN routes | Clean |
| #23030 | The WSL tailnet address | Clean |
| #24064 | Fewer backoff resets on foreground | Clean |
Decisions so far
| # | Decision |
|---|---|
| 1 | Your own untagged devices sign in with zero clicks on a personal tailnet. t3 up or the Settings switch is the opt-in. This replaces the one-tap approval for each device. |
| 2 | The hosted web app keeps pairing codes until Serve identity is designed. |
| 3 | The account fleet with no tunnel is a later add-on. If it ships, hosts opt in before their tailnet address goes to the relay. |
| 4 | T3 overwrites or removes a Serve mapping only when it is ours or stale. |
| 5 | Never close a healthy session to try a better route. |
| 6 | A "Tailscale only" network mode. t3 up turns it on. |
| 7 | Only personal tailnets matter. On a shared tailnet, the box still accepts only your own devices. |
| 8 | Apps add the T3 machines they find on their own and show a notice. A machine you remove stays removed. |
| 9 | On an untagged box, t3 up prints a short tip about tagging servers that run agents. |
| 10 | Each box has Block and Unblock for each device. |
Questions for you
None right now. Every decision is in the table above.