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
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>
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>
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
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.
Summary
SetExplicitServices([]tailcfg.Service)toLocalBackend— a browser-safe alternative to the OS portlist scanner that bypasses theShouldUploadServicesgate when services are declared explicitly.setServices(services)method on the WASM IPN object, accepting{proto, port, description?}entries.servicesfield on every node in the netmap JSON (self and peers), populated fromHostinfo.Serviceswith internal peerapi entries stripped (those are already inpeerAPIURL).SetExplicitServicescallsAuto.RestartMap()(new exported method oncontrolclient.Auto) after updating hostinfo, so the control server sends back a fresh streaming netmap andnotifyNetMapactually fires with the new services — a plain hostinfo sync only triggers a "lite" map update whose response body is discarded.Design notes
The
ShouldUploadServiceshook 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 anexplicitServicesfield toLocalBackend.hostInfoWithServicesLockedonly clearshi.Serviceswhen that slice is empty, leaving all non-WASM callers unaffected.userServicesFromViewreturns a non-nil empty slice (notnil) so the JSON-encodedservicesfield is always[]rather thannullwhen a node has none — the JS/TS side declaresservicesas a non-optional array.Test plan
GOOS=js GOARCH=wasm go build ./cmd/tsconnect/wasm/— passesgo build tailscale.com/ipn/ipnlocal— passespackages/tsconnect/src/ipn.test.ts), which spins up real nodes against a headscale control server and verifiessetServices(),self.servicesinnotifyNetMap, andpeer.servicesvisibility on a second node — run 5x in a row with zero flakesAdd 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>dd9c9f6844to7bfc64c379