Skip to content

feat: resolve Tailscale hostnames at sync time and auto-rebind stale sink IPs - #119

Merged
mvanhorn merged 4 commits into
mainfrom
cursor/tailscale-hostname-resolution-8e99
Aug 24, 2026
Merged

mvanhorn merged 4 commits into
mainfrom
cursor/tailscale-hostname-resolution-8e99

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Summary

After Tailscale re-auth, the sink may get a new 100.x IP while a leftover offline node keeps the old hostname. This broke sync when sink.url had a frozen IP that no longer exists. This PR enables sink.url: http://grok-bot:9999/sync (MagicDNS hostname) to keep working when the sink's 100.x changes after re-auth, and auto-rebinds the sink listener when listen.addr is stale.

Also fixes a blocking issue where large first syncs (16k+ cookies) timed out because the sink's ReadTimeout (60s) was much shorter than the source's ClientTimeout (5m).

Changes

Source-side hostname resolution

  • Add ResolvePeerIP to tsclient that resolves Tailscale hostnames via tailscale status --json, preferring Online peers over offline duplicates
  • Add ResolveSinkURL helper that rewrites hostname URLs to resolved IPs
  • Source now resolves sink.url hostnames at sync time before POST
  • If resolution fails (Tailscale not available, peer offline), falls back to original URL

Sink-side IP rebind

  • Add maybeRebindListenAddr that detects when listen.addr IP is stale (not on any local interface) and auto-rebinds to current tailnet IP
  • Preserves the configured port, only swaps the IP
  • Localhost bindings are not subject to rebind

Timeout alignment

  • Increase SinkSync ReadTimeout and WriteTimeout from 60s to 5 minutes
  • Now matches SyncClient.ClientTimeout (5m)
  • Fixes "i/o timeout" 400 errors on large first syncs over slow Tailscale links

Documentation

  • Update README, quickstart.md, and example configs to recommend hostnames over frozen IPs
  • Add notes about auto-rebind behavior

Verified

  • go build ./...
  • go vet ./...
  • go test ./...

Notes

New behavior:

  • sink.url can now use MagicDNS hostnames (e.g., http://grok-bot:9999/sync or http://grok-bot.tail-xxxx.ts.net:9999/sync)
  • Source resolves hostname → IP via Tailscale status JSON at each sync, preferring Online peers
  • When multiple peers share a hostname (common after re-auth creates a new node while old offline node lingers), the Online one wins
  • Sink auto-rebinds if configured IP is no longer locally bound
  • Sink /sync read/write timeout increased from 60s to 5m

Backward compatible:

  • Existing IP-literal sink.url and listen.addr continue to work unchanged
  • Pairing / per-peer keys / crypto unchanged
  • No forced migration

Test coverage:

  • Online vs offline duplicate hostname resolution
  • Hostname URL rewriting
  • Sink rebind when listen IP is stale
Open in Web Open in Cursor 

cursoragent and others added 2 commits August 23, 2026 23:46
…sink IPs

After Tailscale re-auth, the sink may get a new 100.x IP while a leftover
offline node keeps the old hostname. This breaks sync when sink.url has a
frozen IP that no longer exists.

Changes:

1. Source-side hostname resolution:
   - Add ResolvePeerIP to tsclient that resolves Tailscale hostnames via
     `tailscale status --json`, preferring Online peers over offline duplicates
   - Add ResolveSinkURL helper that rewrites hostname URLs to resolved IPs
   - Source now resolves sink.url hostnames at sync time before POST

2. Sink-side IP rebind:
   - Add maybeRebindListenAddr that detects when listen.addr IP is stale
     (not on any local interface) and auto-rebinds to current tailnet IP
   - Preserves the configured port, only swaps the IP
   - Localhost bindings are not subject to rebind

3. Documentation:
   - Update README, quickstart.md, and example configs to recommend hostnames
   - Add notes about auto-rebind behavior

This enables sink.url: http://grok-bot:9999/sync (hostname) to keep working
when the sink's 100.x changes after re-auth. Existing IP-literal configs
continue to work unchanged.

Tests: cover online vs offline duplicate hostname, URL rewriting, sink rebind

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
The sink's /sync endpoint had a 60-second ReadTimeout while the source
client had a 5-minute ClientTimeout. A first full sync with 16k+ cookies
over a slow Tailscale link could not finish reading within 60s, causing
a 400 'i/o timeout' error.

Align SinkSync ReadTimeout and WriteTimeout with SyncClient's 5-minute
timeout to accommodate large initial syncs.

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown

Greptile Summary

The PR resolves configured sink hostnames through current Tailscale status, fails closed when multiple online peers match, rebinds stale sink listener IPs, and aligns sink transfer deadlines with the source client.

  • Resolves MagicDNS names at each source sync while preserving IP-literal URLs.
  • Detects and rejects ambiguous online peer matches before issuing the sync request.
  • Replaces stale configured sink IPs with the current local tailnet address.
  • Updates examples, documentation, and tests for hostname-based configuration and stale-IP recovery.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
internal/tsclient/tsclient.go Adds online-aware hostname resolution, explicit ambiguity errors, and URL host rewriting while preserving the configured request components.
internal/cli/source.go Resolves the sink endpoint immediately before POST and aborts when peer selection is ambiguous.
internal/cli/sink.go Detects stale tailnet listener addresses and rebinds them to the current local tailnet IP while retaining the configured port.
internal/cli/httpserver/httpserver.go Aligns sink transfer deadlines with the five-minute source sync-client timeout.
internal/tsclient/tsclient_test.go Covers hostname forms, online/offline duplicate selection, ambiguity handling, and resolved URL reconstruction.
internal/cli/source_test.go Verifies ambiguous peer resolution aborts before any sync request is posted.

Sequence Diagram

sequenceDiagram
  participant Source as Source
  participant TS as Tailscale status
  participant Sink as Sink
  Source->>TS: Resolve configured sink hostname
  alt Exactly one online peer
    TS-->>Source: Current tailnet IPv4
    Source->>Sink: POST sealed sync envelope
  else Multiple online peers
    TS-->>Source: Ambiguous peer error
    Source--xSink: Abort without POST
  else Soft resolution failure
    TS-->>Source: Resolution error
    Source->>Sink: Attempt configured URL
  end
  Note over Sink: On startup, replace a stale configured tailnet IP with the current local tailnet IP
Loading

Reviews (3): Last reviewed commit: "fix: abort push on ErrAmbiguousPeer inst..." | Re-trigger Greptile

Comment thread internal/tsclient/tsclient.go Outdated
Comment thread internal/cli/httpserver/httpserver.go
ResolvePeerIP now returns ErrAmbiguousPeer when multiple Online peers
match the same hostname, rather than nondeterministically picking the
first one from map iteration.

The error message includes all matching IPs so the operator can either:
- Pin sink.url to a specific 100.x IP
- Delete the leftover node from Tailscale admin console

The offline+online duplicate hostname case still correctly picks the
single online peer.

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

The 5-minute SinkSync read/write timeout is intentional. We hit a real 16k-cookie first sync that 400'd at 60s over DERP. The timeout now matches SyncClient.ClientTimeout (5m) so both ends have the same budget for large initial syncs over slow/congested Tailscale links.

Comment thread internal/cli/source.go Outdated
When ResolveSinkURL returns ErrAmbiguousPeer (multiple online peers
share the hostname), the push now fails closed instead of falling back
to the original hostname URL. Falling back would hand peer selection
to MagicDNS/system DNS and undo the fail-closed behavior.

Soft failures (Tailscale CLI missing, peer not found, peer offline)
still fall back to the original URL so HTTP can report the connection
error.

Added tests:
- TestSourcePushAmbiguousPeerAbortsWithoutPOST: verifies ErrAmbiguousPeer
  aborts and sends no HTTP request
- TestSourcePushSoftResolveErrorFallsBackToHostname: verifies soft
  failures still POST with the original URL

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
@mvanhorn
mvanhorn merged commit 97dd731 into main Aug 24, 2026
5 of 6 checks passed
@mvanhorn
mvanhorn deleted the cursor/tailscale-hostname-resolution-8e99 branch August 24, 2026 08:34
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