feat: resolve Tailscale hostnames at sync time and auto-rebind stale sink IPs - #119
Merged
Merged
Conversation
…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 SummaryThe 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.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| 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
Reviews (3): Last reviewed commit: "fix: abort push on ErrAmbiguousPeer inst..." | Re-trigger Greptile
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>
|
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 |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.urlhad a frozen IP that no longer exists. This PR enablessink.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 whenlisten.addris 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
ResolvePeerIPto tsclient that resolves Tailscale hostnames viatailscale status --json, preferring Online peers over offline duplicatesResolveSinkURLhelper that rewrites hostname URLs to resolved IPssink.urlhostnames at sync time before POSTSink-side IP rebind
maybeRebindListenAddrthat detects whenlisten.addrIP is stale (not on any local interface) and auto-rebinds to current tailnet IPTimeout alignment
ReadTimeoutandWriteTimeoutfrom 60s to 5 minutesSyncClient.ClientTimeout(5m)Documentation
Verified
go build ./...go vet ./...go test ./...Notes
New behavior:
sink.urlcan now use MagicDNS hostnames (e.g.,http://grok-bot:9999/syncorhttp://grok-bot.tail-xxxx.ts.net:9999/sync)Backward compatible:
sink.urlandlisten.addrcontinue to work unchangedTest coverage: