Transport Discovery¶
| Status | Implemented |
|---|---|
| Revision | 2026-10-27, skywire c18b044dc |
| Code | pkg/deployment/tpd, pkg/services/tpd, pkg/transport, pkg/visor |
Abstract¶
Transport discovery (TPD) is the deployment service that records which transports exist between visors. It ingests each visor's own transport list, expires entries that are not refreshed, answers queries over HTTP, and republishes aggregates as CXO feeds. A visor's transports are also available from the visor itself.
Overview¶
Visors report to TPD, and route finders, address resolvers, the reward system and visors read from it. The authoritative source for one visor's transports is that visor, so a reader that needs them can bypass TPD. See Transport data from visors.
Addressing¶
TPD is served over dmsg HTTP at dmsg://<tpd-pk>:80. The key is
transport_discovery_dmsg in deployment.Services. Its CXO feeds are served on
the dmsg ports in Interfaces under the same key.
Data Formats¶
transport.Entry (pkg/transport/entry.go):
| JSON name | Type | Meaning |
|---|---|---|
t_id |
UUID | Transport ID. transport.MakeTransportID derives it from the two edges and the type. |
edges |
array of two public keys | Edges in ascending order. |
type |
string | tptypes.Type: stcpr, sudph, stcp, dmsg, squicr, swsr, swtr, webrtc. |
label |
string | transport.Label: user, automatic, skycoin, setup, sysfeed. |
latency_ms |
number | Round trip time. Omitted when zero. |
bandwidth |
integer | Total bytes. Omitted when zero. |
throughput_bps |
number | Observed peak goodput. Omitted when zero. |
transport.SignedEntry wraps an entry for the legacy registration route:
entry, signatures (two signatures), registered, latency, bandwidth,
version. TPD stores only the ID, edges, type, label and update time.
transport.CompactEntry is the form used on visor feeds and in queries:
| JSON name | Type | Meaning |
|---|---|---|
r |
public key | The edge that is not the reporting visor. |
t |
string | Transport type. |
l |
string | Label. Omitted for automatic. |
A reader rebuilds the full entry with transport.EntryFromCompact(reporter, ce).
store.TransportSummary: total_transports, by_type (map), unique_visors,
and partial and missing_batches when a read failed. store.VisorSummary:
pk, on, version, daily, timeline. store.TransportUptimeSummary:
id, on, type, edge_a, edge_b, daily, timeline. Error bodies are
{"error": "<text>"} (api.Error).
Interfaces¶
HTTP API¶
Every route in pkg/deployment/tpd/api/api.go. Routes in the first two groups
are limited to 30 requests per minute with a burst of 10, per caller. The caller
is the dmsg peer key, or RemoteAddr when it is not a key. A rejected request
returns 429. Request bodies are limited to maxBodyBytes (8 MiB) and a larger
body returns 413. Bodies of 1024 bytes or more (httputil.CompressMinBytes) are
gzip compressed when the client accepts it.
Status codes common to the handlers that call writeError: 400 for an invalid
public key or transport ID or a JSON syntax error, 404 for
store.ErrTransportNotFound, 409 for store.ErrAlreadyRegistered, 408 for a
deadline, 413 for a body over the limit, and 500 for anything else.
Authenticated routes¶
Authentication is 02. A dmsg caller is identified by its dmsg key.
| Method and path | Request | Response |
|---|---|---|
GET /transports/id:{uuid} |
200 Entry. 400, 404. |
|
GET /transports/edge:{pk} |
200 array of Entry, gzip when accepted. 400. 404 when the visor has no transports. |
|
POST /transports/ |
array of SignedEntry |
201 with the request array. 400, 500. |
POST /v3/transports/ |
array of Entry, header SW-Visor-Version |
201 with the accepted entries, an empty array when none. |
DELETE /transports/id:{uuid} |
200 with the text transport deleted. 400 when the ID is invalid or the caller is not an edge. 404. |
|
POST /transports/delete-batch |
array of ID strings | 200 {"deleted": n, "skipped": n}. A body that is not a JSON array returns 500. |
GET /v4/update?version={v} |
200 {"status":"ok"}. Records a visor heartbeat. 500. |
Any other kind: in /transports/{ref} returns 404. In
POST /v3/transports/ an entry is accepted only when the caller is one of its
edges, and other entries are skipped. Both registration routes record a
transport heartbeat for each entry and a visor heartbeat for the caller.
DELETE /transports/id:{uuid} requires the caller to be an edge of the
transport. POST /transports/delete-batch skips IDs that are invalid, unknown,
not owned by the caller, or fail to delete.
Public routes¶
| Method and path | Response |
|---|---|
POST /transports/edges |
Body is an array of public key strings. 200 array of Entry for the union of the keys, deduplicated by ID, empty array when none. 400 for a body that is not a JSON array. |
GET /v3/transports/edge:{pk} |
200 array of Entry, empty array when none. 400. |
GET /all-transports |
410 {"error": ...}. The list is served on the routing feed. |
GET /all-transports/stats?selfTransports=hide |
200 TransportSummary. 404 when there are no transports. hide excludes self transports. |
GET /all-transports/per-key-stats?selfTransports=hide |
200 object mapping a public key to its counts by type and total. 404 when empty. |
GET /transports/stats/{edge} |
200 {"total": n, "by_type": {...}}. 400. 404 when the visor has none. |
DELETE /transports/deregister |
Deregistration whitelist only, see 02. Body is an array of transport IDs. 403 for an unlisted key or a bad signature. 400 for a bad key, signature or body. 200 when finished. |
GET /bandwidth/transport/{id}?period=daily&limit=7 |
200 bandwidth history. 400, 404. |
GET /bandwidth/visor/{pk}?period=daily&limit=7 |
200 bandwidth history for the visor's transports. 400. |
GET /metric |
200 network aggregate. |
GET /metric/visor/{pks} |
200 aggregate for comma separated visors. 400. |
GET /metrics |
200 per transport metrics. |
GET /metrics/{ids} |
200 metrics for comma separated transport IDs. 400. |
GET /metrics/visor/{pks} |
200 per transport metrics for comma separated visors. 400. |
GET /uptimes?v={v2,v3}&visors={pk;pk} |
200 array of VisorSummary. Without v only pk, on and version. v2 adds daily. v3 adds timeline. |
POST /uptimes |
Body {"pks": [...], "v": "v2"}. Same response as GET. 400 for invalid JSON or an unknown v. |
GET /uptimes/transports?v=&visors=&tp=&type=&edges=true |
200 array of TransportUptimeSummary. visors and tp are semicolon separated. edges=true includes edge_a and edge_b. |
POST /uptimes/transports |
Body {"pks","ids","type","edges","v"}. Same response. 400 for invalid JSON. |
GET /metrics/uptime |
200 {"total_transports","online","by_type":{type:{"total","online"}}}. |
GET /metrics/uptime/{ids} |
200 transport uptime for comma separated IDs. 400. |
GET /metrics/uptime/visor/{pks} |
200 transport uptime for comma separated visors. 400. |
GET /version?on= |
200 map from version to count. on is true, false, all or none. |
GET /versions?on=&status=true |
200 array of {"pk","version","on"}. |
GET /versions/{pks} |
Same for comma separated keys. An unknown key appears with an empty version. 400. |
Other routes¶
| Method and path | Response |
|---|---|
GET /health |
200 httputil.HealthCheckResponse with service name transport-discovery. |
GET /security/nonces/{pk} |
Nonce recovery, see 02. |
GET / |
The charts page. 404 until the charts are running. |
GET /graph, /graph/engine.wasm, /graph/engine.js |
The transport graph page and its engine. /graph returns 404 until the charts run and 503 while the transport cache is cold. |
POST /statuses |
410. |
GET /uptime/now, /uptime/sessions, /uptime/timeline, /uptime/dates |
The service's own uptime record. 503 until a recorder is set. |
CXO Feeds¶
Ports are constants in pkg/skyenv. Layout and cadence are in
31. TPD aggregates two visor feeds and publishes six.
| Port | Identifier | Direction | Content |
|---|---|---|---|
| 50 | DmsgCXOPort |
aggregated from each visor | Visor telemetry and, as a fallback, the tp-list leaf. |
| 69 | DmsgVisorTPListCXOPort |
aggregated from each visor | The visor's own transport list at tp-list. |
| 51 | DmsgTPDMetricsCXOPort |
published | metrics/day/<YYYY-MM-DD>. |
| 52 | DmsgTPDUptimeCXOPort |
published | uptimes/days/<n>. |
| 73 | DmsgTPDStatsCXOPort |
published | stats/network, stats/versions, stats/daily. |
| 75 | DmsgTPDRoutingCXOPort |
published | routing/<pk> and routing/delta/<seq>. |
| 77 | DmsgTPDVisorBWCXOPort |
published | visorbw/day/<YYYY-MM-DD>. |
| 79 | DmsgTPDPerKeyCXOPort |
published | perkey/stats. |
| 55 | DmsgTPDAllTransportsCXOPort |
none | TPD starts no publisher on this port. Subscribers still map the prefix transports/all/. |
Behaviour¶
Registration¶
A visor registers its transports by publishing its list, not by calling HTTP.
When the visor has a CXO publisher for the TPD, the transport manager
(transport.Manager.publishTPDList) writes the full list as the tp-list leaf.
It does so every transportReRegisterInterval (90 seconds) and, after a
deletion debounce of deletionDebounce (5 seconds), after a transport closes.
A visor without a CXO publisher calls POST /v3/transports/ instead and
removes closed transports with POST /transports/delete-batch.
The visor announces to TPD (treestore.Publisher.AnnounceTo). TPD's aggregator
(cxoaggregator) holds the inbound connection and subscribes to the feed whose
key equals the remote key. TPD applies a tp-list leaf with
API.ReconcileTransportsFromCXO.
- Only entries of which the reporter is an edge are accepted.
- Every accepted entry is written, or its lifetime extended when unchanged and due, and the reporter's heartbeat is recorded.
- The reporter's stored transports that are absent from the list are deregistered, unless the other edge still lists them.
- When a fetch fails, the aggregator re-applies the last good list with
API.RefreshTransportsFromCXO, which deletes nothing.
The aggregator also reads the legacy leaves transports/list,
transports/<uuid>/entry and transports/<uuid>/tombstone, the telemetry
shards transports/telemetry/<sh>, the legacy transports/<uuid>/current, and
transports/<uuid>/<YYYY-MM-DD>/timeline.
Deregistration¶
A transport is removed in four ways.
- The registering visor omits it from its next
tp-listleaf. - The registering visor calls
DELETE /transports/id:{uuid}orPOST /transports/delete-batch, or an older visor publishes atransports/<uuid>/tombstoneleaf. - A key in the deregistration whitelist calls
DELETE /transports/deregister. - The entry expires.
Entry timeout¶
Each stored entry has a lifetime of the configured EntryTimeout, default
tpd.DefaultEntryTimeout (5 minutes). Registration and refresh reset it.
Unchanged entries from the same reporter are refreshed after a third of the
timeout (API.SetEntryTimeout).
Caches¶
| Value | Identifier |
|---|---|
| 30 seconds | transportsCacheDelay, refresh of the in memory transport list |
| 5 minutes | uptimesCacheDelay, refresh of the uptime summaries |
| 1 hour | backupTickerDelay, bandwidth archive and timeline archive |
Transport data from visors¶
TPD is not the only source of a visor's transports. Each of the following returns the visor's own list.
- CXO feed. Each visor publishes its list on
DmsgVisorTPListCXOPort(69) under its own key (buildTPListPublisher,pkg/visor/init_stats.go). The root has one leaf,tp-list, which is gzip of the JSON{"version": "<build version>", "c": [CompactEntry...]}. TPD aggregates it. Any subscriber on the feed's allowlist can read it. The allowlist is the visor's peer whitelist plus the TPD key (composeFeedAllowlist). If the dedicated publisher fails to start, the visor publishes the same leaf on its telemetry feed at port 50. - Transport query. Every visor listens on
DmsgTransportQueryPort(68) (router.ServeTransportQueryListener). The service is a gob RPC server (gobrpc) namedTransportQueryRPCGatewaywith the methodQuery. The request isrouter.TransportQueryand the reply isrouter.TransportQueryResponse(target_pkandentriesasCompactEntry). The visor answers only when the query'sTargetPKis its own key,RSNPKis one of its trusted route setup nodes, andRSNSigverifies overSignablePayload. The route setup node signs the query through the setup RPCSignTransportQuery(13). The reply omits closed transports and transports labelledsetup. A stream has a 30 second deadline. The router uses it to build two hop routes without TPD. - Visor RPC.
DmsgVisorRPCPort(65) serves the visor's net/rpc API, includingTransports. Access is limited to the visor's hypervisors and dmsgpty whitelist. The router uses it as a second source for destinations that trust the caller. - Signed list over HTTP. The visor's dmsg HTTP service (port 80, package
logserver) servesGET /transportsonly to callers that reached the visor over a skywire transport. It returns 403 to other callers, 503 when the list is unavailable, 304 whenIf-None-Matchequals the currentETag, and otherwise atransport.SignedList(pk,at,entriesasCompactEntry,sig). The signature is by the visor key over the owner, the build time and the entries.SignedList.Versionnames the set and is theETag. The visor rebuilds the list at most every 5 seconds (transportListTTL) and excludes closed transports and transports labelledsetup. - Local history.
GET /stats/transportsand/stats/transports/historyon the same HTTP service return the visor's transport records with first seen, last seen and daily rollups. They are limited to the survey whitelist when one is configured.
Security Considerations¶
Registration is authenticated by the visor key, either the HTTP signature or the CXO root signature, and TPD accepts only entries that name the reporter as an edge. Entries are not signed by both edges, so a visor can register a transport to a peer that does not hold it; the entry expires unless refreshed. The public routes carry no authentication and are rate limited per caller. The deregistration route is limited to the deregistration whitelist. The transport query is authorised by a route setup node signature bound to one target and one requester. Visor feeds are limited to the peer whitelist and the TPD.