Skip to content

feat: own the tailscale serve mapping (supersedes #26)#36

Merged
AltanS merged 3 commits into
mainfrom
feat/tailscale-serve-ownership
Jul 26, 2026
Merged

feat: own the tailscale serve mapping (supersedes #26)#36
AltanS merged 3 commits into
mainfrom
feat/tailscale-serve-ownership

Conversation

@AltanS

@AltanS AltanS commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Extracts the Tailscale ownership tracking from #26, fixes the upgrade path it broke, and documents the bring-your-own-tunnel case that #26 was really reaching for. The NetBird front door itself is not taken — reasoning in #26 (comment).

What changes

Ownership tracking (a004449, authored by @iamtimmy). cmd_unserve used to run a blind tailscale serve --https=443 off keyed only off COLLIE_SERVE_MODE, so unserve/uninstall could silently unpublish a mapping Collie never created. Publishing had the mirror problem: tailscale serve --bg … / replaces an existing root handler. Both directions now prove ownership against a recorded <config-dir>/tailscale-managed-handler.

Adopt-on-match (65889da). The check as written refused to publish over any existing root mount, which is the state of every install that predates ownership tracking — Collie's own mount, no record of it. Verified against this host's real serve config and in the sandbox in both modes:

error: Tailscale serve already has an unowned root mount on :8787; refusing to overwrite it

Ownership is now decided by where the mount points. A root already proxying to our own http://127.0.0.1:$PORT is adopted; anything else is still refused; a foreground serve session is never adopted. Also stops a failed front door from aborting start — the bridge is up on loopback and the banner has to print.

Variant E (6550041). COLLIE_SKIP_SERVE=1 was always the bring-your-own-ingress switch, just filed under "reverse proxy" where a NetBird or ZeroTier user wouldn't find it. Documents the general case, and records in CLAUDE.md why Collie manages exactly one front door.

Tests

scripts/collie-ctl.test.sh is the first coverage the control script has had: fake tailscale/systemctl on a scratch PATH, throwaway $HOME, so it runs anywhere and touches nothing real. Wired into the pre-push hook (bun run test alone wouldn't have run it — the hook calls bun test ./bridge directly).

Every assertion negative-controlled by sabotaging a value and confirming the failure, then confirming restore is green.

Co-authored with @iamtimmy, whose commit this builds on.

iamtimmy and others added 3 commits July 26, 2026 23:56
`cmd_unserve` ran a blind `tailscale serve --https=443 off` (or `--http=$PORT off`)
keyed only off COLLIE_SERVE_MODE. If anything else on the host owned that root
mount, `unserve`/`uninstall` silently unpublished it. Publishing had the mirror
problem: `tailscale serve --bg … /` REPLACES an existing root handler, so `start`
could take over a mapping Collie never created.

Record the one mapping we publish in `<config-dir>/tailscale-managed-handler`
(`<mode>:<port>|<HostPort>|<proxy>`), and make both directions prove ownership:

- publishing refuses when the target root is occupied by a handler we don't own,
  or when the listener already speaks the opposite protocol;
- teardown removes the recorded mapping only when the live root still matches the
  record, clears the record when the root is already gone, and refuses (keeping
  the record) when it was replaced out from under us.

Also adds scripts/collie-ctl.test.sh, the first test coverage the control script
has had. It fakes `tailscale` and `systemctl` on a scratch PATH with a throwaway
$HOME, so the lifecycle runs anywhere and touches nothing real.

Extracted from #26, which wrapped this in a NetBird front door that isn't being
taken. The NetBird track, COLLIE_FRONT_DOOR and their tests are omitted;
COLLIE_SKIP_SERVE stays the proxy switch.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The ownership check refused to publish over ANY existing root mount on the target
port. Every install that predates ownership tracking has exactly one: Collie's
own mount, and no record of it, because no version that writes the record has run
there. So `serve` failed on precisely the deployments that already worked:

  tailscale serve: no Collie-managed mapping recorded
  error: Tailscale serve already has an unowned root mount on :8787; refusing to overwrite it

Reproduced against this host's real `tailscale serve status --json` and in the
sandbox in both HTTP and HTTPS modes, with a fresh-host control that succeeds.

Decide ownership by where the mount points, not by whether we have a record of
it. A root already proxying to our own `http://127.0.0.1:$PORT` is adopted:
republishing over it is a no-op, and we record it on the way through. A root
pointing anywhere else is still refused, and a foreground serve session is never
adopted — it belongs to a live process that is not us.

Also stop a failed front door from aborting `start`. cmd_serve can now return
nonzero, and under `set -e` a bare call exited before print_status_banner, so a
serve problem left the operator with no banner and no URL — while the bridge was
up on loopback the whole time. That inverted the README's troubleshooting flow,
which says the bridge is fine and only the ingress failed.

Wires the lifecycle suite into the pre-push hook. `bun run test` alone doesn't
cover it: the hook calls `bun test ./bridge` directly, so the suite would only
ever run when someone remembered to.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The README read as though Tailscale were a requirement. It isn't: the bridge's
entire Tailscale coupling is one header read, and `COLLIE_SKIP_SERVE=1` has
always been the "you own the ingress" switch — it was just filed under
"reverse proxy", so a NetBird or ZeroTier user had no reason to find it.

Variant E states the general case: point any tunnel at 127.0.0.1:$COLLIE_PORT,
carry the Variant B proxy requirements over, know that COLLIE_TRUSTED_USER is
inert outside `tailscale serve`, and pin a stable hostname before installing the
PWA. Plus the reason the funnel rule isn't about Tailscale — it's about
reachability, and any public URL inherits it.

Records in CLAUDE.md why Collie manages exactly one front door, so the next
proposal doesn't have to re-derive it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@AltanS
AltanS merged commit 837e9f0 into main Jul 26, 2026
1 check passed
@AltanS
AltanS deleted the feat/tailscale-serve-ownership branch July 26, 2026 22:01
@AltanS AltanS mentioned this pull request Jul 26, 2026
6 tasks
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