Skip to content

feat(access): exempt direct loopback peers from the mobile access-token gate#3453

Merged
BunsDev merged 1 commit into
mainfrom
feat-local-peer-tokenless
Jul 18, 2026
Merged

feat(access): exempt direct loopback peers from the mobile access-token gate#3453
BunsDev merged 1 commit into
mainfrom
feat-local-peer-tokenless

Conversation

@BunsDev

@BunsDev BunsDev commented Jul 18, 2026

Copy link
Copy Markdown
Member

What

Local requests no longer need a mobile invite token while COVEN_CAVE_ACCESS_TOKEN is armed. Today, provisioning a phone pairing secret (Settings · Phone / scripts/mobile-tailscale.sh) locks out every local browser and even the desktop app's own surfaces behind the access gate — the boot re-arm in server.ts keeps the gate armed on every subsequent dev boot.

How

shouldRequireMobileAccessCredential always returned true because the middleware only sees client-controlled headers ("unless a future caller can pass a non-spoofable remote socket address into this decision"). This PR is that future caller:

  • server.ts (the only code that sees the raw TCP socket) classifies each request: loopback socket peer and no forwarding headers (forwarded, x-forwarded-*, via) and loopback Host → direct loopback. It deletes any client-supplied x-coven-cave-local-peer header, then stamps the header with a per-boot random secret on direct-loopback requests only. The secret overwrites any inherited env value and never leaves the process.
  • src/proxy.ts exempts requests whose stamp exactly matches COVEN_CAVE_LOCAL_PEER_SECRET from mobileAccessGate. If Next ever runs without server.ts in front, the env is unset and the exemption fails closed.
  • PTY upgrade gate honors the same classification straight off the socket, so local terminals work while a token is armed.

Why Tailscale Serve stays gated

Serve delivers remote phones over loopback, but always adds x-forwarded-* headers (which the remote client cannot strip) and forwards a non-loopback authority — either signal alone prevents the stamp. Signed-invite verification, the 307 cookie exchange, and the CSRF origin/referer gates are all unchanged.

Verification

Live-booted with a token armed and probed:

  1. direct loopback page GET → passes gate (404 from Next, not 401) ✅
  2. X-Forwarded-For present → 401 ✅
  3. forwarded + spoofed x-coven-cave-local-peer → 401 ✅
  4. loopback socket but ts.net Host → 401 ✅
  5. direct loopback /api → normal JSON, no token ✅
  6. forwarded + valid token → 307 cookie exchange ✅
  7. forwarded + invalid token → 401 ✅

Full suite: 789 test files pass; tsc --noEmit clean; server.mjs regenerated. New pins in middleware.test.ts (exemption keyed to the per-boot secret, never a bare marker), server-pty-ws.test.ts (strip-before-stamp, forwarding-header list, per-boot mint), proxy-behavior.test.ts (isTrustedLocalPeer fail-closed cases).

Closes cave-vn2r.

🤖 Generated with Claude Code

…en gate

Local requests (the desktop app, local browsers, curl on this machine) no
longer need a mobile invite while COVEN_CAVE_ACCESS_TOKEN is armed. Only
server.ts sees the raw TCP socket, so it now classifies each request:
loopback peer + no forwarding headers + loopback Host = direct loopback.
Those requests get x-coven-cave-local-peer stamped with a per-boot random
secret (any client-supplied copy is deleted first), and proxy.ts exempts
exact matches from mobileAccessGate. The PTY upgrade gate honors the same
classification straight off the socket.

Tailscale-Serve-forwarded phones also arrive over loopback but always carry
x-forwarded-* headers and a ts.net Host, so they are never stamped and stay
token-gated; their signed-invite and cookie-exchange flows are unchanged.
If Next ever runs without server.ts in front, the secret env is unset and
the exemption fails closed. CSRF origin/referer gates are untouched.

Verified live: direct loopback page/API pass tokenless; forwarded, spoofed-
header, and foreign-Host requests still 401; forwarded + valid token still
completes the 307 cookie exchange.

Closes cave-vn2r.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 18, 2026 21:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a “direct loopback” exemption so local desktop-app and local-browser requests are not blocked by the mobile access-token gate when COVEN_CAVE_ACCESS_TOKEN is configured. It does this by having the custom Node server (the only place that can see the non-spoofable TCP peer address) stamp a per-boot secret onto verified direct-loopback requests, and having the proxy/middleware trust only that stamp.

Changes:

  • Add per-boot COVEN_CAVE_LOCAL_PEER_SECRET minting plus direct-loopback classification and request stamping in server.ts/server.mjs.
  • Update src/proxy.ts + src/proxy-helpers.ts to verify the stamp (isTrustedLocalPeer) and bypass the mobile access gate only for stamped direct-loopback peers.
  • Extend tests to pin the direct-loopback classification, “strip-before-stamp” behavior, and fail-closed semantics when the secret is absent.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
server.ts Mints a per-boot local-peer secret, classifies direct loopback from the raw socket + forwarding markers + Host, stamps a trusted header, and exempts direct-loopback PTY upgrades.
server.mjs Regenerated JS output mirroring the new per-boot secret + loopback stamping + PTY upgrade exemption behavior.
src/proxy.ts Verifies the stamped header against the per-boot secret and passes trustedLocalPeer into the mobile access gate decision.
src/proxy-helpers.ts Adds LOCAL_PEER_HEADER constant and isTrustedLocalPeer() helper; updates shouldRequireMobileAccessCredential() to accept trustedLocalPeer.
src/server-pty-ws.test.ts Pins the new PTY upgrade exemption and the precise direct-loopback classification + strip-before-stamp behavior in server.ts.
src/proxy-behavior.test.ts Adds behavior coverage for shouldRequireMobileAccessCredential(..., trustedLocalPeer) and isTrustedLocalPeer() fail-closed cases.
src/middleware.test.ts Pins that the mobile gate exemption is keyed to isTrustedLocalPeer(..., process.env.COVEN_CAVE_LOCAL_PEER_SECRET) (not a spoofable bare marker).
Comments suppressed due to low confidence (1)

server.mjs:414

  • The HTTP handler strips x-coven-cave-local-peer before forwarding to Next, but the websocket upgrade path that forwards to nextUpgradeHandler does not. Deleting the header here too avoids accidentally trusting a spoofable client header on non-PTY websocket upgrades and keeps behavior consistent with the HTTP path.
server.on("upgrade", (req, socket, head) => {
  let pathname;
  let query;
  try {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server.ts
Comment on lines +635 to +640
// The local-peer stamp is trustworthy only because any client-supplied copy
// dies here, before Next (and proxy.ts) ever see the request.
delete req.headers[LOCAL_PEER_HEADER];
if (isDirectLoopbackRequest(req)) {
req.headers[LOCAL_PEER_HEADER] = LOCAL_PEER_SECRET;
}
@BunsDev
BunsDev merged commit 9629f24 into main Jul 18, 2026
15 checks passed
@BunsDev
BunsDev deleted the feat-local-peer-tokenless branch July 18, 2026 21:53
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