Skip to content

Reach a machine's contexts from your other devices on Tailscale - #41

Merged
chris-allen merged 3 commits into
mainfrom
tailnet
Nov 1, 2026
Merged

chris-allen merged 3 commits into
mainfrom
tailnet

Conversation

@chris-allen

Copy link
Copy Markdown
Contributor

Grove only served loopback, so a worktree running on one machine couldn't be opened from a laptop or phone. This adds sharing over Tailscale, and the trust to open those hostnames without a warning.

Sharing

grove share ties a machine to the tailnet it's on. Every hostname gains the machine's name, app.grov.site becoming app.<host>.grov.site, so the same worktree on two machines is two origins, and the origin is the same from every device, the machine itself included.

  • While on that tailnet, the daemon answers DNS for *.<host>.grov.site on its tailnet address (UDP and TCP) and forwards tailnet port 443 to itself through Tailscale Serve as raw TCP, so it still picks the route and certificate from the hostname.
  • On any other tailnet, or with Tailscale down, it closes the resolver and releases its mapping, so joining someone else's tailnet doesn't offer them your dev servers.
  • It never takes 443 from another Serve handler, background or foreground. t3code's is the usual one, and grove names it.
  • The daemon re-reads the share record on every check, so grove share --off takes effect without a restart.
  • Hostnames stay host-qualified off the tailnet, where they resolve to the machine itself, so a running dev server's URL never moves.
  • One step stays manual: a split DNS entry in the Tailscale admin console, which grove share prints. Answering on port 53 lowers the unprivileged port floor, offered the way grove install offers its own step.
  • Grove talks to tailscaled's local API with the standard library, with no new dependency. Linux only for now.

Trust

Each machine has its own root, so opening another machine's hostnames means trusting its root.

  • grove trust <host> fetches a sharing machine's root and installs it. It dials the peer's tailnet IP from Tailscale's peer list, never DNS, which WireGuard ties to that node's key, and refuses a root the served certificate doesn't chain to.
  • grove trust --send <device> delivers this machine's root to a phone over Taildrop and prints the install steps for its OS.
  • NODE_EXTRA_CA_CERTS carries trusted peer roots, and uninstall removes the ones trust added.

Doctor

A Sharing check names whichever part is missing: Tailscale disconnected, another tailnet, nothing answering DNS, the split DNS entry absent, or 443 held by something else.

Testing

  • go test -race -count=1 ./..., go vet ./..., gofmt -l ., a darwin cross-build, and the docs build and link check.
  • New tests cover the resolver through Go's own DNS client over UDP and TCP, Serve claim and release against a fake tailscaled (including refusing t3code's handler, foreground sessions, and a write after someone else's change), the daemon standing down on another tailnet and when the record goes, and the root fetch against a real proxy, including an impostor whose certificate doesn't chain to the root it offers.
  • cmd/grove's TestMain now gives the suite its own state directory, since a maintainer's own share record renamed every hostname the tests expect.
  • End to end between two Linux machines and an Android phone on one tailnet: hostnames resolving through split DNS from each, validated TLS in both directions, routing to a running dev server, and roots trusted with grove trust and delivered with --send.

Found along the way, not fixed here

On Linux, Tailscale in direct mode writes /etc/resolv.conf while nss resolves through systemd-resolved, which then races MagicDNS against the LAN's DNS and occasionally answers a grove name with 127.0.0.4. Linking /etc/resolv.conf to resolved's stub fixes it. Doctor can't yet tell this apart from a missing split DNS entry, and that, plus sharing stopping after a reboot until grove starts, are follow-ups.

- grove share gives every hostname the machine's name and serves them
  on the tailnet, standing down on any other
- Never takes tailnet port 443 from another Serve handler such as t3code
- doctor names whichever part of sharing is missing
- grove trust <host> fetches a sharing machine's root over its tailnet
  address and installs it
- grove trust --send <device> delivers the root to a phone over Taildrop
- The split DNS resolver answers over TCP too, which resolvers fall back to
- Port 443 held by a foreground tailscale serve is left alone
- NODE_EXTRA_CA_CERTS carries trusted peer roots alongside this one
@chris-allen
chris-allen merged commit 6059857 into main Nov 1, 2026
15 checks passed
@chris-allen
chris-allen deleted the tailnet branch November 1, 2026 18:39
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.

1 participant