fix(tsconnect): filter offline and unreachable taildrive peers #165
Labels
Clear labels
Agentic
Component/CI
Component/Funnel
Component/React
Component/State
Component/Taildrive
Component/Taildrop
Component/Tailscale
Component/Tailshare
Component/Transport
Component/tsconnect
Component/VFS
Component/WebRTC
Component/Worker
Human
Protocol/FTP
Protocol/HTTP
Protocol/SFTP
Protocol/SMB
Protocol/SSH
Protocol/WebDAV
Protocol/WebSocket
Security
Agent
claude-fable-5
Work done by Claude Fable 5
Agent
claude-opus-4-8
Work done by Claude Opus 4.8
Agent
claude-opus-5
Work done by Claude Opus 5
Agent
claude-sonnet-4-6
Work done by Claude Sonnet 4.6
Agent
claude-sonnet-5
Work done by Claude Sonnet 5
Agent
gpt-5.5
Work done by GPT 5.5
Agent
gpt-5.6-luna
Work done by GPT 5.6 Luna
Agent
gpt-5.6-sol
Work done by GPT 5.6 Sol
Agent
gpt-5.6-terra
Work done by GPT 5.6 Terra
Opened by an agent
Work on the CI tooling
Work on the Tailscale Funnel or certificate system
Work on a React binding
Work on a state store (eg Redux)
Work on the taildrive system
Work on the taildrop system
Work on the Tailscale fork
Work on the Tailshare app
Work on the transport system
Work on the tsconnect packages
Work on the VFS system
Work on the WebRTC system
Work on the worker system
Opened by a human
Kind
Bug
Bug work
Kind
Enhancement
Enhancement work
Kind
Feature
Feature work
Kind
Maintenance
Maintenance work
Priority
P0
1
Critical work that must be done right now
Priority
P1
2
Urgent work
Priority
P2
3
Medium priority work
Priority
P3
4
Low priority work
Priority
P4
5
Lowest priority work, wishlist-tier
Work on the FTP protocol
Work on the HTTP protocol
Work on the SFTP protocol
Work on the SMB protocol
Work on the SSH protocol
Work on the WebDAV protocol
Work on the WebSocket protocol
Security work
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: webnet/webnet#165
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #143.
What
listDrivePeers()returned every peer holdingPeerCapabilityTaildriveSharer. That cap is an ACL grant usually given to a whole group or tag, so the result was most of the tailnet — including offline peers and peers with no reachable peerAPI — despite the doc comment claiming it mirroredLocalBackend.driveRemotesFromPeers.The wasm bridge now applies the same conjunction as upstream (
ipn/ipnlocal/drive.go): online, non-empty peerAPI base, sharer cap. The peerAPI URL was already computed and then discarded, so the check is free.tailscale/cmd/tsconnect/wasm/drive.go(submodule, branchfix/drive-peers-reachabilityonwebnet/tailscale, based onwebnet)IPN.listDrivePeersandIPNDrivePeerdoc comments corrected: the result is peers allowed to share, not peers that expose a sharepeerAPIURLSubmodule
The pointer moves from
15a70243eto the new commit on top of the fork'swebnetbranch, so it also picks upcmd/tsconnect: remove obsolete wasm bridge APIs(drops thessh,fetchandsetExitNodeEnabledbridge methods). Neither@webnet/tsconnectnor@webnet/tsconnect-workerreferences those.The submodule commit sits on a feature branch, opened as webnet/tailscale#18. Merge that (fast-forward preferred, so
webnetlands exactly on the pinned commit) alongside this.Verification
ab987f5npm run build-go --workspace=packages/tsconnect— wasm builds clean;gofmtcleanGOOS=js GOARCH=wasm go build ./cmd/tsconnect/wasmfails onfeature/aceat both the old and new submodule commits — pre-existing and unrelated; the tagged build path used bybuild.shis finenpm run test --workspace=packages/tsconnectagainst the headscale test tailnet: 78 pass, 0 fail, 1 skipped (setExitNode, needsTSCONNECT_TEST_EXIT_NODE_ID). The suite skips entirely in CI, which has noTSCONNECT_TEST_CONTROL_URL/TSCONNECT_TEST_AUTH_KEYlistDrivePeersreturns[]on that tailnet, because no node there carriesPeerCapabilityTaildriveSharer(headscale grants no such cap), so the new per-peer assertions never execute. They are guards against a regression, not evidence the filter works — that would need an ACL granting the sharer cap to an offline or peerAPI-less node. The filter itself was checked by reading it against upstream'sAvailableclosure, which the review independently repeatedab987f5Left open
The issue's "Beyond that" question — whether
listDrivePeersshould stay cheap-and-permissive (and be renamed) or gain an opt-in flag that does N peerAPI round-trips to filter on a non-empty share list — is not addressed here and needs a decision.Autonomous review (Claude Sonnet 5).
Correctness of the Go filter (
tailscale/cmd/tsconnect/wasm/drive.go@ 7e9868f50)Verified against upstream
driveRemoteSource.Remotes'sAvailableclosure inipn/ipnlocal/drive.go(~L376-386):p.Online().Get():NodeView.Online()returnsviews.ValuePointer[bool]wrappingNode.Online *bool(tailcfg/tailcfg_view.go:267).ValuePointer.Get()returns the zero value (false) when the pointer is nil (types/views/views.go:872-878) — no panic, and a nil/unknown online status is correctly treated as "not online", identical to upstream's ownpeer.Online().Get()call on the same type.buildPeerAPIURL(p, selfHave4, selfHave6)vs upstreamcn.PeerAPIBase(peer): same Hostinfo-services-based reachability logic and same self-address-family gating; the one difference (upstream short-circuits on!peer.Hostinfo().Valid(), the wasm helper doesn't) is a no-op in practice since an invalid Hostinfo yields an empty Services() iterator either way. Behaviorally equivalent.i.lb.PeerCaps(a.Addr()).HasCapability(cap)looped overp.Addresses().All()vs upstreamcn.PeerHasCap(peer, cap): upstream's ownPeerHasCap(node_backend.go:530-540) does exactly the same per-address iteration internally, keyed by address rather than node identity in both cases. No divergence.The fix is a correct, faithful mirror of
driveRemotesFromPeers's filtering, and closes the gap described in #143 (offline/unreachable peers holding the sharer ACL cap no longer leak through).TypeScript doc-comment and test changes (
packages/tsconnect/src/ipn.ts,types.ts,ipn.test.ts)Accurate: the corrected doc comments now say "peers allowed to share" rather than "peers that expose a share", matching the cap's real ACL-grant semantics (also correctly called out in the issue's "Beyond that" section, left unaddressed here as scoped). The new test assertions (
p.peerAPIURL !== "",p.online !== false) are consistent with the Go side always populating a non-emptyonline: truefor peers that pass the new filter; usingnotEqual(..., false)rather than asserting=== trueis slightly loose but harmless given the field is optional (online?: boolean) in the type.npm run typecheck --workspace=packages/tsconnectpasses clean locally.Submodule bump side effects
Confirmed via grep across
packages/tsconnect/srcandpackages/tsconnect-worker/src: no references to the removedssh,fetch, orsetExitNodeEnabledwasm bridge methods (thefetch(...)hits inindex.tsare all calls to the browser/globalfetch()API for loading the wasm binary, unrelated to the removed bridge method). The unrelatedcmd/tsconnect: remove obsolete wasm bridge APIscommit riding along on the submodule bump is inert for this repo.Nothing else broke. No other findings — this is a small, well-scoped, correct fix.