feat(tsconnect): expose service advertisement to JS #9

Merged
codinget merged 3 commits from feat/tsconnect-service-advertisement into webnet 2026-06-16 12:24:54 +02:00
Owner

Summary

  • Adds SetExplicitServices([]tailcfg.Service) to LocalBackend — a browser-safe alternative to the OS portlist scanner that bypasses the ShouldUploadServices gate when services are declared explicitly.
  • Exposes a setServices(services) method on the WASM IPN object, accepting {proto, port, description?} entries.
  • Includes a services field on every node in the netmap JSON (self and peers), populated from Hostinfo.Services with internal peerapi entries stripped (those are already in peerAPIURL).
  • SetExplicitServices calls Auto.RestartMap() (new exported method on controlclient.Auto) after updating hostinfo, so the control server sends back a fresh streaming netmap and notifyNetMap actually fires with the new services — a plain hostinfo sync only triggers a "lite" map update whose response body is discarded.

Design notes

The ShouldUploadServices hook is normally set by the portlist extension via OS-level port scanning, which can't run in a browser. Rather than registering a hook (which panics if set twice), the fix adds an explicitServices field to LocalBackend. hostInfoWithServicesLocked only clears hi.Services when that slice is empty, leaving all non-WASM callers unaffected.

userServicesFromView returns a non-nil empty slice (not nil) so the JSON-encoded services field is always [] rather than null when a node has none — the JS/TS side declares services as a non-optional array.

Test plan

  • GOOS=js GOARCH=wasm go build ./cmd/tsconnect/wasm/ — passes
  • go build tailscale.com/ipn/ipnlocal — passes
  • Exercised via the integration test suite in webnet/webnet#24 (packages/tsconnect/src/ipn.test.ts), which spins up real nodes against a headscale control server and verifies setServices(), self.services in notifyNetMap, and peer.services visibility on a second node — run 5x in a row with zero flakes
## Summary - Adds `SetExplicitServices([]tailcfg.Service)` to `LocalBackend` — a browser-safe alternative to the OS portlist scanner that bypasses the `ShouldUploadServices` gate when services are declared explicitly. - Exposes a `setServices(services)` method on the WASM IPN object, accepting `{proto, port, description?}` entries. - Includes a `services` field on every node in the netmap JSON (self and peers), populated from `Hostinfo.Services` with internal peerapi entries stripped (those are already in `peerAPIURL`). - `SetExplicitServices` calls `Auto.RestartMap()` (new exported method on `controlclient.Auto`) after updating hostinfo, so the control server sends back a fresh streaming netmap and `notifyNetMap` actually fires with the new services — a plain hostinfo sync only triggers a "lite" map update whose response body is discarded. ## Design notes The `ShouldUploadServices` hook is normally set by the portlist extension via OS-level port scanning, which can't run in a browser. Rather than registering a hook (which panics if set twice), the fix adds an `explicitServices` field to `LocalBackend`. `hostInfoWithServicesLocked` only clears `hi.Services` when that slice is empty, leaving all non-WASM callers unaffected. `userServicesFromView` returns a non-nil empty slice (not `nil`) so the JSON-encoded `services` field is always `[]` rather than `null` when a node has none — the JS/TS side declares `services` as a non-optional array. ## Test plan - `GOOS=js GOARCH=wasm go build ./cmd/tsconnect/wasm/` — passes - `go build tailscale.com/ipn/ipnlocal` — passes - Exercised via the integration test suite in webnet/webnet#24 (`packages/tsconnect/src/ipn.test.ts`), which spins up real nodes against a headscale control server and verifies `setServices()`, `self.services` in `notifyNetMap`, and `peer.services` visibility on a second node — run 5x in a row with zero flakes
codinget added 1 commit 2026-06-16 00:20:47 +02:00
Add SetExplicitServices on LocalBackend so the browser WASM node can
declare TCP/UDP services that get uploaded to the control server and
distributed to all peers in the netmap — without the OS port-scanner
(portlist extension) that cannot run in a browser.

The ShouldUploadServices gate in hostInfoWithServicesLocked is bypassed
when services were set explicitly, leaving all other callers unaffected.

On the JS side, a new setServices(services) method accepts an array of
{proto, port, description?} objects.  The netmap JSON now includes a
services field on every node (self and peers), populated from
Hostinfo.Services with internal peerapi entries stripped (they are
already reflected in peerAPIURL).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
codinget force-pushed feat/tsconnect-service-advertisement from dd9c9f6844 to 7bfc64c379 2026-06-16 00:20:47 +02:00 Compare
codinget added 1 commit 2026-06-16 03:09:46 +02:00
The previous implementation only triggered a lite map update (non-streaming,
OmitPeers=true), whose response is discarded. This meant notifyNetMap was
never called after setServices, so self.services was never visible to the
local node and peers received the update only on their next periodic poll.

Add RestartMap() to controlclient.Auto and call it from SetExplicitServices
after the lite update. This cancels the current streaming poll and starts a
fresh one, causing the control server to send back a full netmap that
includes the updated SelfNode.Hostinfo.Services.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
codinget added 1 commit 2026-06-16 10:08:52 +02:00
userServicesFromView returned a nil slice when a node advertised no
services, which (combined with the omitempty tag) caused the
services field to be dropped or serialize as null instead of [].
TypeScript declares services as a non-optional array, so JS callers
calling .find()/.some() on it would throw intermittently depending on
which netmap snapshot they observed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
codinget marked the pull request as ready for review 2026-06-16 10:36:52 +02:00
codinget merged commit 78c4511a3d into webnet 2026-06-16 12:24:54 +02:00
codinget deleted branch feat/tsconnect-service-advertisement 2026-06-16 12:24:54 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: webnet/tailscale#9