Help us shape the future of Go! Take the 2026 Go Developer Survey

router

package
v1.3.100 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: AGPL-3.0 Imports: 50 Imported by: 0

Documentation

Overview

Package router pkg/router/bottleneck.go c2-net-routing

Package router pkg/router/cascade_ack_registry.go c2-net-routing

ackRegistry is the shared correlation table that maps a cascade sessionID to the goroutine waiting for that session's CascadeAck.

It is shared between the CascadeHandler (which receives ACK packets off the transport manager's single cascade-handler slot and dispatches them) and any CascadeBuilder that injects cascades on the same visor. On the SOURCE visor, the source-side CascadeBuilder sends reserve/install cascades down the route's transports, but the ACKs come back on those same transports and are delivered to the visor's one registered cascade handler. Sharing this registry lets the handler hand those ACKs to the waiting builder instead of needing a second handler slot.

Package router pkg/router/cascade_builder.go c2-net-routing

CascadeBuilder constructs nested cascade messages for the RSN. It builds the Russian-doll structure from last hop to first, signs each layer for its target visor, and sends the cascade to the first hop.

Package router pkg/router/cascade_handler.go c2-net-routing

CascadeHandler processes cascade protocol messages on a visor. It handles CascadeSetupPacket and CascadeAckPacket arriving on route ID 0 of any transport.

Package router pkg/router/cascade_relay.go c2-net-routing

RSN relay: allows visors to reach the Route Setup Node through transport neighbors that have a direct "setup"-labeled transport to the RSN. The relay uses route ID 0 CascadeSetup/CascadeAck packets, piggybacking on the existing cascade infrastructure.

The relay mechanism works as follows: 1. Visor discovers RSN relay peers (cached from previous RSN responses) 2. Visor checks if it transports any relay peer 3. If yes, sends the setup RPC request to that peer on route ID 0 4. The relay peer forwards to the RSN over its "setup" transport 5. Response flows back the same path

This is Phase 2 of the cascade protocol spec.

Package router pkg/router/cascade_route.go c2-net-routing

createRouteGroupCascade implements the two-phase cascade protocol for route setup: reserve IDs from each hop, then install rules. Falls back to DMSG-based setup on failure.

Package router pkg/router/cascade_source.go c2-net-routing

Source-driven cascade orchestration.

In the source-driven design the RSN is a pure dmsg-reachable SIGNING ORACLE: it signs the per-hop reserve/install cascades but never dials hops and is never on the data path. The SOURCE (the requesting visor) drives the cascade down its OWN transports:

  1. Source -> RSN (RPC): CascadeSignReserve -> signed reserve cascade bytes + session IDs for forward and reverse.
  2. Source injects the reserve cascades into Forward[0].TpID / Reverse[0].TpID via its own CascadeBuilder.SendCascade and collects the reserved route-ID ACKs.
  3. Source -> RSN (RPC): CascadeSignInstall (route + session IDs + collected route IDs) -> signed install cascade bytes + the initiating EdgeRules. The RSN recomputes the rules deterministically; it does not trust source-supplied rules.
  4. Source injects the install cascades via SendCascade. Done.

Package router pkg/router/cascade_source_iface.go c2-net-routing

Package router pkg/router/client_pool.go c2-net-routing

ClientPool keeps reusable RPC connections to remote visors so the route setup node doesn't pay a full DMSG noise handshake on every route setup request. Connections are keyed by remote PK and evicted after an idle TTL.

Package router pkg/router/datagram_keys.go c2-net-routing faithful-UDP per-datagram AEAD master key from a route's Noise session. Stage 4b of #2607.

The datagram path deliberately has no stream-Noise channel of its own (per-datagram AEAD replaces the continuous keystream). Rather than run a second handshake, the datagram sibling of a route keys itself off the reliable route's *already-completed* Noise session: both peers take the session's ChannelBinding (the Noise handshake hash — identical on both ends, bound to the full transcript incl. any PQ-hybrid payload) and HKDF-expand it into a shared 32-byte master key. NewDatagramCipher then expands that master, per direction, into the actual ChaCha20-Poly1305 subkeys.

Domain separation: this derivation uses info label "skywire-datagram-master-v1", distinct from the per-direction labels NewDatagramCipher uses ("skywire-datagram-v1-init/-resp"), so the master and the direction subkeys can never collide even though both HKDF off related material.

Package router pkg/router/datagram_setup.go c2-net-routing construction of the faithful-UDP DatagramRouteGroup sibling. Stage 4b of #2607.

A datagram route is NOT a separate route. The reliable route is set up exactly as usual (saveRouteGroupRules: rules + transport + one-time Noise KK handshake → RouteGroup in rgsNs). When BOTH ends locally intend datagram mode for the route, each additionally builds a DatagramRouteGroup over the *same* forward/reverse rules and transport, keyed off the reliable session's ChannelBinding — no second handshake. DataPackets then flow through the reliable group, DatagramPackets through the sibling, dispatched by packet type (router_packet.go).

"Both ends locally intend it" (on-demand-by-local-intent, not a wire negotiation):

  • dial side: DialOptions.Datagram (the DialPacket / forwarded-UDP dialer sets it);
  • accept side: RegisterDatagramPort marks the local serving port (a forwarded_ports.udp port, or a packet-mode app listener).

If only one side intends it, that side's sibling exists but the peer drops its DatagramPackets — no reliable traffic is affected.

Package router pkg/router/dead_route_cache.go

Package router delivery_crc.go: the in-mux DELIVERY check (CapDeliveryCRC).

Framing. When both edges negotiate CapDeliveryCRC, a sequenced DATA frame's payload on the wire is

app_payload ‖ crc32c(seq_be32 ‖ app_payload)   (4 bytes, big-endian)

The stamp is applied in wrapPayload BEFORE the per-frame AEAD seal, so the trailer rides inside the ciphertext and a SACK retransmit or an FEC reconstruction of the same sequence reproduces byte-identical frames. The receiver verifies and strips the trailer in deliverData at DELIVERY time — after reordering, on the in-order path — not on receipt.

Why here and not at the wire: per-frame AEAD already authenticates ONE frame as it arrives. What nothing checked before is that the bytes handed to the application after reassembly are the bytes the sender wrote, in that order — a reorder/flush defect (#4005) stayed invisible until an application hash failed. Binding the CRC to the sequence number means a frame delivered at the wrong position fails the check, not just a frame whose bytes were mangled.

Package router — in-flight first-hop reservations for diversify dials.

siblingRouteGroupExclusions answers "which first hops are taken?" from the route groups that already EXIST. That is blind to the dials still in flight, and a standby-pool fill is exactly the workload that runs several at once (setup.fill_inflight = 8, tunnel.audition_parallel = 3): at startup eight tunnels dialed inside one second each saw an empty exclusion set and each picked the same lowest-latency direct transport, so the pool came up eight deep on ONE first hop (#5125).

A dial therefore CLAIMS its first hop — transport, peer and remote IP — for the duration of the setup, and a sibling dial to the same exit treats the claim exactly like a first hop a live route group holds. The claim is released when the dial returns, by which time its route group is registered and siblingRouteGroupExclusions can see it for itself.

Package router pkg/router/dial_hook.go c2-net-routing routing policy integration point. The router calls a configured DialHook before each DialRoutes invocation; the hook can adjust the per-dial knobs (MuxRoutes, MinHops) or refuse the dial.

The hook is optional. With no hook configured (Config.DialHook is nil), DialRoutes behaves identically to its pre-integration shape. This keeps the integration zero-cost when no operator policy is in play.

The hook is failure-safe: any error from BeforeDial is treated as "no adjustment" and dialing proceeds with the caller's original opts. A buggy or panicking policy cannot break dialing.

Package router pkg/router/dial_tunnel_legs.go c2-net-routing

Multi-leg tunnels AT DIAL TIME.

An app tunnel (skysocks-client's `--tunnels N`, `--routed`) asks for MuxRoutes=1 — "form a route group, even a single-route one" — and its legs are bolted on afterwards, either by the operator (`proxy mux add`) or by the background self-heal growing toward the adaptive width. That costs the first seconds of every session: the tunnel carries on one leg while the others are still being planned and set up.

The leg count is a VISOR-side decision (the mux width lives in the router's adaptive preset, not in the app process, so the app cannot read it), which is why the raise happens here rather than in the app's dial request.

Package router pkg/router/direct_setup.go c2-net-routing

Package router pkg/router/direct_setup_rpc_native.go c2-net-routing

Package router pkg/router/dmsg_wrapper.go c2-net-routing

Package router pkg/router/fec.go — forward-error-correction primitive for the mux data plane (skycoin/skywire #4270). Aggregating one ordered stream across heterogeneous-RTT legs is head-of-line-bound: the no-skip reorder frontier waits for whatever frames were striped onto a slow leg. Retransmit recovery is coupled to that slow leg's RTT. Erasure coding removes the wait: the receiver reconstructs a missing/late data symbol from REPAIR symbols that arrived on the FAST legs, so recovery delay is independent of the slow leg (the escape route (c) from the in-order-stream wall; see the research survey in memory).

This file is the coding core only — a systematic Cauchy-Reed-Solomon block erasure code over GF(2^8): K data symbols are transmitted unchanged (systematic → zero decode cost when nothing is lost) plus R repair symbols; the receiver recovers ALL K data symbols from ANY K of the K+R (MDS). Integration into the mux striping / reorder buffer is a separate step; keeping the code standalone makes it exhaustively unit-testable without the live mesh.

Package router pkg/router/fec_flush.go — idle-flush policy for the packet-mux FEC tail (fec_mux.go, #4270).

THE WEAKNESS THIS CLOSES. Fixed-block FEC only emits repair when a block fills to K data frames (fecStriper.Add). On a unidirectional bulk stream the LAST block is almost always partial: after the final j<K frames the sender goes idle, so those j frames get NO repair, and a tail frame striped onto a slow leg stalls the no-skip reorder frontier with no FEC rescue — the one documented failure mode of fixed-block FEC on a bulk transfer.

THE FIX (adapted to a systematic block code with ZERO wire change). When the sender has been idle for T with a partial block pending, the route-group send loop (RouteGroup.fecFlushServiceFn) completes the block by emitting the K-j remaining slots as REAL empty PADDING frames through the normal send path. Each advances writeSeq and the striper, and the K-th completes the block so Add emits the repair as at any block boundary. The padding frames are real sealed frames (encoder/decoder agree in the wire domain), they fill the seq gap the no-skip reorder would otherwise stall on, and the receiver delivers them as 0-byte reads that the mux delivery loop filters out of the app stream (the app never writes an empty frame — Write rejects len==0). So the tail block becomes a genuine K-full MDS block with no new frame type and no receiver change.

This file holds the pure, deterministic policy the send loop consults; the emission itself lives in route_group.go (which owns leg selection).

Package router pkg/router/fec_mux.go — wiring the systematic Cauchy-RS erasure core (fec.go, #4270) into the packet-mux striping / reorder data plane.

The aggregation wall is head-of-line blocking: the no-skip reorder frontier (reorder.go) cannot advance past a sequence number striped onto a slow leg, so one ordered stream over heterogeneous-RTT legs is capped at the SLOW leg's rate — retransmit recovery is likewise bound to that leg's RTT. Erasure coding removes the wait: the sender groups K consecutive data frames into a block and emits R repair frames; if a data frame is late/lost on a slow leg but ANY K of the block's K+R symbols have arrived (repair frames ride the FAST legs), the receiver RECONSTRUCTS the missing frame and the frontier advances at the fast leg's rate instead of the slow one.

Layering: FEC operates in the WIRE (post-seal) domain. On send the striper is fed the on-wire frame payload AFTER per-frame seal (route_mux.wrapPayload); on receive the reassembler is fed the on-wire payload BEFORE open (route_mux.deliverData), and a reconstructed on-wire frame is passed through the normal open() path before entering the reorder buffer. Because repair symbols are linear combinations of already-sealed frames they reveal nothing beyond the ciphertext, so no separate repair-frame nonce is needed and the code works uniformly whether per-frame noise is on or off. This file is transport-agnostic and driven entirely by (seq, wireBytes) and (blockID, idx, symbol) tuples, so it is exhaustively unit-testable without the live mesh (see fec_mux_test.go).

Package router pkg/router/hol_retx.go c2-net-routing

Package router pkg/router/id_reserver.go c2-net-routing

Package router pkg/router/leg_adopt.go c2-net-routing

Leg ADOPTION — the last move of the shape axis: a LEG RESERVE becomes a stream-level tunnel again.

A split (leg_split.go) hands a packet-level leg back to the pool as a standalone group, and re-home (leg_rehome.go) can take it as a leg again. But the reserve has no app session and no end-to-end handshake, so it could never carry a stream: a chain that was ever split out stayed packet-level for good. Adoption closes that loop. The dialing app asks for a new tunnel naming the reserve (DialOptions.AdoptReservePort); both edges re-point the reserve's one consume rule at the new tunnel's descriptor — the same one-rule rewrite as re-home and split, on the same capability and the same leg control dialect, with one more flag (routing.LegRehomeAdopt) — and the chain is then set up EXACTLY like a freshly dialed route group from that point on: saveRouteGroupRules runs the handshake (initiator here, responder at the exit), the exit's copy goes through AcceptRoutes to whichever app listens on the service port, and the dialing app gets its conn from DialRoutes.

The descriptor is the dial's own, so the local port is the fresh one the app's dial reserved — the reserve's local port is shared with the tunnel it was split out of and could not be reused.

An exit that never learned the flag reads the request as a re-home naming a group it does not hold and refuses it; one that cannot be reached never answers. Either way the reserve is left exactly as it was and the app dials the way it always did.

Package router pkg/router/leg_hops_match.go c2-net-routing

One hop length per packet-level group (leg.hops_match).

A packet-level group stripes every frame across its legs, so its legs should be interchangeable: a direct transport to the exit next to a two-hop chain reorders against it on every frame, fails over to a path with a different shape, and hands the direct leg's observer the exit relationship the chain was built to hide. Every path that adds a leg on its own — the pool arbiter, the shape converger, the pool plan, the route-finder grow, the dial-time and rotation aux legs — asks legHopsMatch first. An operator's explicit `mux add` is not gated.

Package router pkg/router/leg_rehome.go c2-net-routing

Leg RE-HOME: an ACTIVE route group adopts the already-built route chain of a STANDBY route group as one of its packet-level mux legs, in place, with no setup-node dial.

The chain's intermediate rules are pure routeID -> (routeID, transport) switches and carry no descriptor (routing.IntermediaryForwardRule), so they never learn the chain changed hands. What binds a chain to one route group is its single ConsumeRule at each edge, whose descriptor selects the group (router_packet.go: desc := rule.RouteDescriptor() -> r.rgsNs[desc]). Re-home REWRITES that one rule at each edge — same key route ID, new descriptor — and moves the leg object (its transport, its two rules, its recorded hops) into the target group's mux. The standby group is then legless and closes.

This is the incremental half of the shared-trunk end state in docs/design/shared-warm-route-pool.md Phase 3: one chain still terminates in one consume rule, it just changes owner, so no demux is needed at the exit. It takes the capability bit that doc reserved — routing.CapLegRehome.

See docs/design/leg-rehome.md for the packet, both sequences and the in-flight-frame rules.

Package router pkg/router/leg_rtt_window.go c2-net-routing

Package router pkg/router/leg_split.go c2-net-routing

Leg SPLIT — the reverse of leg re-home (leg_rehome.go).

Re-home moves a whole standby CHAIN into an active group as one more packet-level mux leg. A split moves it back out: the leg leaves the group and becomes a standalone STANDBY route group again, with its own ports and its own consume rule at each edge, keeping the same transport and the same reserved route IDs at every hop. Nothing is dialed, nothing is closed.

It is the same rewrite in the other direction — one consume rule per edge re-pointed at a different descriptor — so it rides the SAME packet type with one more flag, routing.LegRehomeSplit, and the same capability bit (routing.CapLegRehome). An exit that never negotiated it simply never acks, and the caller falls back to what the release path did before: closing the transport.

Why it matters: a route has to be able to change from stream level to packet level and back trivially. Before this, the pool arbiter's release (pool_arbiter.go releaseLegByTransport) CLOSED a leg it had taken from the standby pool, so every load episode cost the app a fresh setup dial to refill the pool. A split returns the chain to the pool intact, ready to be taken again by re-home.

Package router pkg/router/local_route_bfs.go c2-net-routing

The breadth-first search behind calculateLocalRoutes' multi-hop fallback: given the visor's own first-hop transports and one snapshot of the network-wide transport graph, find the shortest path to dst whose length lands in [minHops, maxHops].

It lives in its own file because it is the single most expensive thing the router does per dial — on a visor with no direct transport to the destination it walks the whole ~16k-edge TPD graph — and it needs to be benchmarkable in isolation (see local_route_bfs_test.go).

Package router pkg/router/map.go c2-net-routing

Package router pkg/router/mux_control_frame.go c2-net-routing

IN-BAND leg control: the re-home and split request/ack/refusal/commit (leg_rehome.go, leg_split.go) carried as a mux control frame inside an ordinary routing.DataPacket, so it survives ANY relay.

The problem it solves. A relay forwards routing.LegRehomePacket only if its build knows the type (router_packet.go). Live, every re-home whose leg went through an unpinned fleet relay timed out, while the same request on a leg whose transport reached the exit directly was acked every time. Relays cannot be upgraded on demand and the fleet is heterogeneous by design. A DataPacket, though, is forwarded by route ID with the payload untouched — so the control message rides one, and no relay needs to change.

Where it sits relative to the data sequence space. A control frame is NOT application data and must never touch the reorder/SACK machinery: the mux carries a reliable ordered byte stream whose frontier may not be advanced or gapped by anything the app did not write. So a control frame takes its sequence number from a RESERVED high band (muxCtrlSeqBase and up, its own counter), and the receiver dispatches it in handlePacketNow — BEFORE handleDataPacket, the reorder buffer, the SACK tracker and the delivery CRC. It therefore cannot advance the frontier, cannot appear in a SACK, cannot be counted as a reorder drop, and is not retransmit-buffered: a lost control frame is recovered by the re-home ack timeout, exactly as a lost LegRehomePacket was.

Integrity. Identical to a data frame's: per-frame AEAD under the frame's own sequence-nonce when CapPerFrameNoise is negotiated, and a CRC32C over (seq ‖ body) when it is not — the same stamp delivery_crc.go applies, so an in-band frame is never less protected than the raw packet it replaces (which had no integrity check of its own at all).

Which mux seals a reply. A frame is opened by the group that owns the chain's consume rule AT THE RECEIVER when it lands, and per-frame keys are per route group — so a reply must be sealed by the group that owns the chain at the PEER, which during a move is not always the group that ends up with it locally. See replyRehomeOn's callers.

Package router mux_counters.go: cumulative, process-lifetime tallies of mux state transitions whose only other record is the bounded MuxEvent ring (mux_events.go), which drops old entries under sustained churn. Observability only — nothing here is read by control flow, and adding a count never changes what the router decides.

Package router pkg/router/mux_events.go c2-net-routing mux_events.go holds the router's bounded history of route-group and mux-leg changes.

Package router pkg/router/mux_scheduler.go c2-net-routing

Package router pkg/router/mux_shape.go c2-net-routing

The shape vocabulary. A SESSION is one (app, exit) pair; it owns k ACTIVE tunnels holding n_1..n_k legs, and that (k; n_1…n_k) is its SHAPE — "4x1" pure stream level (four tunnels, no packet striping), "1x4" pure packet level, "2x2" today's default (docs/design/mux-shape-axis.md).

Read-only as shipped: this file MEASURES the shape a session has and reports the shape mux.shape asks for. Nothing here moves a chain; the converger is a later step, and until it lands the target is advisory.

Package router pkg/router/mux_shape_converge.go c2-net-routing

The shape CONVERGER — the half of docs/design/mux-shape-axis.md that moves.

mux_shape.go measures a session's shape and reports the one mux.shape asks for. This file closes the gap between them, one move per arbiter tick, using only the four primitives that CONSERVE the session's chain budget C = Σ n_i + |pool|:

standby -> leg     compose     leg_rehome.go rehomeChain (GrowMuxFromPool
                               fallback for an exit without CapLegRehome)
leg -> standby     decompose   leg_split.go splitLeg
standby -> tunnel  promote     the role label flips to active
tunnel -> standby  park        the role label flips to standby, AFTER the
                               tunnel has been decomposed to one leg

Nothing here dials and nothing here closes. A move that cannot be made is DECLINED with the invariant that declined it, never forced.

mux.shape=auto is not converged at all: auto IS today's arbiter, so shapeStep hands the session straight back to poolArbiterStepExcluding and the default behavior is bit-for-bit what develop does. Only an operator who names a shape takes this path.

A tunnel this file PROMOTES carries no yamux session of its own: that session belongs to the dialing app. MuxInfo.ShapeTunnels publishes the target k and the app's reconcileActiveSet (pkg/skysocks/client_live_ops.go) follows it, promoting one of ITS standby tunnels per tunnel the shape asks for, so the streams spread as wide as the shape says. The one case still out of reach is a chain this file DECOMPOSED into a fresh standby group: the app never dialed it, so it has no session to promote.

Two holds stop the moves below. leg.split_on_release gates every decompose, and mux.shape_hold gates the whole step — the visor sets it for an app whose pool.freeze or tunnel.freeze_active is on (I8), which is how an app-process freeze reaches a package that cannot read the app's knobs.

Package router pkg/router/mux_shape_retry.go c2-net-routing

The shape converger's RETRY BACKOFF, and the leg it picks to move.

A re-home or a split that the exit never acknowledges is not a transient: the frame is being dropped by a hop that will still be dropping it in five seconds. Without a memory the converger asked the same dead leg again every arbiter tick, and because splitLeg QUIESCES the leg before it tells the exit (leg_split.go), the tunnel stopped striping over that chain for the whole leg.rehome_ack_timeout of every tick — measured live on 2026-09-23 as a 30 MB download taking 527 s against ~55 s in a shape that was not converging.

So a leg that fails to ack is remembered BY TRANSPORT and left alone for a backoff that doubles from shape.retry_backoff up to shapeRetryCeilingFactor times it, and the converger prefers a leg that has not failed — the direct leg to the exit first, since a transited split is the one a fleet intermediate silently drops.

Package router pkg/router/network_stats.go c2-net-routing

Package router pkg/router/noise_route_group.go c2-net-routing

Package router pkg/router/oracle_plan_cache.go c2-net-routing

ONE destination-transport query per fill, and N distinct paths out of it.

The RSN-oracle already computes the whole candidate set: rsn_oracle_routes.go intersects this visor's own live transports with the destination's own transports (fetched authoritatively through an RSN-signed query) and ranks every shared intermediate. On the campaign rig that intersection is 274 common intermediates — the local visor holds transports to 349 peers, the exit to 576 — and computeDisjoint2HopRoutes returns all 274, best first.

Then oracle2HopRoutes threw 273 of them away and returned legs[0], because its caller wanted one route. So a standby-pool fill of eight tunnels paid eight signed transport queries (a setup-node round trip plus a dmsg-direct delivery to the exit, capped at 14 s each) to compute the same 274-element set eight times — and, since a dial's exclusions come from SIBLING route groups that do not exist yet when the dials are concurrent, every one of them would have ranked the same intermediate first.

This cache fixes both halves:

  • legsFor is a singleflight with a TTL (warm.plan_ttl). N concurrent dials to one exit make ONE query; the rest wait on it and read the same set.
  • claim hands each caller the best candidate no sibling has taken, holding that claim for setup.plan_claim_ttl. N concurrent dials therefore take N DISTINCT intermediates without needing to see each other's route groups.

A claim is advisory. It expires on its own, it is never a promise that the dial succeeded, and a caller that finds every candidate claimed falls back to the best one — the previous behavior, never worse.

Package router pkg/router/parallel_route_config.go c2-net-routing

This file holds the pieces of the parallel-route-setup feature that must compile for EVERY build target (including the plain-tinygo edge whose router.go still calls newSuspectHopCache and whose SetDefaults reads the K bounds): the config bounds and the minimal suspect-hop penalty cache. The race machinery itself (which references the route-setup dialer, NoiseRouteGroup, route-finder, etc.) lives in parallel_route_setup.go behind the non-tinygo build tag. Keep this file free of route-setup-only symbols.

Package router pkg/router/parallel_route_setup.go c2-net-routing

parallel_route_setup.go implements PARALLEL candidate route-group setup: the steady-connection fix. Instead of the old one-candidate-at-a-time retry loop — where every dead / non-forwarding candidate burned the full handshakeAwaitTimeout (~10s) before the next was tried, so a run of bad candidates could burn the whole ~90s dial ceiling and wedge the app in "starting" — DialRoutes now sets up the top-K candidate routes CONCURRENTLY and the FIRST to complete its reciprocal handshake WINS and becomes the working route. Losing / failed candidates are canceled, torn down, and their intermediate hops marked suspect for a short TTL so the NEXT dial front-loads known-good hops.

Data-model constraint (why the two phases below): incoming handshake / data packets are demuxed to a route group by RouteDescriptor (see dispatchToRouteGroup), and every candidate of one dial shares the SAME descriptor (same src/dst/ports). The router therefore holds at most ONE raw route group per descriptor (r.rgsRaw[desc]), so two candidates cannot run their reciprocal handshakes concurrently — the second would collide on the descriptor. The race is consequently two-phase:

phase 1 (CONCURRENT): dial/reserve route IDs across every candidate's hops.
    This is where an unreachable intermediate fails id_reservation and loses
    the race cheaply, instead of blocking the whole dial. This is the
    dominant live failure mode (dead/non-forwarding intermediate).
phase 2 (SERIAL, fastest-reservation-first): complete the reciprocal
    handshake on reserved candidates; the FIRST to complete wins and is
    returned immediately (it is the never-dropped working base — additional
    mux legs grow on top later and must never unseat it). Because the
    candidates are already reserved, walking to the next one on a handshake
    failure costs no re-fetch and no re-reservation — unlike the old loop.

Package router pkg/router/park_hysteresis.go c2-net-routing

Package router pkg/router/pk_sort.go c2-net-routing

Package router pkg/router/pool_arbiter.go c2-net-routing

The standby POOL ARBITER — a pooled tunnel is a stream reserve AND a packet-level mux leg, decided at runtime rather than at dial time.

The proxy's standby pool (pkg/skysocks/client.go) holds tunnels open, pings them and ranks them so a stream that needs a route does not wait for a setup dial. Each one is a whole route group with ONE leg. Two facts follow:

  • A standby tunnel must stay single-leg. Widening one costs a second route chain to the same exit for a tunnel that is carrying nothing, and the exit pays for it: a rig run with the visor mux width at 2 grew a second leg on every one of 32 pooled tunnels through the self-heal top-up — 64 legs to one exit, 57 leg_parked, 23 reorder wedges, truncated downloads and +67 MiB of exit RSS. So width, dial.tunnel_legs and self-heal widening are ACTIVE-tunnel powers; see poolWideningAllowed.

  • The idle chains in that pool are exactly what a LOADED active tunnel wants. When the fan-out load signal engages on an active group and its leg count is under the mux width, this file takes the best pooled tunnel and makes it a leg — by re-home (leg_rehome.go: the chain changes owner in place, no dial) or, against an exit that never negotiated CapLegRehome, by growing on the pool's own plan (pool_legs.go). When the load goes away the leg is released and the app's fill tops the pool back up.

With pool.compose_idle (the default), the width is not a load response at all: an ACTIVE tunnel is held at pool.active_width legs whenever it is below it, idle included, and is never released below that width. The packet-level mux is here for privacy and for surviving a cut leg, and a spare that only appears after the load latches cannot survive a cut that happens before it — so the striping cost of an idle second chain is accepted. Legs taken ABOVE the idle width under load still go back after pool.leg_release.

The hysteresis is what keeps this from becoming churn: the load must have LATCHED (the forward fan-out's own latch, or a continuous transfer episode of at least unidir.fanout_engage in the reverse direction), at most one leg per pool.leg_interval, never below pool.min_standby tunnels left in the pool, and a leg is released only after pool.leg_release with no load at all.

Stream promotion always wins. The arbiter only ever considers a sibling the APP still labels standby — the promoter re-stamps its pick "active" through NoteTunnelEvent before it puts a stream on it, so a tunnel the promoter has just taken is invisible here by the time the arbiter next ticks.

Package router pkg/router/pool_legs.go c2-net-routing

POOL-SOURCED MUX LEGS — grow an active tunnel on the routes its own STANDBY tunnels already proved.

A multi-tunnel proxy (skysocks-client's standby pool, skyenv. SkysocksClientStandbyPool) holds one tunnel per disjoint route to the exit, up to pool.size_cap. Each one is a whole route group with a SINGLE leg (the app dials MuxRoutes=1), already ranked by the promoter and already carrying a live first-hop transport. When the ACTIVE tunnel wants a second packet-level mux leg, today it discovers one from scratch: a route-finder round trip plus the disjointness BFS, per leg, per group — while a dozen ready, measured, disjoint paths to that same exit are sitting in the pool.

This file makes the pool the FIRST source of aux-leg plans. It does not — and cannot — hand a standby tunnel's RULES to the active group: a chain's consume rule at each end binds it to one route descriptor, so rules are never shared between groups (docs/design/shared-warm-route-pool.md). What is shared is the PLAN: the forward+reverse hop sequence, which is group-independent. Seeding those plans into the visor-level warmRoutePool means addOneAuxLeg — and the grow operation below — dial their own rule chain onto a known-good route with NO route-finder call at all.

The governing rule from the same design doc's last section: a pool entry is a (transport, route_id) logical route, and legs MAY share a transport with a standby tunnel; what must never be double-used is the ACTIVE set's binding bottleneck. So a plan whose first hop the target group already holds is skipped here, before it can reach the cache — two legs on one first hop are one link's capacity wearing two route IDs.

Ranking is by the standby tunnel's MEASURED end-to-end route latency (the number a routing policy judges a leg by — MuxLeg.RouteLatencyMS / legEndToEndLatencyMs), with the first hop's observed throughput prior (transport.Entry.ThroughputBps via ManagedTransport.GetThroughputBps, the same signal the dial ranker reads through throughputFor) as the secondary key. A pool tunnel has been pinged and used; that is strictly better information than the route-finder's topology-only estimate.

Settlement: the app's own pool settlement marks (poolSettledAt / poolSettledN, pkg/skysocks/client.go) never cross into the visor — the router sees tunnels, not the app's pool state machine. So there is nothing to wait for: the seed is taken LAZILY on each grow, off whatever standby tunnels exist at that instant. An empty or unsettled pool seeds nothing and every path below falls through to today's route-finder dial, unchanged.

Package router pkg/router/rack_tlp.go c2-net-routing

Package router pkg/router/reorder.go c2-net-routing

Package router pkg/router/route_group.go c2-net-routing

Package router pkg/router/route_group_close.go c2-net-routing

Package router pkg/router/route_group_dialer.go c2-net-routing

Package router pkg/router/route_group_hops.go c2-net-routing

Package router pkg/router/route_group_intake.go c2-net-routing

Package router pkg/router/route_group_legs.go c2-net-routing

Package router pkg/router/route_group_noise.go c2-net-routing

Package router pkg/router/route_group_ping.go c2-net-routing

Package router pkg/router/route_group_sack.go c2-net-routing

Package router pkg/router/route_group_services.go c2-net-routing

Package router pkg/router/route_group_stats.go c2-net-routing

Package router pkg/router/route_mux.go c2-net-routing

Package router pkg/router/route_mux_ack.go c2-net-routing

Package router pkg/router/route_mux_delivery.go c2-net-routing

Package router pkg/router/route_mux_legs.go c2-net-routing

Package router pkg/router/route_mux_record.go c2-net-routing

Package router pkg/router/route_mux_select.go c2-net-routing

Package router pkg/router/route_mux_window.go c2-net-routing

Package router pkg/router/route_status.go c2-net-routing

Package router pkg/router/router.go c2-net-routing

Package router pkg/router/router_config.go c2-net-routing router_config.go contains setters, config methods, and Close.

Package router pkg/router/router_dial.go c2-net-routing router_dial.go contains route dialing and route finding logic.

Package router pkg/router/router_dial_filter.go c2-net-routing

Package router pkg/router/router_dial_local.go c2-net-routing

Package router pkg/router/router_dial_mux.go c2-net-routing

Package router pkg/router/router_dial_select.go c2-net-routing

Package router pkg/router/router_forward.go c2-net-routing

The TRANSIT write path: what this visor does with a frame it only relays.

forwardPacket used to end in a SYNCHRONOUS tp.WritePacket on the router's single inbound packet loop, with the router's serve context — which never cancels. managedTransport.writeTo takes the transport write lock and only then sets a deadline of one minute, so ONE peer that stopped draining (a stranger routing through us, whose frames are the cheapest thing on the visor to drop) parked the loop that feeds EVERY route group. Measured live on 2026-09-18: a ~30 s all-paths blackout on the local visor, every transport logging "Dropping packet: readCh full for 30s (application not reading)" while the goroutine dump showed the loop in writeTo for a transit peer. The same class as #5018, which took the route-group READ side off this loop with a per-group intake worker; this is the write side.

So: the loop hands the frame to a bounded per-next-hop-transport queue and returns. One writer goroutine per transport drains it — which also preserves the ordering the shared write lock used to give — and each write carries forward.write_timeout as its own deadline. When the peer does not drain, the queue fills and frames are DROPPED with a counted, named reason (forward_drop_queue_full / forward_drop_write_timeout), visible in `visor state --select diag` as intake.forward_queues and, once per forward.drop_event_window per transport, as a forward_drops mux event.

Package router pkg/router/router_gc.go c2-net-routing router_gc.go contains garbage collection for routing rules.

Package router pkg/router/router_intake.go c3-rtr-core

Intake diagnostics for `visor state`: what the router's single inbound packet loop is doing with what the transports hand it. Counted here rather than only logged so a stalled or misdelivered path shows up in a snapshot.

Package router pkg/router/router_leg_controls.go c2-net-routing

Per-LEG operator controls on a live route group, and the negotiated view of the group they act on.

Two of these existed in the data plane with no way to reach them: WeightModeExplicit / SetExplicitWeights (transport_selector.go) had no caller outside the routing-policy distribution, and a FORWARD-ONLY leg (appendRouteAsymmetric addFwd=true/addRev=false) could only be produced by a preset's rotation hook. Both are operator decisions — "put 70% of the send on this leg", "this leg is upload capacity only" — so both get an RPC here and a CLI verb in `skywire cli proxy mux`.

These are OPTIONAL router capabilities: pkg/visor reaches them by asserting this interface on its router rather than widening the Router interface, so a router build without them (tinygo source-only) simply reports the control as unavailable instead of failing to compile.

Package router pkg/router/router_mux_ops.go c2-net-routing router_mux_ops.go contains mux route operations on the router.

Package router pkg/router/router_packet.go c2-net-routing router_packet.go contains all packet handling logic.

Package router pkg/router/router_pending.go c2-net-routing

Package router pkg/router/router_pending_legs.go c2-net-routing

Package router pkg/router/router_route_source.go c3-rtr-core

Where multi-hop routes came from, for `visor state --select diag`: the route finder, the local graph (a hypervisor's attached visors, #4750), or the local fallback after a route-finder miss. Counted so the saving from a local graph is visible in a snapshot rather than only in logs.

Package router pkg/router/router_rules.go c2-net-routing router_rules.go contains rule and route group management logic.

Package router pkg/router/router_serve.go c2-net-routing router_serve.go contains functions for serving and accepting routes.

Package router pkg/router/router_setup_rpc_native.go c2-net-routing

Package router pkg/router/routerclient.go c2-net-routing

Package router pkg/router/rpc_gateway.go c2-net-routing

Package router pkg/router/rsn_oracle_routes.go c2-net-routing

RSN-oracle 2-hop route computation (phase-1).

Given the source's OWN transports and the destination's OWN transports (fetched authoritatively from the destination via the RSN-signed transport-query, transport_query.go), the set of single-intermediate (2-hop) routes S->I->D is the overlap of the two peer sets: an intermediate I is usable iff S has a live transport S-I AND D has a transport I-D. No TPD snapshot is consulted — the common route type becomes independent of the transport-discovery service's global view.

This is the S-side crux and is deliberately a pure function of its inputs (computeDisjoint2HopRoutes) so it is unit-testable with mock transport sets. The result is a disjoint set of legs (one per distinct intermediate PK) ready to feed the existing mux establishment (establishMuxRoutes consumes ForwardHops/ReverseHops per leg).

Package router pkg/router/rules_map.go c2-net-routing

Package router pkg/router/sack.go c2-net-routing

Package router pkg/router/sbd_trial.go c2-net-routing

The route group's half of "a shared-bottleneck park is a trial": the pending trials, the verified-independent pair suppression, and the unpark a failed trial performs. The decision arithmetic and the constants live in bottleneck.go next to the detector whose ruling they qualify.

Package router pkg/router/service_gate.go c2-net-routing

Suspending a route group's service loops while they cannot do useful work.

A group runs seven knob-paced service loops. Three of them tick ten and two times a second — send-window, tlp, reorder-stall — and on a group that is holding a route open without moving bytes, all three are provable no-ops: tlpProbeSeq bails when the retransmit buffer holds nothing outstanding, reorderStallServiceFn bails when the frontier has no gap, and a send window recomputed from unchanged inputs yields the unchanged window. Two more — legstate-resync and unidir-flip — iterate from leg index 1, so they are no-ops on a single-leg group by construction.

That is ~23 timer wakeups per second per group per end spent proving there is nothing to do. It does not matter at one group; the standby tunnel pool holds dozens, and its ceiling is in the hundreds, where it is most of the visor's idle CPU.

So those five loops take a gate. While the gate says dormant the loop stops its ticker and parks on a broadcast channel, costing nothing but its stack; the write path, the receive path and the leg-append path wake it. The gate is re-evaluated after every wake, so a spurious wake is free.

The loops that are NOT gated are the ones that must fire precisely when nothing is happening: keep-alive, leg-liveness, sack, and leg-dataprogress — whose sole-leg black-hole path exists to notice a leg that sent bytes and delivered none.

Package router pkg/router/settings.go c2-net-routing

Runtime-settable router knobs, so `skywire cli route settings` can move the dataplane on a running visor instead of a rebuild and a fleet deploy.

The values live in pkg/router/routersettings — one catalog entry per knob, each seeded with the constant this package compiled with, so a visor that never sets one behaves byte for byte as it did. This file is the bridge: the named accessors the rest of the package and pkg/visor call, plus the per-route-group resolution of an app's overrides.

Two shapes of read:

  • a package-level accessor (DeadRouteHold, SBDMinSamples, …) reads the visor-wide value. Used where there is no route group in hand;
  • a *routersettings.View read through rg.knobs() / m.knobs() carries the owning app's overrides. The view is resolved ONCE when the group is built and re-resolved only when the catalog version moves (the group's send-window service tick notices), never per packet.

Package router pkg/router/settings_defaults.go c2-net-routing

The compiled defaults that do not belong to any one mechanism's file, kept as named constants so the catalog has something to be asserted against (TestCatalogDefaultsMatchConstants) and so a reader of `route settings` can find the value the binary was built with.

Package router pkg/router/settings_dial.go c2-net-routing

The DIAL-TIME half of the router's knob bridge: the route-ranking priors, how many candidates a dial asks the finder for, the warm-plan cache shape, the dead-route young-death window, the dial-time tunnel width and the operator's prefer-these-peers list.

Same contract as settings.go, and the SAME catalog: every scalar here is a pkg/router/routersettings entry, so it is set with `route settings k=v`, read back in `route settings --json` under knobs / knob_detail, scoped per app where a dial knows the app, persisted into routing.router_settings and restored at the next start. `route settings dial` is a convenience VIEW over this section of the catalog, not a second mechanism.

It is a separate file from settings.go only to keep the two sets reviewable apart: settings.go holds the send-window/bottleneck/forward knobs that act on a LIVE route group, this one holds the knobs a dial reads while it is picking a route.

The one non-catalog member is the prefer-these-peers LIST: the catalog's payload is an int64 per knob, which a set of public keys is not. It keeps its own atomic here and its own RPC field.

Package router pkg/router/settings_group.go c2-net-routing

Per-route-group resolution of the knob catalog.

`route settings --app <name> k=v` sets an override map for the route groups a given app owns, so a subject client and its paired reference client can run different values on one visor. The app name is the one the visor already knows: the tag saveRouteGroupRules attaches from the appserver proc's dial options (RouteGroup.SetAppName).

Resolution happens ONCE when the group is built, again when the app tag arrives, and thereafter only when the catalog version moves — the group's send-window service loop calls refreshKnobs on its tick, which is the change notification. A read on the data path is one atomic pointer load; nothing resolves per packet.

Package router pkg/router/setup_batch.go c2-net-routing

The setup-node side of the batched route-setup protocol (routing/batch_route.go): install N routes to ONE destination with the per-hop work coalesced.

What coalescing actually removes. A single CreateRouteGroup for a 2-hop route S->I->D does, per route:

3 dials / pool borrows (S, I, D)   -> one id-reservation RPC each
1 AddIntermediaryRules RPC on I
1 AddEdgeRules RPC on D

Eight such routes to the same exit over eight different intermediates cost 8 setup requests and 8*5 = 40 router RPCs, of which 16 are the SAME two peers (S and D) asked for route IDs eight times each.

CreateRouteGroupBatch builds ONE IDReserver over every member's forward and reverse hop lists. NewIDReserver already tallies "how many route IDs does this PK owe" across all the paths it is given, so S and D are dialed once and asked once — for all eight routes' worth of IDs in a single RPC. The intermediary-rule broadcast is merged the same way: the rules of every member that traverses a given hop go out in one AddIntermediaryRules call to that hop, so a batch whose members SHARE an intermediate (the common shape once the pool grows past the count of distinct first hops) collapses those too.

The 8-route fill above becomes 1 setup request + 10 id reservations (S, D, I1..I8) + 8 intermediary installs + 8 destination edge installs = 27 RPCs against 48, and the two peers common to every route are dialed twice instead of sixteen times.

Partial success is the contract. A member whose intermediate is unreachable, whose route fails Check, or whose destination edge install is refused comes back with an Error string while its siblings come back installed — and only that member's already-installed rules are torn down, from its own tracker. The only all-or-nothing step is the shared id reservation: if the source or the destination cannot be reached at all there is no batch to run, and every member fails with that one error.

Package router pkg/router/setup_batch_client.go c2-net-routing

The INITIATOR side of batched route setup: collect the sibling dials a multi-route request produces and send them to the setup node as one message.

Why a coalescer and not a "DialRoutesBatch" entry point. Every multi-route dial this visor makes already fans out into concurrent single dials — the standby-pool fill, `--tunnels N`, the mux leg self-heal, a diversify dial's K-race. They arrive at setupNodeDialer.Dial within milliseconds of each other, each carrying its own BidirectionalRoute to the SAME exit. Collecting them here batches every one of those callers without any of them knowing about batching, and without touching DialRoutes' 650 lines of route-group construction, handshake and rule bookkeeping.

The window is paid by the FIRST dial of a burst and by nobody else: a lone dial waits setup.batch_window (40 ms by default) against a setup round trip whose measured p50 is 0.6-2.1 s. A burst of eight pays it once, for all eight.

Negotiation, not a flag. The leader asks the setup node for its capability list once per connection; a node that does not advertise CapBatchRouteSetup (or that predates the Capabilities RPC entirely, answering "can't find method") makes the whole group fall back to today's concurrent single requests. Nothing has to be configured on either side, and a mixed fleet of setup nodes works.

Package router pkg/router/setup_path_stats.go c2-net-routing

Which setup path this visor's routes actually took, on the VISOR side.

The setup node's /stats counts requests by kind, but a visor operator cannot read the setup node's whitelisted view and, more to the point, the question they are asking is about their own dials: did the batched form get used at all, did the oracle query once per fill or once per route, and did the concurrent dials of a fill end up on distinct intermediates. This is that, under `visor state --select diag` as route_setup.

Package router pkg/router/setup_rpc_gateway.go c2-net-routing

Package router pkg/router/setupclient.go c2-net-routing

Package router pkg/router/setupconfig.go c2-net-routing

Package router pkg/router/setupnode.go c2-net-routing

Package router pkg/router/tpd_cache.go c2-net-routing

Package router pkg/router/transport_close.go c2-net-routing

What happens to a route group when a transport underneath it goes away.

A leg is a (transport, forward rule, consume rule) triple, and nothing used to tell a leg that its transport had closed. The transport manager has fired a close hook since the VStream muxes needed one (transport.Manager. OnTransportClosed), but the router never registered: it found out by polling tp.IsClosed() from the keep-alive and leg-liveness services, and both of those step AROUND a closed transport rather than rule on it. legLivenessServiceFn skips a closed leg, so that leg is never probed, never missed and never declared dead; pruneDeadTransports refuses to drop the last leg. A single-leg group whose transport the visor itself removed therefore kept all of its rules and stayed registered, and the session on top only learned of the death when one of its own writes happened to land on the dead transport and error.

On a download that write can be a long time coming: the bytes stop, so the app consumes nothing more, so it emits no window update, so nothing is written, so nothing errors. Three runs of near-identical trees on the standby-pool bench measured the same cut at 0.5 s, 5.3 s and 14.4 s to first byte — the spread was entirely "when does this side next write?", not anything about the failover, which is same-tick once the session errors.

A transport the visor removed is not a silent peer. The liveness probe is for peers that went quiet; this is a link we KNOW is gone, so it is ruled on at the close rather than at a timeout.

Package router pkg/router/transport_list_routes.go c2-net-routing

Package router pkg/router/transport_query.go c2-net-routing

RSN-oracle transport-list query protocol (phase-1).

A source visor S that wants to build a SINGLE-INTERMEDIATE (2-hop) route S->I->D needs only two transport sets: its OWN (known locally) and the destination D's OWN. The intermediate set is their overlap in peer PKs. D's transports are authoritative and fresh from D itself, so a 2-hop route can be computed WITHOUT the transport-discovery service (TPD) — TPD is only needed for routes with >=2 intermediates.

This file defines the query the source carries to the destination, reusing the cascade RSN-signature primitive (cipher.Sign/Verify against a per-visor trusted-RSN allowlist): the setup node (RSN) signs a query CAPABILITY targeting D, and D honors the query only if the RSN is on its trust list and the signature verifies for D's own PK — exactly the authorization model CascadeSetup.Verify already enforces (see cascade.go). The query is a one-shot control-plane message, so it marshals as JSON (the compact-entry response already carries json tags); the cascade's hand-rolled binary form is reserved for the per-hop data-plane-adjacent path.

Package router pkg/router/transport_query_dmsg.go c2-net-routing

dmsg-direct (phase-1) delivery for the RSN-oracle transport-list query.

A source visor S carries an RSN-signed TransportQuery to destination D over the always-available dmsg relay — the same way `tp --remote` / the setup client reach a remote visor's RPC over dmsg (see setupclient.go). D listens on skyenv.DmsgTransportQueryPort, verifies the RSN signature against its trusted-RSN allowlist, and returns its own transport list.

Both halves are served over gob RPC to match the existing setup-node path (net/rpc server, gobrpc client — wire-compatible), so no new codec is introduced. The visor wires both halves at startup (init_router.go).

Package router pkg/router/transport_selector.go c2-net-routing

Package router pkg/router/unidir.go c2-net-routing

Package router pkg/router/warm_route_pool.go c2-net-routing

Package router pkg/router/wrappers.go c2-net-routing

Index

Constants

View Source
const (
	// DatagramCounterSize is the on-wire counter prefix length.
	DatagramCounterSize = 8

	// DatagramTagSize is the Poly1305 authentication tag length.
	DatagramTagSize = chacha20poly1305.Overhead

	// DatagramOverhead is the total per-datagram wire overhead
	// (counter prefix + auth tag). Plaintext + DatagramOverhead is
	// what lands in a DatagramPacket's payload.
	DatagramOverhead = DatagramCounterSize + DatagramTagSize

	// MaxDatagramPlaintext is the largest plaintext datagram body
	// that fits in a single DatagramPacket after AEAD wrapping.
	// Computed from MaxDatagramPayload (the routing-packet payload
	// cap) minus DatagramOverhead.
	MaxDatagramPlaintext = MaxDatagramPayload - DatagramOverhead

	// DefaultRekeyTimeInterval is the time-based rekey trigger.
	// WireGuard uses 2 minutes; matching that for operational
	// familiarity.
	DefaultRekeyTimeInterval = 2 * time.Minute

	// DefaultRekeyPacketLimit is the packet-count rekey trigger.
	// 2^32 = ~4.3B packets, well below the ChaCha 2^60 bound but
	// generous compared to realistic workloads (a 100 Kpps stream
	// for ~12 hours).
	DefaultRekeyPacketLimit uint64 = 1 << 32
)

Datagram AEAD wire constants. Public so Stage 4's MaxPayload computation can subtract the correct overhead.

View Source
const (
	MuxEventGroupCreated = "group_created"
	MuxEventGroupClosed  = "group_closed"
	MuxEventLegAdded     = "leg_added"
	MuxEventLegRemoved   = "leg_removed"
	MuxEventLegDropped   = "leg_dropped"
	MuxEventLegParked    = "leg_parked"
	// MuxEventLegPromoted is a warm-standby leg entering the active stripe set —
	// the operator pinned it (mux add / mux set) or the policy tick promoted it.
	// The counterpart of MuxEventLegParked, so churn is countable in both
	// directions.
	MuxEventLegPromoted = "leg_promoted"
	// MuxEventParkTrialFailed is a shared-bottleneck park that GOODPUT refuted:
	// the group's aggregate delivered-bytes rate fell by more than the trial
	// limit with the leg parked, so the legs were carrying independent capacity
	// after all. Its Reason names both rates (before → after, B/s) and the
	// exemption the pair earned, so a bench run can count wrong SBD rulings and
	// see what each cost. Always followed by the leg_promoted of the unpark.
	MuxEventParkTrialFailed = "park_trial_failed"
	// MuxEventSBDRuling is a shared-bottleneck ruling the detector MADE and did
	// not act on: with demotion off (the default, see sbdDemoteDefault) a leg the
	// detector judges redundant is recorded and kept. Its Reason names the leg,
	// the active leg it was judged against, the pair's summary statistics and the
	// send-path rate behind the reading — everything a bench run needs to score
	// the detector against what the transfer actually did, without letting it take
	// a leg away to find out.
	MuxEventSBDRuling = "sbd_ruling"
	// MuxEventLegStalled is a leg the data-progress detector found stalled
	// behind an open reorder gap on a side that does NOT own the active set —
	// an ACCEPTOR honoring the initiator's CapLegState mirror. The observation
	// is still worth recording (it names the leg and the gap age), but the park
	// belongs to the initiator, so no park follows and nothing is mirrored back.
	MuxEventLegStalled    = "leg_stalled"
	MuxEventLegAddFailed  = "leg_add_failed"
	MuxEventPrimaryRehome = "primary_rehomed"
	// MuxEventForwardRehomed is the FORWARD direction (client → exit: uploads and
	// requests) moving to a different leg. It rides ONE leg — the direct leg, or
	// else the lowest-latency one — so every move is a decision worth a line: its
	// Reason says whether the leg it held went away (dead, parked to standby, or
	// outclassed by a direct leg that joined) or a challenger measured far enough
	// below it for long enough (--forward-switch-margin, two consecutive samples).
	// A run full of these is the flapping the margin exists to stop.
	MuxEventForwardRehomed = "forward_rehomed"
	// MuxEventForwardFanout / ...Confined bracket the FORWARD direction's
	// load-triggered fan-out: the leg it is confined to sat at its send window
	// for unidir.fanout_engage without a break and a sibling within
	// unidir.fanout_max_skew took the overflow, and later the load subsided and
	// the direction went back to the one leg. An upload that never raises one is
	// an upload that fitted on a single leg — the confinement working, not the
	// aggregation missing.
	MuxEventForwardFanout   = "forward_fanout"
	MuxEventForwardConfined = "forward_confined"
	// MuxEventReorderWedge / ...Cleared bracket a RECEIVE-side reorder wedge:
	// the frontier held past reorderTimeout because the sender's retransmit
	// never refilled the missing sequence, and then it advanced again. Whole-
	// group events (LegIndex -1) whose Reason names the stuck seq, how long it
	// stayed stuck and how many packets were dammed behind it — so the wedge
	// outlives the visor log ring and is readable from `visor state`.
	MuxEventReorderWedge        = "reorder_wedge"
	MuxEventReorderWedgeCleared = "reorder_wedge_cleared"
	// MuxEventDialDecision is recorded once per group this visor dialed with
	// DiversifyTransports set: its Reason is the route-choice trail (sibling
	// exclusions, candidate filtering, the chosen first hop), so a tunnel that
	// shares a first hop with its siblings says why, from `visor state`.
	MuxEventDialDecision = "dial_decision"

	// MuxEventPoolLegTaken / ...Released bracket the standby-pool arbiter's dual
	// use of a pooled tunnel (pool_arbiter.go): a LOADED active tunnel took a
	// pooled chain as one more packet-level mux leg, and gave it back when the
	// load was gone. The Reason names the load signal and the standby tunnel's
	// own port, so a leg that is not the group's own dial says where it is from.
	MuxEventPoolLegTaken    = "pool_leg_taken"
	MuxEventPoolLegReleased = "pool_leg_released"

	// The TUNNEL-level counterparts of the leg events above, decided by the
	// APP that holds the tunnels (the skysocks standby pool) and reported into
	// this ring through NoteTunnelEvent. A tunnel is a whole route group, so
	// these are group events (LegIndex 0, stamped with the group's first hop)
	// rather than leg events.
	//
	// MuxEventTunnelPromoted is a standby tunnel entering the active set —
	// after an active tunnel died, or because the promoter judged it better.
	// MuxEventTunnelParked is its counterpart, an active tunnel going back to
	// standby, so churn is countable in both directions. MuxEventTunnelRetired
	// is the death that starts a failover: the app saw no pong and no bytes on
	// the tunnel for its hard-dead window and closed the session.
	MuxEventTunnelPromoted = "tunnel_promoted"
	MuxEventTunnelParked   = "tunnel_parked"
	MuxEventTunnelRetired  = "tunnel_retired"
	// MuxEventTunnelConsumed is a standby tunnel SPENT rather than lost: its
	// whole built chain was re-homed into an active group as one more mux leg
	// (leg_rehome.go), so the tunnel's own route group closed on purpose. The
	// distinction matters to the app: a death re-arms the redial backoff and the
	// pool fill, and doing that for a tunnel nobody lost is a redial storm. The
	// Reason names the group the chain went to.
	MuxEventTunnelConsumed = "tunnel_consumed"
	// MuxEventTunnelSnubbed / ...Unsnubbed bracket a QUEUE-AWARE no-progress
	// sit-out, borrowed from bittorrent: a tunnel holding outstanding chunks
	// that has delivered no byte and no ack for its snub bound is snubbed, its
	// outstanding work re-issued elsewhere, and it is re-tried with ONE chunk
	// after the hold. The Reason names the silence and the bound.
	MuxEventTunnelSnubbed   = "tunnel_snubbed"
	MuxEventTunnelUnsnubbed = "tunnel_unsnubbed"

	// MuxEventLegProbeOnly / ...LegFullShare bracket the OUTCLASSED-LEG gate: a
	// leg whose delay basis exceeds the group's best active leg's by more than
	// --leg-starve-ratio while delivering under 1/that of its goodput is cut to
	// --leg-probe-bytes per window instead of the
	// proportional share the round-robin schedule would give it, and restored
	// when its basis comes back. Neither is a park — the leg keeps its rules and
	// its probe — so they are recorded apart from leg_parked/leg_promoted. Their
	// Reason names both bases and the ratio, which is what a bench run scores the
	// gate by.
	MuxEventLegProbeOnly = "leg_probe_only"
	MuxEventLegFullShare = "leg_full_share"
)

Mux event kinds.

View Source
const (
	MuxByLocal    = "local"
	MuxByRemote   = "remote"
	MuxByOperator = "operator"
	MuxByAdaptive = "adaptive"
	MuxByPolicy   = "policy"
	// MuxByPeer is a state change this side did not decide: the REMOTE end parked
	// or promoted the leg and signaled it over CapLegState, and we mirrored it.
	// Distinct from MuxByRemote (which marks a remote-initiated group/leg
	// teardown) so a leg that is standby purely because the peer says so is
	// attributable from `visor state` — it was previously adopted silently, which
	// is how a leg stayed parked for five minutes with zero park events logged.
	MuxByPeer = "peer"
)

Mux event initiators (MuxEvent.By).

View Source
const (
	// DefaultRouteKeepAlive is the default expiration interval for routes.
	// Routes are refreshed on every data transfer. This timeout only
	// fires when no data has been sent for this long. Previously 24h,
	// reduced to 10 minutes so idle routes (e.g. from the resolving
	// proxy's one-shot HTTP requests) are cleaned up promptly instead
	// of consuming routing table entries for a full day.
	DefaultRouteKeepAlive = 10 * time.Minute
	// DefaultRulesGCInterval is the default duration for garbage collection of routing rules.
	DefaultRulesGCInterval = 10 * time.Second
)
View Source
const (
	// CapBatchRouteSetup means DialRouteGroupBatch is implemented: N routes to
	// one destination in one request, with the per-hop work coalesced.
	CapBatchRouteSetup = "batch-route-setup"
	// CapCascadeSign means the CascadeSign* RPCs are implemented (the
	// source-driven cascade, where the node signs and the source injects).
	CapCascadeSign = "cascade-sign"
	// CapTransportQuery means SignTransportQuery is implemented (the RSN-oracle
	// destination-transport query).
	CapTransportQuery = "transport-query"
)

Setup-node capability names. A client sends a newer request shape only when the node has advertised the matching capability, so an un-upgraded setup node never receives a message it cannot decode and no flag has to be flipped on either side (see Capabilities / HealthCheck).

View Source
const (
	RequestTimeout = time.Second * 60
	ReadTimeout    = time.Second * 30
)

Various timeouts for setup node.

View Source
const DefaultPoolTTL = maxPooledReuseIdle

DefaultPoolTTL is how long an idle connection stays in the pool before the eviction loop reaps it. It MUST stay at or below maxPooledReuseIdle: a connection that idles past the stream read deadline is already shut down, so keeping it pooled only risks Get handing out a dead connection. The RSN's hot destinations are hit every few seconds (lastUsed refreshed on Put) so they never idle out; colder ones re-dial fresh, which is correct since their pooled stream would have died anyway.

View Source
const DialTimeout = 10 * time.Second

DialTimeout is the maximum time allowed for a single dial attempt to a remote visor. Without this, dials to unreachable visors block indefinitely in DialStream.readResponse, leaking goroutines. This caps the dial itself; the RPC deadline is set separately. Keep this short to free ephemeral ports quickly under load.

View Source
const MakeMapTimeout = 60 * time.Second

MakeMapTimeout is the hard deadline for MakeMap to dial all visors concurrently. This prevents indefinite blocking when multiple visors are unreachable, which was the primary cause of goroutine accumulation in the setup-node (113+ stuck goroutines observed in production pprof dumps).

View Source
const MaxDatagramPayload = math.MaxUint16

MaxDatagramPayload is the largest datagram body that fits in a single DatagramPacket on the wire. The packet header carries a uint16 payload-size field, capping the total payload (including any Stage 3 AEAD overhead) at 65535 bytes. Apps consult DatagramRouteGroup.MaxPayload() rather than this constant so the answer accounts for the AEAD tag + nonce overhead Stage 3 will reserve.

View Source
const MuxEventForwardDrops = "forward_drops"

MuxEventForwardDrops is recorded the first time a next-hop transport loses transit frames inside a forward.drop_event_window — a whole-transport event (LegIndex -1) naming the peer and the counters, so a wedged transit peer is readable from `visor state` long after the log ring has rolled.

View Source
const MuxEventRingSizeDefault = 256

MuxEventRingSize is how many mux events the router keeps.

View Source
const RPCName = "RPCGateway"

RPCName is the RPC gateway object name (the net/rpc / gobrpc service name the route-setup client dials as "RPCGateway.<Method>"). It lives in this untagged file so both the native client (routerclient.go, !tinygo) and the TinyGo HandleFunc registration (router_setup_rpc_tinygo.go) can reference it.

Variables

View Source
var (
	// ErrCounterExhausted is returned by Seal when the per-direction
	// counter has reached its rekey limit and no rekey has happened
	// yet. The caller should trigger a rekey and retry.
	ErrCounterExhausted = errors.New("datagram-crypto: counter exhausted, rekey required")

	// ErrAEADAuth is returned by Open when the auth tag does not
	// verify, the counter falls outside the replay window, or the
	// counter was already seen. Indistinguishable to the caller by
	// design — leaking which failure occurred would expose timing
	// signal to an attacker probing the window.
	ErrAEADAuth = errors.New("datagram-crypto: auth failure")

	// ErrCiphertextTooShort is returned when the input to Open is
	// shorter than the minimum (counter prefix + tag).
	ErrCiphertextTooShort = errors.New("datagram-crypto: ciphertext shorter than overhead")

	// ErrPlaintextTooLong is returned when Seal's input exceeds
	// MaxDatagramPlaintext.
	ErrPlaintextTooLong = errors.New("datagram-crypto: plaintext exceeds MaxDatagramPlaintext")
)

Errors surfaced by DatagramCipher operations.

View Source
var (
	// ErrNoTransports is returned when RouteGroup has no transports.
	ErrNoTransports = errors.New("no transports")
	// ErrNoRules is returned when RouteGroup has no rules.
	ErrNoRules = errors.New("no rules")
	// ErrBadTransport is returned when transport is nil.
	ErrBadTransport = errors.New("bad transport")
	// ErrRuleTransportMismatch is returned when number of forward rules does not equal to number of transports.
	ErrRuleTransportMismatch = errors.New("rule/transport mismatch")
	// ErrNoSuitableTransport is returned when no suitable transport was found.
	ErrNoSuitableTransport = errors.New("no suitable transport")
	// ErrNoRouteFound is return when no route founds after specific tries
	ErrNoRouteFound = errors.New("no route founds")
)
View Source
var (
	// ErrUnknownPacketType is returned when packet type is unknown.
	ErrUnknownPacketType = errors.New("unknown packet type")

	// ErrRemoteEmptyPK occurs when the specified remote public key is empty.
	ErrRemoteEmptyPK = errors.New("empty remote public key")

	// ErrDialPolicyDropped is returned by DialRoutes when the
	// configured DialHook's BeforeDial returns Fallback="drop".
	// Operators see this as the explicit signal that their
	// routing policy refused this dial; it's distinct from any
	// route-finder failure or transport error.
	ErrDialPolicyDropped = errors.New("dial refused by routing policy")

	// ErrNoTransportFound is returned when not even one transport is found.
	ErrNoTransportFound = errors.New("no transport found")
)
View Source
var (
	// ErrTransportQueryUntrustedRSN is returned when the query's RSN public key
	// is not on the responding visor's trusted-RSN allowlist. Mirrors
	// routing.ErrCascadeUntrustedRSN for the query path.
	ErrTransportQueryUntrustedRSN = errors.New("transport-query: RSN public key not trusted")
	// ErrTransportQuerySigInvalid is returned when the RSN signature does not
	// verify against the responding visor's own PK.
	ErrTransportQuerySigInvalid = errors.New("transport-query: RSN signature verification failed")
	// ErrTransportQueryWrongTarget is returned when the query's TargetPK is not
	// the responding visor's own PK (a query signed for a different destination
	// must not be answered by this one).
	ErrTransportQueryWrongTarget = errors.New("transport-query: target PK mismatch")
)
View Source
var ErrAdoptUnsupported = fmt.Errorf("%w: leg reserve adoption unavailable", ErrRehomeUnsupported)

ErrAdoptUnsupported is returned when a leg reserve cannot be adopted in place: there is no such reserve, it has no mux that negotiated CapLegRehome, or it holds other than exactly one chain.

View Source
var ErrCircuitOpen = fmt.Errorf("route setup: destination circuit breaker open")

ErrCircuitOpen is returned by CreateRouteGroup when the per- destination circuit breaker has short-circuited a setup attempt. It is distinct from a dial failure so callers and stats can tell the difference between "this destination is known bad, skipping" and "we tried and failed again".

View Source
var ErrClosed = errors.New("datagram-route-group: closed")

ErrClosed is returned by ReadFrom / WriteTo after Close.

View Source
var ErrNoDisjointFirstHop = errors.New("no disjoint first-hop route is free")

ErrNoDisjointFirstHop is returned to a dial that set DialOptions.RequireDisjointFirstHop when the ranked candidate list is exhausted: every route this visor can offer to the destination leaves over a first-hop transport / peer / IP that a sibling route group to the same destination already holds.

It is a SETTLED answer, not a transient failure — the topology is what it is — so the caller must treat it as "stop dialing", never as "retry". The skysocks standby pool stops filling on it and re-arms only when a tunnel dies or the transport set changes; a retry loop against it is exactly the setup-node storm of #4325.

View Source
var ErrNoKey = errors.New("id reservoir has no key")

ErrNoKey is returned when key is not found.

View Source
var ErrPayloadTooBig = errors.New("datagram-route-group: payload exceeds MaxPayload")

ErrPayloadTooBig is returned by WriteTo when the caller provides a datagram larger than MaxPayload. Mirrors net.UDPConn's behavior on oversized sends but is surfaced explicitly rather than as an opaque EMSGSIZE — apps that consulted MaxPayload() should never see this in practice.

View Source
var ErrPolicyAvoidDirect = errors.New("routing policy: avoid direct transport, use overlay")

ErrPolicyAvoidDirect is returned from the direct-dial policy hook when the policy wants to force the overlay path. The caller (tryDirectDial) treats this like any other direct-dial failure and falls through to route setup — no extra plumbing required.

View Source
var ErrRehomeNoAck = errors.New("re-home not acknowledged")

ErrRehomeNoAck is the exit not answering a re-home request in time or refusing it: both groups are left intact, so the caller may fall back to dialing the leg on the pool's plan instead of giving up on the take.

View Source
var ErrRehomeUnsupported = errors.New("leg re-home unsupported (peer did not negotiate CapLegRehome)")

ErrRehomeUnsupported is returned when a chain cannot be re-homed in place: the peer did not advertise CapLegRehome, or a group involved has no mux. The caller's fallback is to spend the pooled tunnel the DIAL-based way — a pool-sourced leg through the setup node, reusing the pool's route plan and warm transport (see poolSourcedLegFallback).

View Source
var ErrSetupNode = errors.New("failed to dial to a setup node")

ErrSetupNode is used when the visor is unable to connect to a setup node

View Source
var ErrSplitUnsupported = fmt.Errorf("%w: leg split unavailable", ErrRehomeUnsupported)

ErrSplitUnsupported is returned when a leg cannot be split back out in place: the peer did not negotiate CapLegRehome, the group has no mux, or the group is not registered with a router that can hold the new standby group. The caller's fallback is to close the leg's transport, which is what the release path did before splits existed.

Functions

func BroadcastIntermediaryRules

func BroadcastIntermediaryRules(ctx context.Context, log logrus.FieldLogger, rtIDR IDReserver, interRules RulesMap, installed *installedRules) (err error)

BroadcastIntermediaryRules broadcasts routing rules to the intermediary routers. On each hop's successful install it records the installed RouteIDs into `installed` (when non-nil) so a later setup failure can tear them down instead of orphaning them.

func CreateRouteGroup

func CreateRouteGroup(ctx context.Context, dialer network.Dialer, pool *ClientPool, cascade *CascadeBuilder, biRt routing.BidirectionalRoute, metrics setupmetrics.Metrics) (resp routing.EdgeRules, err error)

CreateRouteGroup creates a route group by communicating with routers used within the bidirectional route. The following steps are taken: * Check the validity of bi route input. * Consult the per-destination circuit breaker; fast-fail if open. * Route IDs are reserved from the routers. * Intermediary rules are broadcasted to the intermediary routers. * Edge rules are broadcasted to the responding router. * Edge rules is returned (to the initiating router).

func DeadRouteHold

func DeadRouteHold() time.Duration

DeadRouteHold is the first exclusion window a route that died young is kept out of the diversify search for.

func DeadRouteHoldMax

func DeadRouteHoldMax() time.Duration

DeadRouteHoldMax caps the doubling applied on each repeat death.

func DeadRouteYoungAge

func DeadRouteYoungAge() time.Duration

DeadRouteYoungAge is the "died right after dial" window: a group that lives longer than this and then closes is a normal teardown, not a dead route.

func DeliveryCRCAdvertised

func DeliveryCRCAdvertised() bool

DeliveryCRCAdvertised reports whether CapDeliveryCRC is advertised on new route groups. It gates OUR sender side: with the knob off we never stamp a frame, and because the peer only strips when we advertised too, the two ends can never disagree about whether the 4-byte trailer is present.

func DialDiversifyCandidates

func DialDiversifyCandidates() int

DialDiversifyCandidates is how many routes a DIVERSIFY dial asks the route finder for. A standby-pool fill is not looking for the best route, it is looking for one the tunnels it already holds do not use — and the finder answers rank-ordered, so the top few are exactly the ones they do.

func DialForegroundMux

func DialForegroundMux() int

DialForegroundMux bounds how many mux legs are established SYNCHRONOUSLY at dial time; the background self-heal fills the rest of the pool.

func DialMuxRouteHeadroom

func DialMuxRouteHeadroom() int

DialMuxRouteHeadroom is the extra routes requested on top of the mux degree, so the disjoint-path pick can still reach the target count when some returned candidates share intermediates — the sibling-diversify count.

func DialPreferPKs

func DialPreferPKs() []cipher.PubKey

DialPreferPKs returns the operator's prefer-these-peers list — the routes that should be taken FIRST. Nil when unset (the ranking is untouched).

func DialRouteCandidates

func DialRouteCandidates() int

DialRouteCandidates is the floor on how many routes a mux dial asks the route-finder for (the historical default route count).

func DialThroughputPriorScale

func DialThroughputPriorScale() float64

DialThroughputPriorScale multiplies the MEASURED-throughput band penalty in transportCostMs.

func DialTunnelLegs

func DialTunnelLegs() int

DialTunnelLegs is the leg count an APP TUNNEL is dialed with — the fix for a tunnel that asks for a single-route group (MuxRoutes=1) and then has its legs bolted on afterwards by `proxy mux add` / the background self-heal.

 0 (default) — dial exactly what the app asked for. Today's behavior.
-1           — follow the visor's mux width (preset.AdaptRevActive), so a
               tunnel is dialed with the legs the adaptive engine would have
               grown it to anyway.
 n > 1       — dial every tunnel with n legs.

Only a dial that asked for a single-route group is raised; a dial that named its own mux degree, a --direct dial and a datagram dial are all untouched.

This is the visor-wide value; dialTunnelLegsFor reads the same knob through an app's overrides, which is what `route settings --app <name> dial.tunnel_legs=N` sets.

func DialTypePriorScale

func DialTypePriorScale() float64

DialTypePriorScale multiplies the transport-TYPE prior (transportTypeCostMs), the penalty a hop carries until its link has a throughput measurement.

func DialUnknownHopPenaltyMs

func DialUnknownHopPenaltyMs() float64

DialUnknownHopPenaltyMs is what one unmeasured hop costs a partially measured path, in milliseconds.

func DialUnknownLatencyCostMs

func DialUnknownLatencyCostMs() float64

DialUnknownLatencyCostMs is the per-hop cost of a hop with no latency measurement, in milliseconds.

func EcfMaxWindowBytes

func EcfMaxWindowBytes() int64

EcfMaxWindowBytes is the ceiling a leg's send window is clamped to — the number the client-side chunk and upload sizes are sized against.

func EcfMinWindowBytes

func EcfMinWindowBytes() int64

EcfMinWindowBytes is the floor a leg's send window is clamped to.

func EcfWindowMargin

func EcfWindowMargin() float64

EcfWindowMargin is the multiplier on SACK-proven delivery-per-RTT that sets a leg's send window.

func FindDirectRSNTransport

func FindDirectRSNTransport(rsnPK cipher.PubKey, tm *transport.Manager) *transport.ManagedTransport

FindDirectRSNTransport checks if we have a direct "setup"-labeled transport to the RSN.

func ForwardSpill

func ForwardSpill() bool

ForwardSpill reports whether a FORWARD frame may leave its confined leg when that leg is at its send window.

func ForwardSwitchMargin

func ForwardSwitchMargin() float64

ForwardSwitchMargin is how much lower a challenger leg must measure before the forward direction moves to it.

func ForwardSwitchSamples

func ForwardSwitchSamples() int

ForwardSwitchSamples is how many consecutive refreshes a challenger must clear the margin for before the direction moves.

func HOLRetxAdvertised

func HOLRetxAdvertised() bool

HOLRetxAdvertised reports whether CapHOLRetx is advertised on new route groups. Implies SACK: the mechanism reuses the SACK feedback, so it is never advertised on its own.

func LegDelivAlpha

func LegDelivAlpha() float64

LegDelivAlpha is the weight the newest sample carries in a leg's SACK-proven delivery EWMA — the reading the goodput half of the outclassed-leg gate judges on.

func LegParkMinHold

func LegParkMinHold() time.Duration

LegParkMinHold is how long an adaptive park holds before any adaptive promoter may re-admit the leg.

func LegProbeBytes

func LegProbeBytes() int64

LegProbeBytes is how many bytes a leg ruled probe-only may carry per window (the window being its own delay basis, floored at leg.probe_min_window).

func LegStarveRatio

func LegStarveRatio() float64

LegStarveRatio is the one ratio the outclassed-leg ruling uses at both ends: a leg is cut to a probe per window only when its delay basis exceeds the best ready leg's by more than this AND its proven delivery rate is under 1/this of that leg's — so a leg that is slow but productive keeps its share. At or below 1 the gate is off.

func PerFrameNoiseEnabled

func PerFrameNoiseEnabled() bool

PerFrameNoiseEnabled reports whether CapPerFrameNoise is advertised on new route groups.

func PoolBoundRefresh added in v1.3.99

func PoolBoundRefresh() time.Duration

PoolBoundRefresh is how often a pooled exit's disjoint routes are re-counted; 0 leaves the count to the dials.

func ReorderSetupNodes

func ReorderSetupNodes(nodes []cipher.PubKey, successPK cipher.PubKey) []cipher.PubKey

ReorderSetupNodes moves the given public key to the front of the list. This should be called after a successful connection to prioritize working nodes.

func SACKAdvertised

func SACKAdvertised() bool

SACKAdvertised reports whether CapSACK is advertised on new route groups.

func SBDBackoff

func SBDBackoff() time.Duration

SBDBackoff is how long a pair whose park trial failed is exempt from shared-bottleneck merging.

func SBDDemote

func SBDDemote() bool

SBDDemote reports whether a shared-bottleneck ruling may PARK a leg at all.

func SBDEnabled

func SBDEnabled() bool

SBDEnabled reports whether shared-bottleneck detection runs at all. Off stops the sampling as well as the rulings — the explicit switch the rig used to fake with an unreachable sbd.min_samples.

func SBDMinEvidenceRate

func SBDMinEvidenceRate() int64

SBDMinEvidenceRate is the aggregate goodput a group must be carrying before a shared-bottleneck ruling may park one of its legs.

func SBDMinSamples

func SBDMinSamples() int

SBDMinSamples is how many delay samples a leg needs before its statistics are trusted for shared-bottleneck grouping.

func SBDSampleInterval

func SBDSampleInterval() time.Duration

SBDSampleInterval is the minimum spacing between two per-SACK delay samples folded into one leg's window.

func SBDTrialLoss

func SBDTrialLoss() float64

SBDTrialLoss is the fraction of aggregate goodput a park may cost before it is undone.

func SBDTrialWindow

func SBDTrialWindow() time.Duration

SBDTrialWindow is how long a shared-bottleneck park is held as a trial before the aggregate goodput is re-read.

func SendWindowWaitMax

func SendWindowWaitMax() time.Duration

SendWindowWaitMax bounds how long a writer parks when every ready leg is at its window before sending anyway.

func ServeTransportQueryListener

func ServeTransportQueryListener(ctx context.Context, dmsgC *dmsg.Client, gw *TransportQueryRPCGateway, log *logging.Logger) error

ServeTransportQueryListener listens on skyenv.DmsgTransportQueryPort and serves the gateway to any visor that dials it. Blocks until ctx is canceled or the listener fails. The caller starts it in a goroutine (see the visor init wiring). Only invoked when the RSN-oracle path is enabled — a default visor opens no listener here.

func SetDeadRouteHold

func SetDeadRouteHold(d time.Duration) bool

SetDeadRouteHold installs the first exclusion window. Non-positive is refused.

func SetDeadRouteHoldMax

func SetDeadRouteHoldMax(d time.Duration) bool

SetDeadRouteHoldMax installs the exclusion ceiling. Non-positive is refused.

func SetDeadRouteYoungAge

func SetDeadRouteYoungAge(d time.Duration) bool

SetDeadRouteYoungAge installs that window. Non-positive is refused.

func SetDialDiversifyCandidates

func SetDialDiversifyCandidates(n int) bool

SetDialDiversifyCandidates installs that count. Non-positive is refused.

func SetDialForegroundMux

func SetDialForegroundMux(n int) bool

SetDialForegroundMux installs that bound. Non-positive is refused.

func SetDialMuxRouteHeadroom

func SetDialMuxRouteHeadroom(n int) bool

SetDialMuxRouteHeadroom installs that headroom. Negative is refused; 0 means "ask for exactly the mux degree".

func SetDialPreferPKs

func SetDialPreferPKs(pks []cipher.PubKey)

SetDialPreferPKs installs the prefer list. An empty or nil slice clears it. A copy is kept so the caller may reuse its slice.

func SetDialRouteCandidates

func SetDialRouteCandidates(n int) bool

SetDialRouteCandidates installs that floor. Non-positive is refused.

func SetDialThroughputPriorScale

func SetDialThroughputPriorScale(v float64) bool

SetDialThroughputPriorScale installs that multiplier; 0 removes the term.

func SetDialTunnelLegs

func SetDialTunnelLegs(n int) bool

SetDialTunnelLegs installs that count. Values below -1 are refused.

func SetDialTypePriorScale

func SetDialTypePriorScale(v float64) bool

SetDialTypePriorScale installs that multiplier; 0 removes the term.

func SetDialUnknownHopPenaltyMs

func SetDialUnknownHopPenaltyMs(v float64) bool

SetDialUnknownHopPenaltyMs installs that penalty. Negative / NaN / Inf refused; 0 drops the penalty.

func SetDialUnknownLatencyCostMs

func SetDialUnknownLatencyCostMs(v float64) bool

SetDialUnknownLatencyCostMs installs that cost. Negative / NaN / Inf refused; 0 drops the penalty.

func SetEcfMaxWindowBytes

func SetEcfMaxWindowBytes(v int64) bool

SetEcfMaxWindowBytes installs the send-window ceiling. Non-positive is refused.

func SetEcfMinWindowBytes

func SetEcfMinWindowBytes(v int64) bool

SetEcfMinWindowBytes installs the send-window floor. Non-positive is refused.

func SetEcfWindowMargin

func SetEcfWindowMargin(v float64) bool

SetEcfWindowMargin installs the send-window margin. Non-positive is refused.

func SetForwardSpill

func SetForwardSpill(on bool) bool

SetForwardSpill turns forward spill on or off. Always accepted; the bool return keeps the shape of the other setters.

func SetForwardSwitchMargin

func SetForwardSwitchMargin(v float64) bool

SetForwardSwitchMargin installs that margin. Non-positive is refused.

func SetLegDelivAlpha

func SetLegDelivAlpha(v float64) bool

SetLegDelivAlpha installs that weight. Only a value in (0,1] is accepted.

func SetLegParkMinHold

func SetLegParkMinHold(d time.Duration) bool

SetLegParkMinHold installs the adaptive park hold. Non-positive is refused.

func SetLegProbeBytes

func SetLegProbeBytes(v int64) bool

SetLegProbeBytes installs that budget. Non-positive is refused: a budget of zero would silence the leg completely, and the point of the probe is that the leg keeps being measured so the ruling can lift.

func SetLegStarveRatio

func SetLegStarveRatio(v float64) bool

SetLegStarveRatio installs that ratio. Only a finite value above 1 is accepted — at or below 1 every leg would outclass every other, including itself; use a very large ratio to disable the gate instead.

func SetSBDBackoff

func SetSBDBackoff(d time.Duration) bool

SetSBDBackoff installs that exemption. Non-positive is refused.

func SetSBDDemote

func SetSBDDemote(on bool) bool

SetSBDDemote turns shared-bottleneck demotion on or off. Always accepted; the bool return keeps the shape of the other setters.

func SetSBDEnabled

func SetSBDEnabled(on bool) bool

SetSBDEnabled turns shared-bottleneck detection on or off. Always accepted.

func SetSBDMinEvidenceRate

func SetSBDMinEvidenceRate(v int64) bool

SetSBDMinEvidenceRate installs that floor. Non-positive is refused.

func SetSBDMinSamples

func SetSBDMinSamples(n int) bool

SetSBDMinSamples installs the shared-bottleneck sample floor. Non-positive is refused.

func SetSBDSampleInterval

func SetSBDSampleInterval(d time.Duration) bool

SetSBDSampleInterval installs that spacing. Non-positive is refused.

func SetSBDTrialLoss

func SetSBDTrialLoss(v float64) bool

SetSBDTrialLoss installs that fraction. Non-positive is refused.

func SetSBDTrialWindow

func SetSBDTrialWindow(d time.Duration) bool

SetSBDTrialWindow installs the trial window. Non-positive is refused.

func SetSendWindowWaitMax

func SetSendWindowWaitMax(d time.Duration) bool

SetSendWindowWaitMax installs the send-window wait bound. Non-positive is refused.

func SetSetupBatchMax

func SetSetupBatchMax(n int) bool

SetSetupBatchMax installs that cap. Non-positive is refused; values above routing.MaxBatchRoutes are clamped by the sender.

func SetSetupBatchWindow

func SetSetupBatchWindow(d time.Duration) bool

SetSetupBatchWindow installs that window. Non-positive is refused.

func SetSetupCircuitBreaker

func SetSetupCircuitBreaker(on bool) bool

SetSetupCircuitBreaker turns that lockout on or off.

func SetSetupCircuitFailThreshold

func SetSetupCircuitFailThreshold(n int) bool

SetSetupCircuitFailThreshold installs that threshold. Non-positive is refused.

func SetSetupCircuitFailWindow

func SetSetupCircuitFailWindow(d time.Duration) bool

SetSetupCircuitFailWindow installs that window. Non-positive is refused.

func SetSetupCircuitMaxOpenDuration

func SetSetupCircuitMaxOpenDuration(d time.Duration) bool

SetSetupCircuitMaxOpenDuration installs that ceiling. Non-positive is refused.

func SetSetupCircuitOpenDuration

func SetSetupCircuitOpenDuration(d time.Duration) bool

SetSetupCircuitOpenDuration installs that duration. Non-positive is refused.

func SetSetupFillInflight

func SetSetupFillInflight(n int) bool

SetSetupFillInflight installs that bound. Non-positive is refused.

func SetSetupFirstHopFilterMax

func SetSetupFirstHopFilterMax(n int) bool

SetSetupFirstHopFilterMax installs that threshold. Non-positive is refused.

func SetSetupPlanClaimTTL

func SetSetupPlanClaimTTL(d time.Duration) bool

SetSetupPlanClaimTTL installs that TTL. Non-positive is refused.

func SetWarmPlanBucketCap

func SetWarmPlanBucketCap(n int) bool

SetWarmPlanBucketCap installs that cap. Non-positive is refused.

func SetWarmPlanTTL

func SetWarmPlanTTL(d time.Duration) bool

SetWarmPlanTTL installs that TTL. Non-positive is refused.

func SetupBatchMax

func SetupBatchMax() int

SetupBatchMax is the most routes one batched setup request carries. 1 means "send singles" — the batch form is never used.

func SetupBatchWindow

func SetupBatchWindow() time.Duration

SetupBatchWindow is how long a route setup collects sibling dials to the same exit before sending them as one batched request.

func SetupCircuitBreaker

func SetupCircuitBreaker() bool

SetupCircuitBreaker reports whether the route setup node's per-destination circuit breaker may open. Off by default — see setupmetrics for why.

func SetupCircuitFailThreshold

func SetupCircuitFailThreshold() int

SetupCircuitFailThreshold is how many consecutive failures attributed to one destination trip its breaker.

func SetupCircuitFailWindow

func SetupCircuitFailWindow() time.Duration

SetupCircuitFailWindow is how close together failures must fall to count as consecutive.

func SetupCircuitMaxOpenDuration

func SetupCircuitMaxOpenDuration() time.Duration

SetupCircuitMaxOpenDuration is the total open/half-open time after which a breaker is force-closed.

func SetupCircuitOpenDuration

func SetupCircuitOpenDuration() time.Duration

SetupCircuitOpenDuration is how long a tripped breaker refuses setups before admitting a half-open probe.

func SetupFillInflight

func SetupFillInflight() int

SetupFillInflight is how many standby-pool tunnel dials run concurrently.

func SetupFirstHopFilterMax

func SetupFirstHopFilterMax() int

SetupFirstHopFilterMax is the held-first-hop count beyond which first-hop diversity is a ranking term rather than a filter — under pool.allow_duplicate_route only.

func SetupPlanClaimTTL

func SetupPlanClaimTTL() time.Duration

SetupPlanClaimTTL is how long a concurrent dial holds its claim on an oracle candidate path, so N dials in one fill take N distinct intermediates.

func WarmPlanBucketCap

func WarmPlanBucketCap() int

WarmPlanBucketCap bounds how many distinct disjoint plans the warm pool holds per exit.

func WarmPlanTTL

func WarmPlanTTL() time.Duration

WarmPlanTTL bounds staleness of a cached disjoint plan set in the warm route pool.

func WithForceLegacyRouteSetup

func WithForceLegacyRouteSetup(ctx context.Context) context.Context

WithForceLegacyRouteSetup returns a context that instructs a cascade-capable RouteGroupDialer to bypass the source-driven cascade and use the classic setup-node path for this dial.

Rationale: the source-driven cascade installs rules at the destination over transports, verified by the destination's CascadeHandler via the RSN signature. A destination that does NOT run cascade route-setup only trusts the classic setup nodes (route_setup_nodes) and rejects a cascade-installed route — the route group's reciprocal handshake then never completes and the dial fails. The classic path routes setup through an RSN the destination already trusts, so escalating to it recovers the dial against a mixed fleet.

func WrapDmsgClient

func WrapDmsgClient(dmsgC *dmsg.Client) network.Dialer

WrapDmsgClient wraps a dmsg client to implement snet.Dialer

Types

type BatchSavings

type BatchSavings struct {
	Routes    int
	Issued    int
	Coalesced int
}

BatchSavings is what one batch actually saved, for the setup node's /stats. Coalesced is the number of per-hop RPCs this batch's shape would have cost as N separate requests; Issued is what it cost as one. Both count the id-reservation and intermediary-install RPCs — the two the batch merges — and exclude the per-member destination edge install, which is not merged.

func CreateRouteGroupBatch

func CreateRouteGroupBatch(
	ctx context.Context,
	dialer network.Dialer,
	pool *ClientPool,
	batch routing.BidirectionalRouteBatch,
	metrics setupmetrics.Metrics,
) (results []routing.BatchRouteResult, savings BatchSavings)

CreateRouteGroupBatch installs every route in the batch, coalescing the per-hop work, and returns one result per member in request order.

It never returns a short slice for a non-empty batch: a failure that kills the whole batch (malformed batch, or a shared id reservation that could not be made) is reported as that error on every member, so the caller always learns the fate of every route it asked for.

func (BatchSavings) Saved

func (s BatchSavings) Saved() int

Saved is the per-hop RPC count the coalescing removed.

type CandidateInfo

type CandidateInfo struct {
	// Hops is the ordered intermediate PKs (hex) from source
	// (excluded) to destination (excluded). Empty for a direct
	// transport.
	Hops []string

	// EstLatencyMs is the sum of per-hop latencies along the
	// route, in milliseconds, drawn from the router's existing
	// transport-tracker lookup. Zero when no measurements are
	// available — treat as "unknown," not "fast."
	EstLatencyMs int
}

CandidateInfo is the bare-bones per-candidate description the router hands to RouteSelectingHook.SelectRoute. The hook is responsible for any further enrichment (geo lookup, transport- kind classification) — the router only owns the route-finder response and the latency lookup it already builds for its disjoint-path selection.

type CapabilitiesArgs

type CapabilitiesArgs struct{}

CapabilitiesArgs is the request for the Capabilities RPC.

type CapabilitiesReply

type CapabilitiesReply struct {
	Caps []string `json:"caps"`
}

CapabilitiesReply lists the request shapes this setup node understands.

type CascadeBuilder

type CascadeBuilder struct {
	// contains filtered or unexported fields
}

CascadeBuilder constructs and sends cascade messages.

It serves two roles depending on how it is constructed:

  • RSN-side (NewCascadeBuilder): holds the RSN's rsnPK/rsnSK and SIGNS each per-hop cascade layer. The RSN-side builder is a pure signing oracle — it builds bytes and never sends them itself.
  • Source-side (NewSourceCascadeBuilder): holds a zero rsnSK and never signs; it only INJECTS pre-signed cascade bytes (received from the RSN over RPC) into the source's own transports and collects ACKs.

func NewCascadeBuilder

func NewCascadeBuilder(
	log *logging.Logger,
	rsnPK cipher.PubKey,
	rsnSK cipher.SecKey,
	tm *transport.Manager,
) *CascadeBuilder

NewCascadeBuilder creates a signing builder for the RSN.

func NewSourceCascadeBuilder

func NewSourceCascadeBuilder(
	log *logging.Logger,
	tm *transport.Manager,
	acks *ackRegistry,
) *CascadeBuilder

NewSourceCascadeBuilder creates a send-only builder for the SOURCE visor.

It carries no RSN secret key (it never signs) and shares the ack registry with the visor's CascadeHandler so that ACKs arriving on the visor's single registered cascade handler are routed back to in-flight sends. The source-side builder injects pre-signed cascade bytes — produced by the RSN via the CascadeSign* RPCs — into the source's own transports.

func (*CascadeBuilder) AckRegistry

func (cb *CascadeBuilder) AckRegistry() *ackRegistry

AckRegistry returns the builder's ack registry so a CascadeHandler on the same visor can share it (the source visor has one registered cascade handler that must deliver ACKs to this builder's in-flight sends).

func (*CascadeBuilder) BuildInstallMessage

func (cb *CascadeBuilder) BuildInstallMessage(
	hops []routing.Hop,
	sessionID uint64,
	rulesPerHop map[cipher.PubKey][]routing.Rule,
	edgeDesc routing.RouteDescriptor,
) ([]byte, error)

BuildInstallMessage constructs a nested install cascade from last hop to first. rulesPerHop maps each visor PK to the rules that should be installed on it. edgeDesc, when non-zero, is stamped onto the TERMINAL hop's layer (the responding destination) so that hop calls router.IntroduceRules — creating a route group — instead of only installing forwarding rules. Pass the zero RouteDescriptor for a path whose terminal must not introduce a route group.

func (*CascadeBuilder) BuildReserveMessage

func (cb *CascadeBuilder) BuildReserveMessage(hops []routing.Hop, reserveN uint8) (uint64, []byte, error)

BuildReserveMessage constructs a nested reserve cascade from last hop to first. hops is the route's hop list: [{From:A, To:C, TpID:tp1}, {From:C, To:D, TpID:tp2}, ...]. The target visors are: hops[0].From (source), hops[i].From (intermediaries), hops[last].To (destination).

func (*CascadeBuilder) HandleAck

HandleAck is called by the transport manager's cascade handler when an ACK arrives on one of the RSN's transports. Dispatches to the waiting sender.

func (*CascadeBuilder) SendCascade

func (cb *CascadeBuilder) SendCascade(ctx context.Context, firstHopTpID uuid.UUID, sessionID uint64, payload []byte, timeout time.Duration) (*routing.CascadeAck, error)

SendCascade sends a cascade payload to the first hop and waits for the ACK. firstHopTpID is the transport connecting the RSN to the first hop.

func (*CascadeBuilder) SetTimeouts

func (cb *CascadeBuilder) SetTimeouts(reserve, install time.Duration)

SetTimeouts configures per-phase timeouts.

type CascadeConfig

type CascadeConfig struct {
	// ReserveTimeout is the per-hop timeout for the reserve phase.
	ReserveTimeout time.Duration `json:"reserve_timeout,omitempty"`
	// InstallTimeout is the per-hop timeout for the install phase.
	InstallTimeout time.Duration `json:"install_timeout,omitempty"`
	// SessionTTL is how long to keep session state between phases.
	SessionTTL time.Duration `json:"session_ttl,omitempty"`
}

CascadeConfig configures the cascade route setup protocol.

func (*CascadeConfig) SetCascadeDefaults

func (cc *CascadeConfig) SetCascadeDefaults()

SetCascadeDefaults fills in zero cascade config values.

type CascadeHandler

type CascadeHandler struct {
	// contains filtered or unexported fields
}

CascadeHandler processes cascade protocol messages arriving on any transport.

func NewCascadeHandler

func NewCascadeHandler(
	log *logging.Logger,
	localPK cipher.PubKey,
	trustedRSNPKs []cipher.PubKey,
	rt routing.Table,
	tm *transport.Manager,
	introduceRules func(routing.EdgeRules) error,
) *CascadeHandler

NewCascadeHandler creates a new cascade handler. introduceRules is the router's IntroduceRules (creates a route group + notifies the launcher) and may be nil where no router is available (RSN, tests) — edge installs then fall back to plain rule saving.

func (*CascadeHandler) AckRegistry

func (ch *CascadeHandler) AckRegistry() *ackRegistry

AckRegistry returns the handler's shared ack registry. The source-side CascadeBuilder is constructed against this registry so ACKs arriving on the visor's single registered cascade handler reach the builder's sends.

func (*CascadeHandler) HandlePacket

func (ch *CascadeHandler) HandlePacket(p routing.Packet, sourceTp *transport.ManagedTransport)

HandlePacket is the callback registered with the transport manager. It dispatches cascade packets to the appropriate handler.

func (*CascadeHandler) ProcessLocalOrigin

func (ch *CascadeHandler) ProcessLocalOrigin(payload []byte) (*routing.CascadeAck, error)

ProcessLocalOrigin processes a cascade layer addressed to THIS visor as the route's origin (source). Unlike handleSetup, the origin has no upstream transport to ACK back on: it verifies the RSN signature against our own PK, performs the reserve/install locally, relays the inner payload to the first hop over our own transport, and RETURNS the collected ACK to the caller (runSourceCascade) instead of writing it to a sourceTp.

This is the crux of source-driven cascade: the RSN builds the nested cascade with the source as the OUTERMOST layer (signed for the source's PK). The source must consume that layer itself — shipping it verbatim to the first hop made the hop reject it ("RSN signature verification failed"), since it was signed for the source's PK, not the hop's.

func (*CascadeHandler) SetNoTransit

func (ch *CascadeHandler) SetNoTransit(v bool)

SetNoTransit makes the handler refuse cascade installs that would make this visor an intermediate hop on another visor's route.

type CascadeSignInstallArgs

type CascadeSignInstallArgs struct {
	Route        routing.BidirectionalRoute
	FwdSessionID uint64
	RevSessionID uint64
	FwdRouteIDs  []routing.RouteID
	RevRouteIDs  []routing.RouteID
}

CascadeSignInstallArgs is the request for CascadeSignInstall. The source supplies the route, the session IDs from the reserve phase, and the route IDs it collected per hop. The RSN recomputes rules deterministically from the route + IDs; it does NOT trust source-supplied rules.

type CascadeSignInstallReply

type CascadeSignInstallReply struct {
	FwdInstallBytes []byte
	RevInstallBytes []byte
	InitEdge        routing.EdgeRules
}

CascadeSignInstallReply carries the RSN-signed install cascades plus the initiating-edge rules the source installs locally and returns to the dialer.

type CascadeSignReserveArgs

type CascadeSignReserveArgs struct {
	Route routing.BidirectionalRoute
}

CascadeSignReserveArgs is the request for CascadeSignReserve.

type CascadeSignReserveReply

type CascadeSignReserveReply struct {
	FwdSessionID    uint64
	FwdReserveBytes []byte
	RevSessionID    uint64
	RevReserveBytes []byte
}

CascadeSignReserveReply carries the RSN-signed reserve cascades. The RSN signs but does NOT send — the SOURCE injects these bytes into its own transports (Forward[0].TpID / Reverse[0].TpID) and collects the reserved route-ID ACKs.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is used to interact with the router's API remotely. The setup node uses this.

func NewClient

func NewClient(ctx context.Context, dialer network.Dialer, rPK cipher.PubKey) (*Client, error)

NewClient creates a new Client.

func NewClientFromRaw

func NewClientFromRaw(conn io.ReadWriteCloser, rPK cipher.PubKey) *Client

NewClientFromRaw creates a new client from a raw connection.

func (*Client) AddEdgeRules

func (c *Client) AddEdgeRules(ctx context.Context, rules routing.EdgeRules) (ok bool, err error)

AddEdgeRules adds forward and consume rules to router (forward and reverse).

func (*Client) AddIntermediaryRules

func (c *Client) AddIntermediaryRules(ctx context.Context, rules []routing.Rule) (ok bool, err error)

AddIntermediaryRules adds intermediary rules to router.

func (*Client) Close

func (c *Client) Close() error

Close closes a Client. Safe to call multiple times.

func (*Client) DelRules

func (c *Client) DelRules(ctx context.Context, routeIDs []routing.RouteID) (ok bool, err error)

DelRules removes the routing rules identified by routeIDs from the remote router. Used by CreateRouteGroup's partial-failure cleanup to tear down the rules it installed on this hop before returning an error, instead of orphaning them for the ~10-min keepalive GC.

func (*Client) ReserveIDs

func (c *Client) ReserveIDs(ctx context.Context, n uint8) (rtIDs []routing.RouteID, err error)

ReserveIDs reserves n IDs and returns them.

type ClientPool

type ClientPool struct {
	// contains filtered or unexported fields
}

ClientPool is a thread-safe pool of reusable router RPC clients.

func NewClientPool

func NewClientPool(dialer network.Dialer, ttl time.Duration) *ClientPool

NewClientPool creates a pool that reuses connections to remote visors.

func (*ClientPool) Close

func (p *ClientPool) Close()

Close closes all pooled connections and stops the eviction loop.

func (*ClientPool) Discard

func (p *ClientPool) Discard(client *Client)

Discard closes a client without returning it to the pool. Use this when the connection is known to be broken (RPC error, context cancel).

func (*ClientPool) Get

func (p *ClientPool) Get(ctx context.Context, pk cipher.PubKey) (*Client, error)

Get returns an existing pooled client or dials a new one. The returned client is removed from the pool — the caller owns it and must call Put when done (or Close if the connection is broken).

func (*ClientPool) Put

func (p *ClientPool) Put(client *Client)

Put returns a client to the pool for reuse. If the pool already has a connection to this PK (race between concurrent requests to the same destination), the older one is closed.

func (*ClientPool) Size

func (p *ClientPool) Size() int

Size returns the number of pooled connections.

type Config

type Config struct {
	Logger           *logging.Logger
	MasterLogger     *logging.MasterLogger
	PubKey           cipher.PubKey
	SecKey           cipher.SecKey
	TransportManager *transport.Manager
	RouteFinder      rfclient.Client
	// PreferLocalRouteTo, when set and true for dst, tries the local route
	// calculation (this visor's transports plus the transport-discovery
	// snapshot, which includes any local graph source) before the route
	// finder is asked. A hypervisor sets it for its attached visors.
	PreferLocalRouteTo func(dst cipher.PubKey) bool
	RouteGroupDialer   RouteGroupDialer
	SetupNodes         []cipher.PubKey
	RulesGCInterval    time.Duration
	MinHops            uint16
	MaxHops            uint16
	// ExcludeTransportTypes hard-excludes routes traversing any of these transport
	// types from the candidate set (see visorconfig.Routing.RouteExcludeTransportTypes).
	// Empty = nothing excluded. Lowercased type names.
	ExcludeTransportTypes []string
	// NoTransit refuses every request to act as an intermediate hop on another
	// visor's route (visorconfig.Routing.NoTransit). Routes that originate or
	// terminate here are unaffected.
	NoTransit bool
	// DisableRaceRouteSetup turns OFF the app-dial race (default: race ON).
	// Normally, when a dial would create a direct transport to the peer
	// (min-hops==1, not existing-tp-only), DialRoutes runs that transport
	// creation in the BACKGROUND and concurrently tries to route over EXISTING
	// transports — returning as soon as either lands, instead of blocking on the
	// (~20s worst-case) transport dial first. Set true to restore the legacy
	// synchronous behavior (create-transport-then-route) as a safety valve.
	DisableRaceRouteSetup bool
	// ParallelRouteSetup is the number of candidate route groups whose
	// reciprocal-handshake setup DialRoutes races CONCURRENTLY per dial
	// attempt (top-K candidates the route-finder returned, ranked by
	// latency/suspect-liveness). The FIRST candidate to complete its
	// handshake WINS and immediately becomes the working route — the dial
	// returns on it without waiting for a "better" one; the losers are
	// canceled and torn down. This collapses establishment from "up to
	// N×handshakeAwaitTimeout" (the old one-candidate-at-a-time retry loop,
	// where every dead / non-forwarding candidate burned the full ~10s
	// handshake await before the next was tried — up to the ~90s dial ceiling,
	// leaving the app wedged in "starting") down to roughly one RTT. A dead-hop
	// candidate simply loses the race instead of blocking the whole dial, so
	// the app converges to a working route (single multihop at min_hops>=2,
	// or direct at min_hops==1) instead of wedging.
	//
	// 1 = strictly-sequential behavior (a clean rollback switch). 0 (unset)
	// is normalized to defaultParallelRouteSetup by SetDefaults; values above
	// maxParallelRouteSetup are clamped. Losing / failed candidates' intermediate
	// hops are marked suspect for a short TTL so the NEXT dial front-loads
	// known-good hops (see suspectHopCache; composes with #4063).
	ParallelRouteSetup int
	// AwaitSetupListener, if non-nil, is used instead of opening a fresh
	// listener on DmsgAwaitSetupPort. This lets callers reserve the port
	// earlier in init (before the transport manager is ready) so peers
	// dialing it during the router-init window don't hit "request has no
	// associated listener" warnings.
	AwaitSetupListener *dmsg.Listener
	// AppLookup, if set, resolves a routing.Port to the originating
	// app name. Used to tag rg-scoped log entries with app_name=<n>
	// so 'cli proxy start --verbose' can scope to a session. nil
	// means no lookup is performed and entries are emitted untagged
	// (the existing behavior).
	AppLookup func(routing.Port) (string, bool)

	// DialHook, when non-nil, is invoked before each DialRoutes
	// to let an operator-supplied routing policy adjust the
	// per-dial knobs (MuxRoutes, MinHops) or refuse the dial
	// entirely. See pkg/router/dial_hook.go for the contract and
	// pkg/router/policy/ for the Starlark-backed implementation
	// the visor wires up when conf.Routing.PolicyPerDial is set.
	//
	// When nil (the default) DialRoutes behaves identically to
	// its pre-integration shape — zero-cost when no policy is
	// configured.
	DialHook DialHook

	// PolicyOnControlPorts opts the routing policy (DialHook) BACK IN for
	// control-plane destination ports (pty, dmsgctrl, setup, hypervisor RPC,
	// …). Default false: those small noise-XK control channels bypass the
	// policy entirely and take a plain base route, because its per-dial
	// evaluation + warm-standby/reshape churn destabilizes their handshakes
	// (the pty 32-leg-mux footgun). Set true to let the operator's policy
	// manage control-plane routes too (advanced; e.g. a policy purpose-built
	// for them). See effectiveDialHook / controlPlanePorts.
	PolicyOnControlPorts bool

	// MuxFEC advertises CapFEC on mux route groups so repair frames are
	// striped alongside data (off by default; see visorconfig Routing.MuxFEC).
	MuxFEC bool

	// ExcludeSameLANHops, when true (the default the visor wires from
	// routing.exclude_same_lan_hops), makes route calculation drop candidate
	// INTERMEDIATE hops that sit on this visor's own local network — a peer
	// reached over a private/RFC1918 endpoint, or (the NAT-hairpin case) a peer
	// reached at this visor's own public IP or its /24. Routing a remote-
	// destination path through a same-LAN peer adds a hop but no path diversity
	// (same first-mile link, NAT and failure domain) and can fold the path back
	// onto the source network; for a mux it also starves genuinely-diverse aux
	// legs because the same-LAN peer wins on latency and is re-picked every
	// rotation. Same-LAN peers remain fully usable as direct dial DESTINATIONS —
	// only their use as intermediates is suppressed. The exclusion is computed
	// from live transports via TransportManager.SameLANPeers(SelfPublicIP()).
	ExcludeSameLANHops bool
	// SelfPublicIP returns this visor's current public IP (from STUN), or nil
	// when not yet known. Used only by the ExcludeSameLANHops filter to catch
	// the NAT-hairpin case (a same-LAN peer reached at our own public IP). nil
	// getter or nil result is fine — the private-IP and local-interface /24
	// checks still apply.
	SelfPublicIP func() net.IP
}

Config configures Router.

func (*Config) SetDefaults

func (c *Config) SetDefaults()

SetDefaults sets default values for certain empty values.

type DatagramCipher

type DatagramCipher struct {
	// contains filtered or unexported fields
}

DatagramCipher is the per-direction AEAD state. Two instances per DatagramRouteGroup: one for sealing outbound, one for opening inbound. They share the same Noise-derived master (Wire-level rekey moves both ciphers atomically) but each tracks its own counter (outbound) or replay window (inbound).

Single-direction; thread-safe.

func NewDatagramCipher

func NewDatagramCipher(masterKey [32]byte, role Role, bindPK cipher.PubKey, cfg *DatagramCipherConfig) (*DatagramCipher, error)

NewDatagramCipher constructs an outbound-or-inbound cipher from a 32-byte Noise master key, the route's role, the binding PK (the destination PK on outbound; the sender's PK on inbound — both go into AAD identically). Optional config overrides time/packet rekey limits; zero values fall back to defaults.

func (*DatagramCipher) MaxPlaintext

func (dc *DatagramCipher) MaxPlaintext() int

MaxPlaintext returns the largest plaintext datagram body that Seal will accept. Surfaced via DatagramRouteGroup.MaxPayload to callers.

func (*DatagramCipher) NeedsRekey

func (dc *DatagramCipher) NeedsRekey() bool

NeedsRekey reports whether the current epoch has exceeded the time-based or packet-count-based rekey trigger. The outer scheduling layer polls this and initiates a Noise rekey when it fires. Idempotent — calling does not change state.

func (*DatagramCipher) Open

func (dc *DatagramCipher) Open(routeID uint32, sealed []byte) ([]byte, error)

Open verifies and decrypts a sealed datagram body. Input format matches Seal's output. Returns ErrAEADAuth on any of: tag mismatch, counter outside the replay window, counter already observed. Indistinguishable failure modes are intentional — leaking which failed would expose timing signal to an attacker.

On success the counter is recorded in the replay window so the same datagram cannot be replayed within the window.

func (*DatagramCipher) Rekey

func (dc *DatagramCipher) Rekey(masterKey [32]byte, cfg *DatagramCipherConfig) error

Rekey replaces the AEAD epoch with one derived from a fresh master key. The counter resets to 0; the replay window clears. Both sides must perform the swap in lockstep (driven by the outer route-setup machinery, not by this type).

func (*DatagramCipher) Seal

func (dc *DatagramCipher) Seal(routeID uint32, plaintext []byte) ([]byte, error)

Seal AEAD-encrypts plaintext and prepends the 8-byte counter. Output: [counter (8)] [ciphertext + 16-byte tag]. AAD is constructed from routeID + dc.destPK. Returns ErrCounterExhausted if the counter has reached the rekey limit — the caller should trigger Rekey and retry.

type DatagramCipherConfig

type DatagramCipherConfig struct {
	TimeLimit   time.Duration
	PacketLimit uint64
}

DatagramCipherConfig tunes the rekey thresholds. Zero values use the package defaults (2 min / 2^32 packets).

type DatagramRouteGroup

type DatagramRouteGroup struct {
	// contains filtered or unexported fields
}

DatagramRouteGroup carries UDP-shaped datagrams across a route (or set of routes — multiplexing across multiple paths is a future enhancement, not present in Stage 2). Single-peer: WriteTo only accepts the configured remote address; ReadFrom always returns the same remote.

Lifecycle:

  • Constructed via NewDatagramRouteGroup with rules + transports (the same setup-node plumbing that builds RouteGroups can populate either type — Stage 4 wires this).
  • Handle(packet) is called by the router whenever a DatagramPacket arrives whose RouteID matches one of this group's rules. The packet's payload is queued on readCh and delivered to the next ReadFrom caller.
  • WriteTo wraps the payload in a DatagramPacket (Stage 3 will also AEAD-seal it), picks a transport, and writes it.
  • Close releases the readCh and marks the group dead. Idempotent.

func NewDatagramRouteGroup

func NewDatagramRouteGroup(cfg *RouteGroupConfig, rt routing.Table, desc routing.RouteDescriptor, mLogger *logging.MasterLogger) *DatagramRouteGroup

NewDatagramRouteGroup constructs a DatagramRouteGroup. The returned group has no rules or transports yet — those are added by the caller (the setup-node-side plumbing, Stage 4) before the group is registered with the router for dispatch.

Use cfg=nil for defaults.

func (*DatagramRouteGroup) AEADAuthFailures

func (dg *DatagramRouteGroup) AEADAuthFailures() uint64

AEADAuthFailures returns the count of inbound datagrams dropped due to AEAD authentication failure since group construction. Includes both forged datagrams and replay attempts; intentionally not differentiated to avoid leaking timing signal to an attacker. A rising counter under normal operation indicates either a forgery attempt in progress or a peer-side cipher desync (often a missed rekey).

func (*DatagramRouteGroup) AddRule

AddRule registers a (forward rule, transport, reverse rule) triple. Called by the setup-node integration in Stage 4. Index invariant: tps[i] is the transport for fwd[i]; rvs[i] is the matching consume rule on the receive side.

In Stage 2 this is the only way to populate the group; the router- side automatic plumbing comes later.

func (*DatagramRouteGroup) Close

func (dg *DatagramRouteGroup) Close() error

Close releases the readCh and marks the group dead. Idempotent. In-flight ReadFrom unblocks with ErrClosed; subsequent WriteTo also returns ErrClosed.

func (*DatagramRouteGroup) Handle

func (dg *DatagramRouteGroup) Handle(packet routing.Packet) error

Handle is called by the router (Stage 4) when a DatagramPacket arrives whose RouteID matches one of dg.rvs. The payload is queued on readCh and delivered to the next ReadFrom. If readCh is full, the datagram is dropped (atomic inboundDropped++) — faithful UDP semantics: a slow consumer loses packets.

Returns nil on successful queue, io.ErrClosedPipe if the group is closed (router should de-register the route in that case).

func (*DatagramRouteGroup) InboundDropped

func (dg *DatagramRouteGroup) InboundDropped() uint64

InboundDropped returns the count of inbound datagrams dropped due to a full readCh since group construction. Surfaced for diagnostics; not used in the hot path.

func (*DatagramRouteGroup) IsAlive

func (dg *DatagramRouteGroup) IsAlive() bool

IsAlive returns true if the group is open and recent writes have succeeded. Mirror of RouteGroup.IsAlive's shape so the Group interface stays narrow.

func (*DatagramRouteGroup) LocalAddr

func (dg *DatagramRouteGroup) LocalAddr() net.Addr

LocalAddr returns destination address of underlying RouteDescriptor. Mirrors RouteGroup's convention — the descriptor is built from the dialer's perspective so .Dst is "this side."

func (*DatagramRouteGroup) MaxPayload

func (dg *DatagramRouteGroup) MaxPayload() int

MaxPayload returns the largest plaintext datagram that WriteTo will accept. When AEAD is enabled (Stage 3 cipher installed via SetCiphers) the cap is MaxDatagramPayload minus the 24-byte per-datagram overhead (8-byte counter prefix + 16-byte Poly1305 tag) = 65511. When the cipher is unset the bypass mode returns the raw wire cap.

Apps consult this before sending to avoid the opaque-EMSGSIZE footgun.

func (*DatagramRouteGroup) ReadFrom

func (dg *DatagramRouteGroup) ReadFrom(p []byte) (int, net.Addr, error)

ReadFrom delivers the next inbound datagram payload. Blocks until a payload is available, the read deadline fires, or Close runs. The returned addr is always the group's remote (single-peer model). Buffer too small for the datagram is a partial-read situation: copy what fits, drop the rest, return the truncated length and no error — exactly net.UDPConn's behavior on undersized buffers (subject to ENOBUFS-equivalent surfacing at the Stage 5 app-API layer).

func (*DatagramRouteGroup) RemoteAddr

func (dg *DatagramRouteGroup) RemoteAddr() net.Addr

RemoteAddr returns source address of underlying RouteDescriptor.

func (*DatagramRouteGroup) SetCiphers

func (dg *DatagramRouteGroup) SetCiphers(out, in *DatagramCipher)

SetCiphers installs the per-direction AEAD ciphers. Called by the setup-node integration in Stage 4 after the Noise handshake produces the master key.

Nil ciphers leave the AEAD layer bypassed (raw plaintext on the wire) — only used by Stage 2 unit tests; the production path MUST install ciphers before any traffic flows.

func (*DatagramRouteGroup) SetDeadline

func (dg *DatagramRouteGroup) SetDeadline(t time.Time) error

SetDeadline sets both read and write deadlines.

func (*DatagramRouteGroup) SetReadDeadline

func (dg *DatagramRouteGroup) SetReadDeadline(t time.Time) error

SetReadDeadline sets the read deadline.

func (*DatagramRouteGroup) SetWriteDeadline

func (dg *DatagramRouteGroup) SetWriteDeadline(t time.Time) error

SetWriteDeadline sets the write deadline.

func (*DatagramRouteGroup) WriteTo

func (dg *DatagramRouteGroup) WriteTo(data []byte, addr net.Addr) (int, error)

WriteTo wraps data in a DatagramPacket and sends it over the first available transport. addr is checked against the group's remote (single-peer model); WriteTo on a wrong addr returns an error rather than silently routing elsewhere — apps that use a DatagramRouteGroup as a single-peer net.PacketConn are unlikely to hit this, but app frameworks that pool PacketConns might.

type DialAdjustment

type DialAdjustment struct {
	// MuxRoutes, when > 0, overrides DialOptions.MuxRoutes for
	// this dial.
	MuxRoutes int

	// ForwardMuxRoutes and ReverseMuxRoutes are per-direction
	// overrides for asymmetric mux. Zero falls back to MuxRoutes.
	// Used by policies that want different upstream / downstream
	// fan-out (e.g. ForwardMuxRoutes=1 + ReverseMuxRoutes=4 for
	// a download-heavy workload).
	ForwardMuxRoutes int
	ReverseMuxRoutes int

	// MinHops, when > 0, overrides DialOptions.MinHops for this
	// dial.
	MinHops int

	// ForwardMinHops and ReverseMinHops are per-direction
	// overrides for MinHops. Zero falls back to MinHops.
	ForwardMinHops int
	ReverseMinHops int

	// Fallback drives the no-overlay paths:
	//   "drop"   — refuse the dial with ErrDialPolicyDropped.
	//   "direct" — skip the route-finder, force the direct-
	//              transport short-circuit (UseExistingTpOnly=true
	//              + MinHops=1). If no direct transport exists,
	//              DialRoutes still fails — the script gets the
	//              all-or-nothing semantic it asked for.
	// Any other value is ignored (the dial proceeds normally).
	Fallback string

	// Distribution, when Mode != DistributionUnset, overrides
	// the route group's mux distribution strategy (which by
	// default tracks the router's visor-wide muxMode). The
	// policy layer parses the operator's descriptor string
	// (Starlark RouteSpec.Distribution) into this struct so
	// the router stays free of parser logic.
	Distribution DistributionConfig

	// RotationIntervalSeconds, when > 0, configures the route
	// group's rotation servicePacketLoop to fire OnTick every N
	// seconds. The hook gets the current leg snapshot and
	// returns a RotationAction (drop legs / add leg / no-op),
	// which the route group applies. Zero (default) disables
	// rotation. Set by policies that want continuous bandwidth-
	// spreading rotation across the eligible-peer set.
	RotationIntervalSeconds int

	// AvoidDirect, when true, makes the direct-dial fast path
	// (VStreamMux / sky_forward_conn) skip the direct transport
	// even when one exists — the dial falls through to the
	// overlay route-setup path, which is what makes mux /
	// min_hops / rotation policies meaningful.
	//
	// Set by the policy layer implicitly when the script returned
	// MinHops / ForwardMinHops / ReverseMinHops >= 2 — `min_hops=2`
	// already says "no direct" (direct is 0 intermediates), and
	// the alternative would be a separate avoid_direct knob that
	// duplicates the same intent. The direct-dial bridge in
	// wireDirectDialPolicyHook surfaces ErrPolicyAvoidDirect when
	// this is true; the caller (skywire_networker.tryDirectDial)
	// treats that as a normal direct-dial failure and falls
	// through to route setup.
	AvoidDirect bool
}

DialAdjustment is the hook's return value. Each field is a "override this if non-zero" hint; zero values mean "no change for this knob." Fallback can be set to "drop" to refuse the dial entirely.

type DialError

type DialError struct {
	PK  cipher.PubKey
	Err error
}

DialError identifies which remote PK failed to dial during route setup. The setup-node metrics layer inspects this to avoid tripping the circuit breaker on the destination when the failure was actually on the source side: id_reservation calls MakeMap which dials every hop (source, dest, intermediaries), and a flaky source visor would otherwise poison the destination's breaker, blocking all requests to a healthy destination.

func (*DialError) DialFailedPK

func (e *DialError) DialFailedPK() cipher.PubKey

DialFailedPK reports the remote PK that could not be dialed. The setupmetrics collector probes for this via an anonymous interface to stay free of a router import cycle.

func (*DialError) Error

func (e *DialError) Error() string

Error returns the formatted dial error, preserving the legacy message format ("dial <pk>@<port>: <inner>") so anything that still greps logs keeps working.

func (*DialError) Unwrap

func (e *DialError) Unwrap() error

Unwrap exposes the inner error for errors.Is / errors.As.

type DialHook

type DialHook interface {
	// BeforeDial is invoked synchronously before route setup. Its
	// return value adjusts the dial's opts in-place. Returning the
	// zero DialAdjustment + nil error means "no override; proceed
	// with the caller's opts unchanged."
	//
	// Implementations MUST return quickly — DialRoutes blocks on
	// this call before the route-finder query begins. Budget is
	// the same as the policy's per-call timeout (default 50ms).
	BeforeDial(ctx context.Context, info DialInfo) (DialAdjustment, error)
}

DialHook is called by DialRoutes before route setup. Returns adjustments that the router applies to the dial's opts.

Implementations live in pkg/router/policy and elsewhere. The interface here keeps pkg/router free of the Starlark machinery.

type DialInfo

type DialInfo struct {
	AppName string
	PeerPK  cipher.PubKey
	LPort   routing.Port
	RPort   routing.Port

	// CLIOverrides surfaces any DialOptions values the caller
	// (CLI / app) populated before BeforeDial fires, so the
	// script can honor or override them. Keys mirror DialOptions
	// field names in snake_case (mux_routes, min_hops,
	// forward_mux_routes, etc.). Only non-zero values appear.
	// The policy's adjustment runs AFTER this is captured, so
	// the script gets a true "what did the operator pass" view.
	CLIOverrides map[string]string

	// IsDirectDial is true when the hook is being invoked from
	// the sky_forward_conn / VStreamMux direct-dial path (the
	// short-circuit that uses an existing direct transport
	// instead of going through the route-finder). False for the
	// overlay paths (DialRoutes, PingRoute). Lets scripts apply
	// killswitch-style rules to direct dials too (e.g.
	// "vpn-client must never dial direct over dmsg"). Surfaced
	// as `ctx.is_direct_dial` in Starlark.
	IsDirectDial bool

	// TransportKind is the network type ("stcpr"/"sudph"/"stcp"/
	// "dmsg") of the transport the direct dial would use.
	// Populated only when IsDirectDial is true. Empty for
	// overlay dials. Surfaced as `ctx.transport_kind` so a
	// script can branch on the kind even before the overlay's
	// candidates are available.
	TransportKind string
}

DialInfo summarizes the dial for the hook. It's a flat struct rather than passing DialOptions so the hook stays decoupled from DialOptions' field churn.

type DialOptions

type DialOptions struct {
	MinForwardRts         int
	MaxForwardRts         int
	MinConsumeRts         int
	MaxConsumeRts         int
	Retries               int
	UseExistingTpOnly     bool          // If true, only use routes through existing transports, don't create new ones
	EnsureDirectTransport bool          // If true, create a direct transport to the destination if none exists, then dial direct-only (the `--direct` foot-gun fix). Implies a 1-hop, route-finder-bypassing dial that self-heals when the transport drops.
	TransportID           uuid.UUID     // If set, use this specific transport (skips route calculation for direct transports)
	ForwardHops           []routing.Hop // If set, use these hops for forward path (skips route calculation)
	ReverseHops           []routing.Hop // If set, use these hops for reverse path (skips route calculation)
	MuxRoutes             int           // Number of parallel routes to establish (0 or 1 = single route, >1 = mux)
	ExcludeTransportIDs   []uuid.UUID   // Transport IDs to exclude from route calculation (for mux)
	// ExcludeRemoteTransportIDs is the DESTINATION-side analog of
	// ExcludeTransportIDs: transport IDs the REVERSE path must not leave the
	// destination over.
	//
	// Why it exists: the setup node hands the destination `respEdge.Forward`,
	// which is the ForwardRule generated for the REVERSE route's first hop
	// (setupnode.go GenerateRules — fwdRules is keyed by each route's
	// Hops[0].From, and the reverse route starts at the destination). So the
	// destination's route group registers one transport per leg, and that
	// transport is exactly `Reverse[0].TpID` of the plan the initiator dialed.
	// appendRouteToGroup then refuses any leg whose transport is already in
	// that set ("refusing to append mux leg over transport %s already in the
	// group"), because two legs over one link break the mux data plane (#3954).
	//
	// The forward and reverse paths are ranked INDEPENDENTLY (pickDisjointPath),
	// and a 0-intermediate direct reverse carries no intermediates to exclude —
	// so ExcludeIntermediatePKs cannot keep a new leg's reverse off the direct
	// transport the destination is already using for the primary leg. Without
	// this field every aux mux leg to such a destination is planned, dialed
	// through the setup node (full ID reservation + intermediary rule install)
	// and then refused: the churn measured on the live setup nodes.
	//
	// Populated by the aux-mux-leg planners from the group's live legs
	// (RouteGroup.remoteLegTransportIDs). Unset for every non-mux dial, which
	// is therefore byte-identical to before.
	ExcludeRemoteTransportIDs []uuid.UUID
	// ExcludeFirstHopPeers / ExcludeFirstHopIPs are the diversify dial's
	// PEER-level first-hop exclusion: the remote visor PKs (and, where the
	// underlying transport exposes one, the remote IPs) that a sibling tunnel to
	// the same exit already leaves over.
	//
	// ExcludeTransportIDs alone is not enough, because one peer commonly answers
	// on SEVERAL transports: this visor holds an stcpr, a squicr and a sudph
	// transport to the same exit host. Excluding only the sibling's transport ID
	// left the other two as "free first hops", and a diversify dial took the
	// squicr twin — same host, same NIC, same physical path — so the two tunnels
	// shared one link and split it instead of summing (measured 2026-09-17: 50 MB
	// up 2.57 MB/s, down 5.64 on a 29/21 MB split, against 8.36-8.49 down on two
	// genuinely disjoint routes). Excluding the PEER makes every transport to
	// that peer unavailable, so the candidates are the other intermediates — the
	// disjointness the RFC promises.
	//
	// The IP list catches the same host reached under a different PK. Like the
	// transport-ID exclusion these are a soft preference: a dial that can find
	// nothing else still falls back to a shared first hop rather than failing.
	ExcludeFirstHopPeers []cipher.PubKey
	ExcludeFirstHopIPs   []string
	// Datagram, when true, asks the dial to build a faithful-UDP
	// DatagramRouteGroup sibling over the established route (#2607).
	// The route's reliable RouteGroup is still set up exactly as
	// usual (and is what carries the one-time Noise handshake whose
	// ChannelBinding keys the datagram AEAD); the datagram sibling is
	// built alongside it once the handshake completes. The accept
	// side opts in independently via RegisterDatagramPort — both ends
	// must locally intend datagram mode for it to work.
	Datagram bool
	// MinHops, when > 0, is the per-call minimum hop count constraint.
	// Overrides Config.MinHops for this dial only — needed by callers
	// that want a non-direct path even when the visor's global
	// min_hops is 1 (e.g. `cli visor ping mux-bw --min-hops 2`, which
	// must force every dial through an intermediate to test the
	// "mux via intermediates > direct" bandwidth hypothesis without
	// requiring the operator to bump visor-global config).
	//
	// Setting MinHops >= 2 also suppresses the direct-transport
	// fast path inside DialRoutes (the existing
	// "if r.isTpdExist(rPK) { MinHops = 1 }" downgrade is skipped),
	// since the caller has explicitly asked NOT to use direct.
	//
	// 0 = inherit Config.MinHops (legacy / single-route callers).
	MinHops int
	// ExcludeIntermediatePKs lists intermediate-visor PKs that route-
	// finder paths must NOT contain. Source + destination are never
	// excluded (they're endpoints, not intermediates). The mux loop
	// populates this with the intermediates of routes already
	// established so that subsequent routes are constructed through
	// different intermediate paths — disjoint in the intermediate-PK
	// set sense. Applied in both the HTTP route-finder path (post-
	// filter response) and the local route-calc fallback (filter
	// candidates).
	//
	// Disjoint-intermediate routing is AUTOMATIC for the mux loop
	// (MuxRoutes > 1). Callers that want overlapping intermediates
	// must dial with explicit ForwardHops/ReverseHops (which bypass
	// the route-finder entirely). This field is also exposed so
	// callers outside the mux loop can pre-populate exclusions if
	// they have constraints to express.
	ExcludeIntermediatePKs []cipher.PubKey
	ExcludeDMSG            bool // Exclude DMSG transports (for mux — DMSG is a relay, not suitable for multiplexing)
	// DiversifyTransports opts this dial into visor-side auto-
	// diversification for bandwidth aggregation (docs/mux_aggregation_rfc.md
	// step 3). When set, DialRoutes looks up THIS visor's currently-live
	// route groups to the SAME destination PK:port and seeds
	// ExcludeTransportIDs / ExcludeIntermediatePKs with their first-hop
	// transports + intermediates, so a new tunnel to an exit leaves over a
	// DIFFERENT first-hop transport than the tunnels already established to
	// it — their throughputs then SUM instead of splitting one link.
	//
	// It is bounded and safe by construction: the exclusions are added ONLY
	// when >= 1 sibling route group to the exact same dst already exists (the
	// skysocks multi-tunnel "tunnel 2..N" case). A lone dial (tunnel 1, or
	// any app that dials a dst once) finds no sibling and is byte-identical
	// to today. It reuses the mux's own disjoint machinery, but across route
	// groups rather than across legs within one group. Set by the
	// skysocks-client for its extra tunnels; unset everywhere else, so
	// single-dials, control-plane forwards and same-LAN/direct dials are
	// untouched. Falls back gracefully — if no disjoint transport is free the
	// dial proceeds on a shared path (a shared tunnel beats no tunnel).
	DiversifyTransports bool
	// RequireDisjointFirstHop turns the diversify PREFERENCE above into a
	// REQUIREMENT: rather than conceding a shared first hop when nothing
	// disjoint is free, the dial fails with ErrNoDisjointFirstHop. Only
	// meaningful together with DiversifyTransports (without siblings there is
	// nothing to be disjoint from, and the dial proceeds as usual).
	//
	// It exists so a caller that is GROWING a pool of sibling tunnels can tell
	// "the topology has more disjoint paths to offer" from "it has none left".
	// The skysocks standby pool dials with it set and stops filling on the
	// first ErrNoDisjointFirstHop — the tunnel-level twin of the mux's
	// "no disjoint path available right now; settling at current degree".
	// A shared extra tunnel aggregates nothing, so for that caller a shared
	// path is not a lesser success but a wrong answer.
	RequireDisjointFirstHop bool
	// TunnelRole records what the DIALER intends this route group to be:
	// "active" (it carries streams) or "standby" (it is held open, measured
	// and ready to take over). Set by the skysocks client; empty everywhere
	// else, and empty is the only value a non-tunnel route group ever has.
	// Surfaced as MuxInfo.TunnelRole. It is the dialing end's own label — the
	// accepting end (the exit) has no idea which of its peers' tunnels are in
	// standby, so this field is always empty there.
	TunnelRole string
	// AdoptReservePort, when set, turns the dial into an ADOPTION: instead of
	// setting up a new route, DialRoutes takes the leg reserve (a split's
	// standalone group, leg_split.go) toward the same peer whose far-end port
	// is this one, re-points its chain at the dial's own descriptor and runs
	// the ordinary handshake on it (leg_adopt.go). It fails — and the caller
	// dials the usual way — when no such reserve exists or the peer does not
	// answer. 0 = an ordinary dial.
	AdoptReservePort routing.Port

	// Distribution overrides the route group's per-packet
	// distribution strategy when set (Mode != DistributionUnset).
	// Populated either by a routing-policy script (see
	// DialAdjustment.Distribution) or directly by a CLI caller
	// constructing DialOptions. Zero-value means "use the
	// router's visor-wide muxMode default."
	Distribution DistributionConfig
	KeepAlive    time.Duration // Route keepalive (0 = DefaultRouteKeepAlive). Routes idle longer expire.
	// ParallelRouteSetup, when > 0, overrides Config.ParallelRouteSetup
	// for this dial only — the number of candidate route groups whose
	// setup is raced concurrently (first to complete its handshake wins).
	// 1 forces sequential setup for this dial; 0 inherits the visor
	// config. See Config.ParallelRouteSetup.
	ParallelRouteSetup int
	// AppName is the originating app's name. Set by appnet's
	// DialContextWithOptions when the caller threaded a context
	// carrying it (RPCIngressGateway.Dial uses appnet.WithAppName).
	// router-side: scopedLog prefers this over AppLookup-by-port,
	// since DialRoutes is invoked with an ephemeral source port that
	// isn't the app's registered SetAppPort value.
	AppName string

	// ForwardMinHops / ReverseMinHops are per-direction overrides for
	// MinHops. When > 0 they take precedence over the symmetric
	// MinHops field for THAT direction only. Use case: bandwidth-
	// asymmetric workloads (HTTP GET = tiny upstream, bulk downstream)
	// where the user wants e.g. direct stcpr for the forward leg but
	// multi-hop for the reverse-direction bulk payload.
	//
	// 0 (the default) means "inherit MinHops" — back-compat for
	// callers that only care about symmetric routes.
	//
	// Resolution at use site (see resolveDirMinHops): if the per-
	// direction override is > 0 it wins; else MinHops; else
	// Config.MinHops; else the global default.
	ForwardMinHops int
	ReverseMinHops int

	// ForwardMuxRoutes / ReverseMuxRoutes are per-direction overrides
	// for MuxRoutes. When > 1 they take precedence over the symmetric
	// MuxRoutes field for THAT direction's route count.
	//
	// Use case: the operator's "direct upstream + multi-hop downstream"
	// test — set ForwardMuxRoutes=1 and ReverseMuxRoutes=4 to open a
	// single forward leg (the GET request rides one TCP socket) while
	// the reverse direction aggregates the bulk payload across 4
	// multi-hop legs. The data plane already supports rg.fwd[] and
	// rg.rvs[] being different lengths (the read path is by route-id
	// lookup, not slice index), so this is purely a setup-side change.
	//
	// 0 (the default) means "inherit MuxRoutes" for that direction.
	// When the caller wants strictly N=1 on a side they should set it
	// to 1 explicitly (not 0) since 0 still falls through to MuxRoutes.
	ForwardMuxRoutes int
	ReverseMuxRoutes int

	// RotationIntervalSeconds, when > 0, instructs the route group
	// to fire its rotation hook every N seconds after setup. The
	// hook can drop legs and/or add new ones, enabling bandwidth-
	// spreading policies that rotate across the eligible-peer set.
	// Populated by the policy layer (DialAdjustment) from
	// RouteSpec.RotationIntervalSeconds; zero = no rotation.
	RotationIntervalSeconds int
	// contains filtered or unexported fields
}

DialOptions describes dial options.

func DefaultDialOptions

func DefaultDialOptions() *DialOptions

DefaultDialOptions returns default dial options. Used by default if nil is passed as options.

func (*DialOptions) AnyMinHopsConstraint

func (o *DialOptions) AnyMinHopsConstraint() bool

AnyMinHopsConstraint reports whether the caller has set any min-hops constraint (symmetric or per-direction). Used at the direct-tp downgrade site in DialRoutes: if either direction wants multi-hop, the "transport-exists → drop MinHops to 1" optimization should be suppressed (mirroring the existing opts.MinHops > 1 check).

func (*DialOptions) EffectiveMinHops

func (o *DialOptions) EffectiveMinHops(forward bool) int

EffectiveMinHops returns the per-direction min-hops constraint: forward=true → ForwardMinHops if > 0 else MinHops; forward=false → ReverseMinHops if > 0 else MinHops. Callers (the route-finder query site + the local-BFS fallback) read this once per direction to support bandwidth-asymmetric workloads where forward (small uplink) can use direct/short paths while reverse (bulk downlink) is forced onto multi-hop.

Returns 0 when opts is nil or both per-direction + symmetric fields are 0 — preserves the caller's "inherit Config.MinHops" semantic.

func (*DialOptions) EffectiveMuxRoutes

func (o *DialOptions) EffectiveMuxRoutes(forward bool) int

EffectiveMuxRoutes returns the per-direction mux-route count: forward=true → ForwardMuxRoutes if > 0 else MuxRoutes; forward=false → ReverseMuxRoutes if > 0 else MuxRoutes. Callers (establishMuxRoutes, the dial-shape logger) read this once per direction to support asymmetric mux topologies — most commonly 1 forward + N reverse for download-heavy workloads.

Returns 0 when opts is nil or all relevant fields are 0; preserves the "single route" default behavior.

type DistributionConfig

type DistributionConfig struct {
	// Mode selects the per-packet algorithm. DistributionUnset
	// means "no override."
	Mode DistributionMode

	// Weights are the operator-supplied fractional weights for
	// DistributionWeighted. Must be the same length as the route
	// group's leg count; values are normalized into an integer
	// schedule by the selector. Empty for non-weighted modes.
	Weights []float64

	// SizeThreshold is the payload-size boundary for
	// DistributionSizeThreshold. Packets larger than this go to
	// leg 0 (assumed to be the wider pipe — the first leg the
	// route-finder returned, ranked by latency); smaller packets
	// round-robin across the remaining legs.
	SizeThreshold int

	// DSCPThreshold is the IPv4 DSCP boundary for
	// DistributionDSCPPriority. Packets with DSCP >= this value
	// take leg 0; others round-robin across the rest. Common
	// values: 0x2E (EF — VoIP), 0x10 (AF — interactive).
	DSCPThreshold int
}

DistributionConfig drives the per-packet leg-selection strategy for a mux-enabled route group. Set on DialAdjustment (per-dial, from a routing-policy script) or on DialOptions (per-call from a CLI flag); zero-value (Mode == DistributionUnset) means "no override — use the router's visor-wide muxMode."

See pkg/router/policy/distribution.go for the descriptor grammar that policy scripts emit.

type DistributionMode

type DistributionMode int

DistributionMode enumerates the per-packet distribution strategies the operator can pick from. Names match the vocabulary the RFC's Starlark descriptor parses to.

const (
	// DistributionUnset means "no policy override." The route
	// group uses the router's visor-wide muxMode (currently
	// WeightModeAuto: latency-weighted with round-robin fallback).
	DistributionUnset DistributionMode = iota
	// DistributionRoundRobin distributes packets evenly across
	// all legs, ignoring latency. Same effect as the existing
	// WeightModeEqual.
	DistributionRoundRobin
	// DistributionAuto picks WeightModeAuto explicitly — useful
	// when a script wants to override an operator's globally-
	// configured Equal mode back to latency-weighted for one app.
	DistributionAuto
	// DistributionWeighted uses the operator-supplied fractional
	// weights in Weights[]. Length must match the leg count.
	DistributionWeighted
	// DistributionSizeThreshold routes packets by payload size:
	// > SizeThreshold goes to leg 0, ≤ SizeThreshold round-robins
	// across the rest. Useful for bulk + control mixes (VPN,
	// terminal multiplexing) where large packets should claim
	// the wider pipe and small packets stay on the low-latency
	// leg.
	DistributionSizeThreshold
	// DistributionSticky5Tuple pins each flow (identified by the
	// IPv4 5-tuple — src/dst IP + src/dst port + protocol) to a
	// single leg for the flow's lifetime. Same flow → same leg
	// every packet, deterministic. Meaningful for VPN traffic
	// (vpn-client, vpn-server) where the payload is an IPv4
	// packet; for other apps the selector falls back to hashing
	// the payload prefix (still deterministic by content but no
	// flow semantics). Useful for TCP-over-overlay where leg
	// reordering hurts congestion control.
	DistributionSticky5Tuple
	// DistributionLatencyAdaptive picks the leg with the lowest
	// current transport-layer latency for every packet. Differs
	// from DistributionAuto (latency-weighted schedule, built
	// every Rebuild cycle): adaptive evaluates per-packet, so a
	// leg whose RTT spikes mid-session loses traffic
	// immediately. Cost: one linear scan over the leg array per
	// packet (typically <=4 legs, sub-µs).
	DistributionLatencyAdaptive
	// DistributionDSCPPriority routes packets by the IPv4 DSCP
	// field (upper 6 bits of the ToS byte at IP-header offset 1).
	// Packets with DSCP >= the configured threshold (commonly
	// 0x2E for EF / VoIP, or 0x10+ for AF / interactive) take
	// leg 0 — the low-latency leg. Lower-priority packets
	// round-robin across the rest. Like sticky:5tuple, meaningful
	// for VPN apps; non-IPv4 payloads default to round-robin.
	DistributionDSCPPriority
	// DistributionCapacity spreads packets in proportion to each
	// leg's recently-measured throughput (bytes carried since the
	// last weight rebuild), with an exploration floor so every
	// live leg keeps carrying (and so keeps being measured). It
	// bootstraps as equal round-robin until legs have moved bytes.
	// Unlike DistributionAuto (weights by inverse LATENCY, which
	// dumps a bulk flow onto the single lowest-latency leg and
	// starves higher-latency-but-fat legs), capacity weighting
	// FILLS each leg toward its throughput — the right strategy
	// for aggregating bandwidth across a disjoint multi-leg route.
	// This is the adaptive default's bulk-spread mode.
	DistributionCapacity
	// DistributionECF is the predictive Earliest-Completion-First
	// hold-back scheduler (router.WeightModeECF). Unlike
	// DistributionCapacity, which sprays a fraction of frames onto
	// slower legs in proportion to their goodput — and so keeps
	// head-of-line-stalling the in-order reorder buffer on those slow
	// legs — ECF sends on the fastest leg while it has send capacity
	// and only spills onto a slower leg when the slow leg would deliver
	// its frame sooner than the fast leg can drain its own backlog. It
	// is the mode that actually aggregates across heterogeneous legs
	// without paying the slow-leg HoL cost. Per-packet, O(legs).
	DistributionECF
	// DistributionOTIAS is the Out-of-order Transmission for In-order
	// Arrival scheduler (router.WeightModeOTIAS). It reuses ECF's per-leg
	// estimators but assigns each frame to the leg whose estimated ARRIVAL
	// (backlog drain time + one-way delay) is soonest, deliberately handing
	// later frames to slower-but-idle legs; the reorder buffer restores order.
	DistributionOTIAS
	// DistributionSTMS is the Slide Together Multipath Scheduler
	// (router.WeightModeSTMS). It keeps the head of the stream on the fast
	// leg (filling its send window first) and slides later data onto the
	// soonest-arriving slower leg so pieces converge in order — without ECF's
	// hold-back decline once a leg is in the active set.
	DistributionSTMS
)

func (DistributionMode) String

func (m DistributionMode) String() string

String returns the stable label for a DistributionMode. Used by router-side log fields so operator-facing log lines say "round-robin" instead of "1".

type DstTransportOracle

type DstTransportOracle interface {
	DstTransports(ctx context.Context, src, dst cipher.PubKey) ([]*transport.Entry, error)
}

DstTransportOracle fetches a destination visor's OWN transport list authoritatively from the destination. It is the router-facing seam for the RSN-oracle 2-hop route path: the router calls DstTransports(src, dst) and computes the 2-hop route locally from the result. The visor implements it (typically via fetchDstTransportsViaOracle / NewDmsgRSNOracle) since it owns the RSN RPC connection and the S->D delivery transport; the router stays decoupled from both. Declared here (unbuilt-tagged) so the Router interface referencing it compiles on every target.

func NewDmsgRSNOracle

func NewDmsgRSNOracle(dmsgC *dmsg.Client, setupNodes []cipher.PubKey, log *logging.Logger) DstTransportOracle

NewDmsgRSNOracle builds the production DstTransportOracle: RSN signs over dmsg, query delivered to the destination over dmsg. setupNodes is the trusted-RSN list (conf.EffectiveRouteSetupNodes). Returns nil when no setup nodes are configured, so the caller can leave the oracle unset (path stays inert).

func NewDstTransportOracle

func NewDstTransportOracle(rsnRPC *rpc.Client, deliverer TransportQueryDeliverer) DstTransportOracle

NewDstTransportOracle builds the reference DstTransportOracle from an RSN RPC client and a query deliverer.

type EmbeddedSetupNode

type EmbeddedSetupNode interface {
	// CreateRouteGroup creates a route group using the embedded setup-node's dmsg client.
	CreateRouteGroup(ctx context.Context, biRt routing.BidirectionalRoute) (routing.EdgeRules, error)
	// PK returns the public key of the embedded setup-node.
	PK() cipher.PubKey
	// DmsgClient returns the dmsg client used by the embedded setup-node.
	DmsgClient() *dmsg.Client
}

EmbeddedSetupNode is an interface for embedded route setup-nodes that can create route groups locally without dialing a remote setup-node.

type ForwardQueue

type ForwardQueue struct {
	TpID     uuid.UUID     `json:"tp_id"`
	TpType   string        `json:"tp_type,omitempty"`
	Remote   cipher.PubKey `json:"remote_pk,omitempty"`
	Queue    int           `json:"queue"`
	Capacity int           `json:"capacity"`
	// QueuedBytes is what the frames waiting in it currently hold, against
	// forward.queue_bytes when that bound is set.
	QueuedBytes int64  `json:"queued_bytes"`
	Sent        uint64 `json:"sent"`
	// The named drop reasons, one counter each.
	DropsQueueFull    uint64 `json:"forward_drop_queue_full"`
	DropsWriteTimeout uint64 `json:"forward_drop_write_timeout"`
	DropsWriteError   uint64 `json:"forward_drop_write_error"`
}

ForwardQueue is one next-hop transport's transit write queue: what this visor is relaying for other people's routes, and what it had to drop. A climbing DropsQueueFull or DropsWriteTimeout on one transport with healthy transfers elsewhere is the expected shape of a single wedged transit peer — which is exactly what it must look like, since that peer used to take the whole dataplane down with it.

type Group

type Group interface {
	Close() error
	LocalAddr() net.Addr
	RemoteAddr() net.Addr
	IsAlive() bool
}

Group is the narrow interface every route-group variant shares. Stream-shaped (RouteGroup, net.Conn) and datagram-shaped (DatagramRouteGroup, net.PacketConn) both satisfy it. Read/Write vs ReadFrom/WriteTo deliberately stay on the concrete types — keeping the shared interface narrow avoids leaking stream vs datagram semantics into call sites that only care about lifecycle.

type HealthCheckArgs

type HealthCheckArgs struct{}

HealthCheckArgs is an empty struct for the health check call.

type HealthCheckReply

type HealthCheckReply struct {
	Status string `json:"status"`
	// Caps lists the request shapes this node understands, the same list the
	// Capabilities RPC returns — carried here too so a health probe doubles as
	// the negotiation for a client that is already making one.
	Caps    []string `json:"caps,omitempty"`
	Version string   `json:"version,omitempty"`
	Commit  string   `json:"commit,omitempty"`
	Date    string   `json:"date,omitempty"`
}

HealthCheckReply is returned by the HealthCheck RPC method.

type IDReserver

type IDReserver interface {
	io.Closer
	fmt.Stringer

	// ReserveIDs reserves route IDs from the router clients.
	// It uses an internal map to know how many ids to reserve from each router.
	ReserveIDs(ctx context.Context) error

	// PopID pops a reserved route ID from the ID stack of the given public key.
	PopID(pk cipher.PubKey) (routing.RouteID, bool)

	// TotalIDs returns the total number of route IDs we have reserved from the routers.
	TotalIDs() int

	// Client returns a router client of given public key.
	Client(pk cipher.PubKey) *Client

	// ReturnToPool returns connections to a pool instead of closing them.
	ReturnToPool(pool *ClientPool)
}

IDReserver reserves route IDs from remote routers. It takes in a slice of paths where each path is a slice of hops and each hop has src and dst public keys and also a transport ID.

func NewIDReserver

func NewIDReserver(ctx context.Context, dialer network.Dialer, pool *ClientPool, paths [][]routing.Hop) (IDReserver, error)

NewIDReserver creates a new route ID reserver from a dialer and a slice of paths. The exact number of route IDs to reserve from each router is determined from the slice of paths. If pool is non-nil, connections are borrowed from the pool instead of dialed fresh.

func ReserveRouteIDs

func ReserveRouteIDs(ctx context.Context, log logrus.FieldLogger, dialer network.Dialer, pool *ClientPool, route routing.BidirectionalRoute) (idR IDReserver, err error)

ReserveRouteIDs dials to all routers and reserves required route IDs from them. The number of route IDs to be reserved per router, is extrapolated from the 'route' input.

type IntakeStats

type IntakeStats struct {
	// UnknownPacketTypes counts packets dropped as ErrUnknownPacketType, by
	// type name. Non-zero means a peer sends a type this build does not
	// route, or a transport lost its route-ID-0 handler (#4725).
	UnknownPacketTypes map[string]int64 `json:"unknown_packet_types,omitempty"`
	// ControlPlaneToRouter counts route-ID-0 control packets that reached
	// the router instead of being intercepted on the transport, by type.
	ControlPlaneToRouter map[string]int64 `json:"control_plane_to_router,omitempty"`
	// StaleRouteDrops counts frames for rules that no longer exist (normal
	// during teardown; a climbing count on an idle visor is not).
	StaleRouteDrops int64 `json:"stale_route_drops"`
	// RouteGroupQueues is the two inbound queues of every active route group:
	// the app queue (queue/capacity) and the intake queue between the router's
	// shared read loop and the group's own worker (intake_*).
	//
	// An app queue at capacity is an app that stopped reading. That used to
	// block the shared loop — up to 30 s per packet, stalling every transport
	// on the visor; now it backs up into that group's intake queue instead,
	// and a climbing intake_drops is where the loss shows up. Non-zero
	// intake_drops on one group and healthy transfers elsewhere is the
	// expected shape of a single wedged app.
	RouteGroupQueues []RouteGroupQueue `json:"route_group_queues,omitempty"`
	// ForwardQueues is the TRANSIT write side: one entry per next-hop
	// transport this visor is relaying frames to. Non-zero drops name a peer
	// that stopped draining; they used to be a visor-wide freeze instead
	// (see router_forward.go).
	ForwardQueues []ForwardQueue `json:"forward_queues,omitempty"`
}

IntakeStats is the router's inbound-path view.

type LegChange

type LegChange struct {
	Event    string
	LegIndex int
}

LegChange describes the mutation that triggered the OnLegChange callback. Event is "added" or "dropped"; LegIndex is the leg that changed (the new leg's index for "added", the dropped leg's last-known index for "dropped").

type LegChangeHook

type LegChangeHook interface {
	OnLegChange(info DialInfo, legs []LegInfo, change LegChange) DistributionConfig
}

LegChangeHook is an optional hook fired by the route group whenever its set of mux legs mutates — a new leg added via the establishment loop or appendRouteToGroup, or a leg dropping due to a transport close. The hook can return a new DistributionConfig to re-balance the per-packet selector against the updated leg count; returning the zero value (Mode == DistributionUnset) leaves the existing distribution in place.

The route group calls OnLegChange synchronously from its leg- mutation paths so the schedule rebuild stays consistent with the leg set. Implementations must return quickly — the call budget mirrors the dial-time policy timeout (step ceiling).

type LegInfo

type LegInfo struct {
	Index int
	Kind  string
	// TransportID is the leg transport's stable UUID
	// (ManagedTransport.Entry.ID) as a string. Unlike Index — which
	// shifts when legs are dropped/added and the tps[] slice compacts
	// — TransportID identifies the same underlying transport across
	// ticks, so an on_tick policy can key per-leg state (e.g. an EWMA
	// of latency) by it and smooth signals over time. Empty for a nil
	// leg slot.
	TransportID string
	LatencyMs   int
	Alive       bool
	// Standby is true when the leg is a WARM STANDBY: rules installed and
	// kept alive, but not selected for sending. Lets an on_tick policy see the
	// current gate state so it can promote a warm spare instead of a cold add,
	// or demote a leg it wants to keep warm. See docs/warm_standby_legs_rfc.md.
	Standby bool
	// Direct is true when this leg is the DIRECT (forward-only) leg of a
	// UNIDIRECTIONAL route group: its transport goes straight to a route-group
	// endpoint (1 hop, no intermediary) and it carries only the forward/upload
	// direction. Sourced from routeMux.legIsDirectTp, which is false for a
	// non-directional (symmetric) group — there every leg carries both
	// directions. Lets the adaptive on_tick maintain a floor of active REVERSE
	// (download) legs independent of the direct leg, so a download never falls
	// back onto warm-standby legs for want of an active reverse path.
	Direct bool
	// Flipped is the route group's current unidirectional flip state (the same
	// value on every leg of the group), sourced from routeMux.dirState. Because a
	// leg's send-side direction is an ASSIGNMENT the flip controller can swap —
	// not an intrinsic property — the download-direction class is Direct ==
	// Flipped (unflipped: download rides the multihop legs; flipped: it rides the
	// direct leg). The adaptive on_tick keeps its active-download floor on that
	// class, whichever way the flip currently sits. Always false for a
	// non-directional (symmetric) group.
	Flipped bool
	// SentBytes / RecvBytes are the leg transport's cumulative
	// payload-byte counters (ManagedTransport.GetBandwidth). Lets a
	// policy's on_tick rotate a leg after it carries a byte threshold
	// rather than purely on elapsed time.
	SentBytes uint64
	RecvBytes uint64
	// Retransmits is how many SACK retransmit packets this leg has carried.
	// A high retransmits-to-traffic ratio marks a lossy leg, letting an
	// on_tick policy shed lossy intermediates (not just slow ones).
	Retransmits uint64
	// Hops is the intermediate-PK chain this leg traverses (forward,
	// src->dst, endpoints excluded). Lets on_tick build a precise
	// ExcludeHops set so a rotated-in leg avoids the rotated-out leg's
	// intermediates. Empty for a direct (0-intermediate) leg.
	Hops []string
}

LegInfo describes one mux leg's current state. Index is the position in the route group's tps[] slice; Kind is the transport type ("stcpr" / "sudph" / "stcp" / "dmsg" — though DMSG is filtered out of mux groups at setup so it shouldn't appear here). LatencyMs is the most recent measurement, 0 for "unknown." Alive reflects whether the transport is currently serving (not closed / nil).

type LegStats

type LegStats struct {
	// Index in the rg's tps[] slice. Stable for the rg's lifetime
	// (transports are appended, never re-ordered) so the index is
	// also a stable identifier for runtime add/remove operations.
	Index int
	// SentBytes / SentPackets are what THIS leg carried outbound.
	SentBytes   uint64
	SentPackets uint64
	// RecvBytes / RecvPackets are what THIS leg carried inbound.
	// Resolved from the reverse-rule's KeyRouteID at packet-handle
	// time, so it reflects what actually arrived on this transport,
	// not what the peer said it sent.
	RecvBytes   uint64
	RecvPackets uint64
	// PayloadBytes is the UNIQUE in-order payload this leg delivered (each seq
	// counted once, on the leg it first arrived on) — retransmits/duplicates
	// excluded, so the per-leg values sum to the transfer size and give a
	// confound-free per-direction attribution (which legs carried the download vs
	// the upload), unlike RecvBytes which includes retransmit/duplicate inflation.
	PayloadBytes uint64
	// DupBytes is inbound DUPLICATE data this leg carried (seqs already
	// delivered/buffered on arrival) — the peer's spurious-retransmit waste,
	// which selectFastestTransport concentrates on the fastest leg. RepairBytes
	// is inbound FEC repair frames (deliberate overhead). Together with
	// PayloadBytes they decompose RecvBytes so "why is this (standby) leg
	// receiving" is answerable from telemetry.
	DupBytes    uint64
	RepairBytes uint64
	// Retransmits is how many SACK retransmit packets THIS leg has
	// carried. A high retransmits:sentPackets ratio marks a lossy leg —
	// the signal a routing policy needs to shed lossy intermediates and
	// the scheduler needs to deweight them.
	Retransmits uint64
	// GoodputUpBps / GoodputDownBps are this leg's recent goodput split by
	// direction — the EWMA of the SENT (up) and RECV (down) byte deltas per
	// second over the telemetry refresh window (~1s for the status page).
	// Distinct from the cumulative SentBytes/RecvBytes counters above: they
	// are the RATE each direction is moving now, not the lifetime total. 0
	// until a second sample lands. Sampled in snapshotLegs.
	GoodputUpBps   float64
	GoodputDownBps float64
	// GoodputBps is the combined (up+down) recent goodput, retained as the
	// sum of GoodputUpBps+GoodputDownBps for back-compat with callers that
	// want a single figure.
	GoodputBps float64
}

LegStats is a snapshot of per-mux-leg traffic counters at a point in time. One entry per active route in the rg's tps[] order.

Counters are cumulative since route-group creation (atomic uint64); callers wanting rates take two snapshots and divide by elapsed time. Used by 'cli proxy mux-info' to show where bandwidth is going across the mux'd routes — the missing piece for verifying that adding more routes actually aggregates throughput rather than just trading share.

type LegWeight

type LegWeight struct {
	TransportID string  `json:"transport_id"`
	Weight      float64 `json:"weight"`
}

LegWeight is one leg's explicit send weight: the first-hop transport that identifies the leg, and the fraction of the send schedule it is to carry. Weights are normalized by the selector, so {2,1} and {0.667,0.333} are the same instruction.

type Map

type Map map[cipher.PubKey]*Client

Map is a map of router RPC clients associated with the router's visor PK.

func MakeMap

func MakeMap(ctx context.Context, dialer network.Dialer, pks []cipher.PubKey) (Map, error)

MakeMap makes a Map of the router clients, where the key is the router's visor public key. It creates these router clients by dialing to them concurrently. A hard timeout of MakeMapTimeout is applied to prevent indefinite blocking when visors are unreachable. The results channel is buffered so that goroutines completing after cancellation do not leak waiting to send.

func MakePooledMap

func MakePooledMap(ctx context.Context, pool *ClientPool, pks []cipher.PubKey) (Map, error)

MakePooledMap is like MakeMap but borrows connections from a ClientPool instead of dialing fresh ones. Clients that were successfully used should be returned to the pool via ReturnToPool; broken ones via DiscardFromPool.

func (Map) Client

func (cm Map) Client(rPK cipher.PubKey) *Client

Client returns a router client of given public key.

func (Map) CloseAll

func (cm Map) CloseAll() (errs []error)

CloseAll closes all contained router clients.

func (Map) DiscardFromPool

func (cm Map) DiscardFromPool(pool *ClientPool)

DiscardFromPool closes all clients without pooling them.

func (Map) ReturnToPool

func (cm Map) ReturnToPool(pool *ClientPool)

ReturnToPool returns all clients in the Map to the given pool for reuse.

type MockIDReserver

type MockIDReserver struct {
	mock.Mock
}

MockIDReserver is an autogenerated mock type for the IDReserver type

func NewMockIDReserver

func NewMockIDReserver(t interface {
	mock.TestingT
	Cleanup(func())
}) *MockIDReserver

NewMockIDReserver creates a new instance of MockIDReserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. The first argument is typically a *testing.T value.

func (*MockIDReserver) Client

func (_m *MockIDReserver) Client(pk cipher.PubKey) *Client

Client provides a mock function with given fields: pk

func (*MockIDReserver) Close

func (_m *MockIDReserver) Close() error

Close provides a mock function with no fields

func (*MockIDReserver) PopID

func (_m *MockIDReserver) PopID(pk cipher.PubKey) (routing.RouteID, bool)

PopID provides a mock function with given fields: pk

func (*MockIDReserver) ReserveIDs

func (_m *MockIDReserver) ReserveIDs(ctx context.Context) error

ReserveIDs provides a mock function with given fields: ctx

func (*MockIDReserver) ReturnToPool

func (_m *MockIDReserver) ReturnToPool(_ *ClientPool)

ReturnToPool provides a mock function for returning connections to a pool.

func (*MockIDReserver) String

func (_m *MockIDReserver) String() string

String provides a mock function with no fields

func (*MockIDReserver) TotalIDs

func (_m *MockIDReserver) TotalIDs() int

TotalIDs provides a mock function with no fields

type MockRouteGroupDialer

type MockRouteGroupDialer struct {
	mock.Mock
}

MockRouteGroupDialer is an autogenerated mock type for the RouteGroupDialer type

func NewMockRouteGroupDialer

func NewMockRouteGroupDialer(t interface {
	mock.TestingT
	Cleanup(func())
}) *MockRouteGroupDialer

NewMockRouteGroupDialer creates a new instance of MockRouteGroupDialer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. The first argument is typically a *testing.T value.

func (*MockRouteGroupDialer) Dial

Dial provides a mock function with given fields: ctx, log, dmsgC, setupNodes, req

type MockRouter

type MockRouter struct {
	mock.Mock
	// contains filtered or unexported fields
}

MockRouter is an autogenerated mock type for the Router type

func NewMockRouter

func NewMockRouter(t interface {
	mock.TestingT
	Cleanup(func())
}) *MockRouter

NewMockRouter creates a new instance of MockRouter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. The first argument is typically a *testing.T value.

func (*MockRouter) AcceptDatagram

func (_m *MockRouter) AcceptDatagram(ctx context.Context) (*DatagramRouteGroup, routing.Port, error)

AcceptDatagram provides a mock function with given fields: ctx

func (*MockRouter) AcceptRoutes

func (_m *MockRouter) AcceptRoutes(_a0 context.Context) (net.Conn, error)

AcceptRoutes provides a mock function with given fields: _a0

func (*MockRouter) ActiveRouteStatuses

func (_m *MockRouter) ActiveRouteStatuses() []RouteStatus

ActiveRouteStatuses provides a mock function with no fields

func (*MockRouter) AddMuxRouteByHops

func (_m *MockRouter) AddMuxRouteByHops(_a0 routing.RouteDescriptor, _a1, _a2 []routing.Hop) error

AddMuxRouteByHops provides a mock function

func (*MockRouter) Close

func (_m *MockRouter) Close() error

Close provides a mock function with no fields

func (*MockRouter) CloseRouteGroupsForApp

func (_m *MockRouter) CloseRouteGroupsForApp(_a0 string) int

CloseRouteGroupsForApp provides a mock function

func (*MockRouter) DelRules

func (_m *MockRouter) DelRules(_a0 []routing.RouteID)

DelRules provides a mock function with given fields: _a0

func (*MockRouter) DialHook

func (_m *MockRouter) DialHook() DialHook

DialHook provides a mock function with given fields:

func (*MockRouter) DialRoutes

func (_m *MockRouter) DialRoutes(ctx context.Context, rPK cipher.PubKey, lPort routing.Port, rPort routing.Port, opts *DialOptions) (net.Conn, error)

DialRoutes provides a mock function with given fields: ctx, rPK, lPort, rPort, opts

func (*MockRouter) DialRoutesDatagram

func (_m *MockRouter) DialRoutesDatagram(ctx context.Context, rPK cipher.PubKey, lPort routing.Port, rPort routing.Port, opts *DialOptions) (net.Conn, *DatagramRouteGroup, error)

DialRoutesDatagram provides a mock function with given fields: ctx, rPK, lPort, rPort, opts

func (*MockRouter) EffectiveMinHops

func (_m *MockRouter) EffectiveMinHops(opts *DialOptions) uint16

EffectiveMinHops provides a mock function with given fields: opts

func (*MockRouter) GetExistingTPOnly

func (_m *MockRouter) GetExistingTPOnly() bool

GetExistingTPOnly provides a mock function

func (*MockRouter) GetForceLocalRoutes

func (_m *MockRouter) GetForceLocalRoutes() bool

GetForceLocalRoutes provides a mock function

func (*MockRouter) GetLastRouteCalcTime

func (_m *MockRouter) GetLastRouteCalcTime() time.Duration

GetLastRouteCalcTime provides a mock function with no fields

func (*MockRouter) GetMuxFEC

func (_m *MockRouter) GetMuxFEC() bool

GetMuxFEC provides a mock function; see SetMuxFEC.

func (*MockRouter) GetMuxRoutes

func (_m *MockRouter) GetMuxRoutes() int

GetMuxRoutes provides a mock function

func (*MockRouter) GrowMuxFromPool

func (_m *MockRouter) GrowMuxFromPool(_a0 routing.Port, _a1, _a2 int) (int, error)

GrowMuxFromPool provides a mock function

func (*MockRouter) GrowMuxRoute

func (_m *MockRouter) GrowMuxRoute(_a0 routing.RouteDescriptor, _a1, _a2 int) (int, error)

GrowMuxRoute provides a mock function

func (*MockRouter) IntroduceRules

func (_m *MockRouter) IntroduceRules(rules routing.EdgeRules) error

IntroduceRules provides a mock function with given fields: rules

func (*MockRouter) MuxCounters

func (_m *MockRouter) MuxCounters() MuxCounters

MuxCounters provides a mock function with no fields

func (*MockRouter) MuxEvents

func (_m *MockRouter) MuxEvents() []MuxEvent

MuxEvents provides a mock function with no fields

func (*MockRouter) NoteTunnelEvent

func (_m *MockRouter) NoteTunnelEvent(localPort routing.Port, event string, reason string, role string) bool

NoteTunnelEvent provides a mock function

func (*MockRouter) PingRoute

func (_m *MockRouter) PingRoute(ctx context.Context, rPK cipher.PubKey, lPort routing.Port, rPort routing.Port, opts *DialOptions) (net.Conn, error)

PingRoute provides a mock function with given fields: ctx, rPK, lPort, rPort, opts

func (*MockRouter) RegisterDatagramPort

func (_m *MockRouter) RegisterDatagramPort(port routing.Port)

RegisterDatagramPort provides a mock function with given fields: port

func (*MockRouter) RemoveMuxRouteByTransport

func (_m *MockRouter) RemoveMuxRouteByTransport(_a0 routing.RouteDescriptor, _a1 uuid.UUID) error

RemoveMuxRouteByTransport provides a mock function

func (*MockRouter) ReserveKeys

func (_m *MockRouter) ReserveKeys(n int) ([]routing.RouteID, error)

ReserveKeys provides a mock function with given fields: n

func (*MockRouter) RouteGroupHops

func (_m *MockRouter) RouteGroupHops(_a0 routing.RouteDescriptor) []RouteHopInfo

RouteGroupHops provides a mock function

func (*MockRouter) RouteGroupMuxInfo

func (_m *MockRouter) RouteGroupMuxInfo(_a0 routing.RouteDescriptor) (MuxInfo, bool)

RouteGroupMuxInfo provides a mock function

func (*MockRouter) RouteGroupMuxInfoAll

func (_m *MockRouter) RouteGroupMuxInfoAll() []MuxInfo

RouteGroupMuxInfoAll provides a mock function with no fields

func (*MockRouter) RouteGroupMuxInfoForApp

func (_m *MockRouter) RouteGroupMuxInfoForApp(_a0 string) []MuxInfo

RouteGroupMuxInfoForApp provides a mock function

func (*MockRouter) RoutesCount

func (_m *MockRouter) RoutesCount() int

RoutesCount provides a mock function with no fields

func (*MockRouter) RoutingTableStats

func (_m *MockRouter) RoutingTableStats() routing.RoutingTableStats

RoutingTableStats provides a mock function with no fields

func (*MockRouter) Rule

func (_m *MockRouter) Rule(_a0 routing.RouteID) (routing.Rule, error)

Rule provides a mock function with given fields: _a0

func (*MockRouter) Rules

func (_m *MockRouter) Rules() []routing.Rule

Rules provides a mock function with no fields

func (*MockRouter) SaveRoutingRules

func (_m *MockRouter) SaveRoutingRules(rules ...routing.Rule) error

SaveRoutingRules provides a mock function with given fields: rules

func (*MockRouter) SaveRule

func (_m *MockRouter) SaveRule(_a0 routing.Rule) error

SaveRule provides a mock function with given fields: _a0

func (*MockRouter) Serve

func (_m *MockRouter) Serve(_a0 context.Context) error

Serve provides a mock function with given fields: _a0

func (*MockRouter) SetDstTransportOracle

func (_m *MockRouter) SetDstTransportOracle(_a0 DstTransportOracle)

SetDstTransportOracle provides a mock function with given fields: _a0

func (*MockRouter) SetExistingTPOnly

func (_m *MockRouter) SetExistingTPOnly(_a0 bool)

SetExistingTPOnly provides a mock function with given fields: _a0

func (*MockRouter) SetForceLocalRoutes

func (_m *MockRouter) SetForceLocalRoutes(_a0 bool)

SetForceLocalRoutes provides a mock function with given fields: _a0

func (*MockRouter) SetMinHop

func (_m *MockRouter) SetMinHop(_a0 uint16)

SetMinHop provides a mock function with given fields: _a0

func (*MockRouter) SetMuxDirectionForApp

func (_m *MockRouter) SetMuxDirectionForApp(_a0 string, _a1 byte) (int, error)

SetMuxDirectionForApp provides a mock function

func (*MockRouter) SetMuxFEC

func (_m *MockRouter) SetMuxFEC(v bool)

SetMuxFEC provides a mock function backed by a plain field: the value carries no behaviour worth asserting, and a Called() would panic in every existing MockRouter test that reaches the setting.

func (*MockRouter) SetMuxMode

func (_m *MockRouter) SetMuxMode(_a0 WeightMode)

SetMuxMode provides a mock function with given fields: _a0

func (*MockRouter) SetupIsTrusted

func (_m *MockRouter) SetupIsTrusted(_a0 cipher.PubKey) bool

SetupIsTrusted provides a mock function with given fields: _a0

func (*MockRouter) UnregisterDatagramPort

func (_m *MockRouter) UnregisterDatagramPort(port routing.Port)

UnregisterDatagramPort provides a mock function with given fields: port

type MuxCounters

type MuxCounters struct {
	// TunnelPromotions counts standby-tunnel promotions to active (a pool
	// tunnel flip) — every MuxEventTunnelPromoted recorded by noteTunnelEvent.
	TunnelPromotions uint64 `json:"tunnel_promotions"`
	// LegRehomesSent/Acked count this visor's OWN outbound leg re-homes
	// (leg_rehome.go rehomeChain): Sent when the request lands on the wire,
	// Acked when the peer's ack completes the in-place move. LegRehomesFailed
	// adds the initiator's own timeout/refusal outcomes to the acceptor's
	// refusals counted below — it does not cover a failure AFTER a peer's ack
	// (adopting the leg locally), which is rarer and separately logged as a
	// MuxEvent. LegRehomesReceived counts an incoming re-home REQUEST this
	// visor accepted or refused (leg_rehome.go acceptRehome).
	LegRehomesSent     uint64 `json:"leg_rehomes_sent"`
	LegRehomesReceived uint64 `json:"leg_rehomes_received"`
	LegRehomesAcked    uint64 `json:"leg_rehomes_acked"`
	LegRehomesFailed   uint64 `json:"leg_rehomes_failed"`
	// LegSplitsSent/Received/Acked/Failed count the REVERSE move
	// (leg_split.go): a leg handed back OUT of a group into a standalone
	// standby group of its own, instead of its transport being closed.
	// Sent/Acked/Failed are this visor's own outbound splits, Received an
	// incoming split request it accepted or refused.
	// LegRehomesForwarded/LegSplitsForwarded count re-home and split control
	// frames this visor RELAYED as an intermediate hop (router_packet.go
	// forwardPacket). They are what makes a transited re-home observable at all:
	// on the initiator a chain through a relay that does not forward the type is
	// indistinguishable from an exit that refused, and the relay itself had no
	// tally of its own. A sent-but-never-acked re-home whose first hop reports 0
	// here died at that hop.
	LegRehomesForwarded uint64 `json:"leg_rehomes_forwarded"`
	LegSplitsForwarded  uint64 `json:"leg_splits_forwarded"`
	// LegControlsInBandSent/LegControlsInBand count the IN-BAND leg control
	// frames (mux_control_frame.go) this visor sent and received: the re-home
	// and split messages carried inside a DataPacket, which every relay
	// forwards whatever it runs. A re-home that is sent but never acked with
	// these rising on both edges is an exit that refused, not a hop that
	// dropped the control frame.
	LegControlsInBandSent uint64 `json:"leg_controls_inband_sent"`
	LegControlsInBand     uint64 `json:"leg_controls_inband_received"`
	LegSplitsSent         uint64 `json:"leg_splits_sent"`
	LegSplitsReceived     uint64 `json:"leg_splits_received"`
	LegSplitsAcked        uint64 `json:"leg_splits_acked"`
	LegSplitsFailed       uint64 `json:"leg_splits_failed"`
	// ForwardFanoutEngaged/Released count noteForwardFanout(on=true/false):
	// the forward direction's fan-out-under-load latch (unidir.go) turning on
	// (MuxEventForwardFanout) or off (MuxEventForwardConfined).
	ForwardFanoutEngaged  uint64 `json:"forward_fanout_engaged"`
	ForwardFanoutReleased uint64 `json:"forward_fanout_released"`
	// AEADFailures counts per-frame AEAD (CapPerFrameNoise) open failures
	// across ALL route groups — the stream RouteGroup's mux path and the
	// datagram group's, which also keeps its own per-group tally. A nonzero
	// value is tampering, a stale duplicate after a rekey, or wire corruption
	// the transport did not catch; the frame is always dropped, never delivered.
	AEADFailures uint64 `json:"aead_failures"`
	// DeliveryCRCFailures counts frames dropped at DELIVERY because their
	// CapDeliveryCRC trailer did not match the CRC32C of (seq ‖ payload) — see
	// delivery_crc.go. Unlike AEADFailures this one is the reassembly check: it
	// catches a reorder/flush defect that would otherwise only surface as an
	// application hash failure. It must be 0 on a healthy mux.
	DeliveryCRCFailures uint64 `json:"delivery_crc_failures"`
}

MuxCounters is a snapshot of the whole-router mux counters, surfaced via `visor state --select mux` (StateSnapshot.MuxCounters) and `proxy mux info`.

func MuxCountersSnapshot

func MuxCountersSnapshot() MuxCounters

MuxCountersSnapshot reads the process-wide mux counters without a router instance. The counters are global (see globalMuxCounters), and the emulator bench asserts the integrity ones cell by cell.

type MuxEvent

type MuxEvent struct {
	At    time.Time `json:"at"`
	Event string    `json:"event"`
	// App is the app name the route group was tagged with at dial time
	// (SetAppName), e.g. "via0281a102". Empty on the accept side.
	App string `json:"app,omitempty"`
	// Desc identifies the route group the event belongs to.
	Desc routing.RouteDescriptorFields `json:"desc"`
	// By names who decided: "local", "remote", "operator", "adaptive"
	// (liveness/self-heal) or "policy" (routing-policy rotation hook).
	By string `json:"by,omitempty"`
	// LegIndex is the leg's index at the moment of the event (-1 for
	// whole-group events); Legs is how many legs the group had AFTER it.
	LegIndex int `json:"leg_index"`
	Legs     int `json:"legs"`
	// TpID/TpType/Remote describe the leg's FIRST hop — the transport this
	// visor owns. Zero for whole-group events.
	TpID   uuid.UUID     `json:"tp_id,omitempty"`
	TpType string        `json:"tp_type,omitempty"`
	Remote cipher.PubKey `json:"remote_pk,omitempty"`
	// Hops is the leg's full forward path when the route group recorded one
	// (recordLegRoute); nil for a leg whose path was never stored.
	Hops []RouteHopInfo `json:"hops,omitempty"`
	// Reason is what decided this event, in the words of the code that did
	// ("operator: mux rm", "transport closed", "liveness: no echo for 3
	// probes", "peer retired the leg", …).
	Reason string `json:"reason,omitempty"`
}

MuxEvent is one entry in the router's bounded history of route-group and mux-leg changes, with the reason each one happened. It exists because the question "who took that leg away" had no answer: a pinned two-hop proxy session's `proxy mux info` went from one leg to none mid-measurement while its first-hop transport stayed open, and neither `visor state` nor diag.transport_events (#4919) recorded anything — the visor log ring is the only other witness and it holds minutes. `visor state --select diag` carries the last MuxEventRingSize of these, and `proxy mux info --json` carries each group's most recent few next to its legs.

type MuxInfo

type MuxInfo struct {
	// Desc identifies the route group.
	Desc routing.RouteDescriptor
	// FarEndPK is the OTHER end of Desc — the peer this group's chains reach,
	// never this visor. The setup node hands EACH edge the descriptor that
	// points AT that edge, so Desc.DstPK() is the LOCAL visor on BOTH sides
	// (which is why LocalAddr is desc.Dst() and RemoteAddr is desc.Src());
	// reading DstPK as the "destination"/"exit" therefore printed this visor's
	// own key as the far end of every dialed group. Consumers render this
	// instead of deriving an end from Desc.
	FarEndPK cipher.PubKey
	// MuxEnabled is false for non-mux'd rg's; the per-leg counters
	// are still populated for the single transport in that case.
	MuxEnabled bool
	// SACKEnabled is true when the peers negotiated SACK retx.
	SACKEnabled bool
	// HOLRetxEnabled is true when the peers negotiated CapHOLRetx (proactive
	// head-of-line retransmit — the frontier-blocking seq is fast-retransmitted on
	// the fastest leg after ~one fast-leg RTT instead of the reactive waits).
	HOLRetxEnabled bool
	// PerFrameNoise is true when both edges negotiated CapPerFrameNoise and the
	// mux is sealing/opening each DATA frame under its own sequence-nonce (the
	// inverse-multiplexer path, network.EncryptConn bypassed). False means the
	// group runs the classic stream-noise wrap.
	PerFrameNoise bool
	// AppName is the app that DIALED this route group — the key its session's
	// shape and move history are held under. Distinct from KnobApp below,
	// which is only ever set when that app has an override of its own.
	AppName string
	// KnobApp names the app whose `route settings --app <name>` override set this
	// group resolved against; empty means it runs the visor-wide values. It is
	// how a paired subject/reference run proves the two clients really did get
	// different knobs.
	KnobApp string
	// Directional is true when unidirectional send selection (CapUniDir) is
	// active on this mux: each direction rides a disjoint leg class instead of
	// both striping every leg. Flipped reports the current direction->leg-class
	// mapping. DEFAULT (Flipped=false): this end sends its FORWARD/upload on the
	// DIRECT (1-hop) leg and its REVERSE/download on the MULTIHOP mux legs.
	// FLIPPED=true: the mapping is swapped (the heavy direction took the mux).
	// With the per-leg Direct flag, a reader can tell which class carries which
	// direction without log-grepping. Both false on a non-directional mux.
	Directional bool
	Flipped     bool
	// FlipPinned is the operator's MANUAL direction pin on a directional group:
	// "auto" (the flip controller is in charge — the default), "default" or
	// "flipped" (the mapping is pinned to that state and the controller is
	// dormant until released via mode auto). Empty on a non-directional mux.
	FlipPinned string
	// Distribution names how the mux spreads outbound packets across its legs
	// (the transportSelector weight mode: "auto", "round-robin", "weighted",
	// "capacity", "latency-adaptive", "sticky:5tuple", "size-threshold",
	// "dscp-priority"). Empty for a non-mux group.
	Distribution string
	// ReorderPending is the number of received packets currently buffered
	// out-of-order (head-of-line-blocking depth); ReorderGapAge is how long the
	// current reorder frontier gap has stayed open (0 when contiguous). A rising
	// pending count with a growing gap age is a stalled/black-holing leg.
	ReorderPending int
	ReorderGapAge  time.Duration
	// WriteSeq is the total DATA frames this mux has emitted outbound — a cheap
	// aggregate send-progress counter across all legs.
	WriteSeq uint32
	// FEC telemetry (see routeMux fec* counters). FECEnabled is true when both
	// peers negotiated CapFEC. FECRepairBytesSent/Recv are cumulative repair-frame
	// bytes — the TRUE FEC overhead, separable from data and retransmit.
	// FECReconstructs counts frontier frames recovered from repair.
	FECEnabled         bool
	FECRepairBytesSent uint64
	FECRepairBytesRecv uint64
	FECReconstructs    uint64
	// Legs is in tps[] order. One entry per active mux leg.
	Legs []MuxLeg
	// Events is this group's most recent leg/group changes with their reasons,
	// oldest first (the last muxEventsPerGroup of the router's ring). It puts
	// the churn next to the legs, so a `proxy mux info --json` that shows a leg
	// missing also shows who took it and why. The full ring is
	// diag.mux_events in `visor state`.
	Events []MuxEvent
	// Recovery is the loss-recovery view (sender retransmit machinery +
	// receiver reorder frontier). Nil for a group with no mux.
	Recovery *MuxRecovery
	// TunnelRole is the DIALING app's label for this route group — "active"
	// (it carries streams) or "standby" (held open and measured, ready to take
	// over). Empty for any group that is not one of a multi-tunnel app's
	// tunnels, and ALWAYS empty on the accepting end: an exit knows how many
	// tunnels a client holds but not which of them are in standby, so read the
	// role on the local end.
	// AgeMS is how long this route group has existed. For a pooled STANDBY
	// tunnel it is its AUDITION age — how long it has been held open, pinged
	// and measured without carrying a stream.
	AgeMS      float64
	TunnelRole string
	// DisjointRoutes is how many routes to this group.s far end share no
	// intermediate and no first hop (router.disjointRouteBound). Set by the
	// router on the snapshots it hands out; 0 when not yet counted.
	DisjointRoutes int
	// LegReserve is a standby group a leg split built: a chain the pool may
	// compose back in as a leg, but not a tunnel the app can promote.
	LegReserve bool
	// Shape is the SESSION's measured multiplexing shape — every ACTIVE
	// tunnel this app holds to this exit with its live leg count, written
	// "<k>x<n>" when the tunnels are the same width and "n1,n2,…" when they
	// are not. "4x1" is pure stream level, "1x4" pure packet level, "2x2"
	// the default. ShapeTarget is the shape the session is being held at and
	// ShapeSource where that target came from: "auto" (tunnel.count tunnels
	// at pool.active_width legs, today's rule) or "mux.shape" when the knob
	// names one. The target is ADVISORY — reported, not yet converged toward
	// (docs/design/mux-shape-axis.md).
	//
	// All three are empty on any group that is not an active tunnel: a
	// standby is the pool rather than the shape, and an accepting visor
	// cannot see which of a client's tunnels are in standby.
	Shape       string
	ShapeTarget string
	ShapeSource string
	// ShapeTunnels is the k the target asks for — the number of ACTIVE tunnels
	// the session is converging to. 0 under mux.shape=auto, where the count is
	// the app's own tunnel.count and nothing here overrides it. It is published
	// so the dialing app can promote toward the shape rather than toward
	// tunnel.count (docs/design/mux-shape-axis.md step 6).
	ShapeTunnels int
	// LastMove is the most recent shape move this session made and MoveCounts
	// the running tally, keyed by the four move names (pool_leg_taken,
	// pool_leg_released, tunnel_promoted, tunnel_parked). They make a shape
	// history readable without walking the event ring. Both nil on a session
	// that has never moved.
	LastMove   *MuxShapeMove
	MoveCounts map[string]uint64
}

MuxInfo is a point-in-time snapshot of one route group's multiplexing state — per-leg byte/packet counters plus the transport identity each leg maps to. Returned by Router.MuxInfo for 'cli proxy mux-info'.

type MuxLeg

type MuxLeg struct {
	LegStats
	TransportID string `json:"transport_id"`
	TpType      string `json:"tp_type"`
	RemotePK    string `json:"remote_pk"`
	// Source names where a leg that is NOT this group's own dial came from —
	// "standby :4, re-homed in place" for a leg the pool arbiter took
	// (pool_arbiter.go). Empty for an ordinary dialed leg.
	Source string `json:"source,omitempty"`
	// LatencyMS is the FIRST-HOP transport RTT in ms (the same value
	// 'tp ls' shows). For a multihop leg this is only the near edge, NOT
	// the whole path — use RouteLatencyMS for the end-to-end route.
	LatencyMS float64 `json:"latency_ms"`
	// RouteLatencyMS is the leg's TRUE end-to-end route latency in ms —
	// the EWMA-smoothed round-trip of the per-leg liveness pong across
	// ALL hops (from legE2ELatency). This is the number a routing policy
	// judges a leg by; it can differ sharply from LatencyMS on a multihop
	// leg. Zero until the first pong lands.
	RouteLatencyMS float64 `json:"route_latency_ms"`
	// AckDelayMS is this leg's own EWMA send→ack delay in ms (0 = no sample):
	// the loaded feedback delay its retransmit threshold is judged by
	// (rackThresholdFor), distinct from the idle RTTs above.
	AckDelayMS float64 `json:"ack_delay_ms"`
	// InflightBytes / WindowBytes: the leg's real unacknowledged bytes and the
	// per-leg send window they are bounded by (0 when not a predictive mode).
	InflightBytes float64 `json:"inflight_bytes"`
	WindowBytes   float64 `json:"window_bytes"`
	// Direct is true when this leg's first hop goes straight to the route
	// group's destination (a 1-hop/direct route); false means the leg is
	// multihop (relayed through one or more intermediates). Lets a viewer
	// tell the direct leg from the multihop legs at a glance.
	Direct bool `json:"direct"`
	// Alive is false once the leg's transport is closed. Standby is true
	// when the leg is a WARM STANDBY: rules kept alive but not selected
	// for sending (see docs/warm_standby_legs_rfc.md). Together they are
	// the leg's gate_state for the per-leg telemetry harness.
	Alive   bool `json:"alive"`
	Standby bool `json:"standby"`
	// Hops is this leg's FULL forward route — every hop from this visor to
	// the destination, with full (untruncated) From/To PKs and per-hop
	// transport type. Per-hop LatencyMS is filled where known (first hop
	// owned; single-intermediate far hop derived from route−transport RTT).
	// Empty when the route path wasn't recorded (legacy/accepted routes).
	Hops []RouteHopInfo `json:"hops,omitempty"`
	// CapacityPriorBps is the pool arbiter's PRIOR throughput estimate for
	// this leg's transport (throughputPrior: the transport's live measured
	// rate if it has one, else its catalog entry) — the number a standby
	// tunnel is ranked by BEFORE it is measured. Distinct from GoodputBps,
	// which is the leg's own live EWMA once it has carried traffic.
	CapacityPriorBps float64 `json:"capacity_prior_bps,omitempty"`
}

MuxLeg pairs the per-leg counters with the transport identity. The transport's own Latency / SentBytes / RecvBytes counters are the *transport-level* totals (across all rg's that share the transport); the LegStats here are *rg-scoped* (only what this rg sent through this leg).

type MuxLegController

type MuxLegController interface {
	SetMuxExplicitWeights(desc routing.RouteDescriptor, weights map[uuid.UUID]float64) (MuxWeightsView, error)
	ClearMuxExplicitWeights(desc routing.RouteDescriptor) (MuxWeightsView, error)
	MuxWeightsFor(desc routing.RouteDescriptor) (MuxWeightsView, error)
	AddMuxRouteByHopsForward(desc routing.RouteDescriptor, fwd, rev []routing.Hop) error
	MuxNegotiatedForApp(appName string) []MuxNegotiated
	// RehomeStandbyLeg moves a STANDBY group's whole built chain into the
	// target group as one more mux leg, in place, with no setup-node dial.
	// Returns ErrRehomeUnsupported when the peer did not negotiate
	// CapLegRehome. See leg_rehome.go.
	RehomeStandbyLeg(target, standby routing.RouteDescriptor) error
}

MuxLegController is the optional router capability the visor asserts for the per-leg operator controls. Implemented by *router.

type MuxNegotiated

type MuxNegotiated struct {
	DstPort uint16 `json:"dst_port"`
	SrcPort uint16 `json:"src_port"`
	Remote  string `json:"remote"`
	// AppName is the dialing app's tag on the group, when it has one.
	AppName string `json:"app_name,omitempty"`
	Legs    int    `json:"legs"`
	// Negotiated capabilities — both ends advertised these.
	MuxEnabled     bool `json:"mux_enabled"`
	SACKEnabled    bool `json:"sack_enabled"`
	HOLRetxEnabled bool `json:"hol_retx_enabled"`
	PerFrameNoise  bool `json:"per_frame_noise"`
	FECEnabled     bool `json:"fec_enabled"`
	Directional    bool `json:"directional"`
	// Scheduler in force, and the explicit weights when it is that mode.
	Distribution    string      `json:"distribution"`
	ExplicitWeights []LegWeight `json:"explicit_weights,omitempty"`
	// Send-window shape in effect (the live router settings this end clamps to).
	EcfMinWindowBytes   int64   `json:"ecf_min_window_bytes"`
	EcfMaxWindowBytes   int64   `json:"ecf_max_window_bytes"`
	EcfWindowMargin     float64 `json:"ecf_window_margin"`
	SendWindowWaitMax   string  `json:"send_window_wait_max"`
	ForwardSpill        bool    `json:"forward_spill"`
	ForwardSwitchMargin float64 `json:"forward_switch_margin"`
	// PerLegWindowBytes is each leg's CURRENT send window, in tps[] order —
	// the negotiated shape as the scheduler is actually applying it.
	PerLegWindowBytes []float64 `json:"per_leg_window_bytes,omitempty"`
}

MuxNegotiated is the per-group view of what the two ends actually agreed on and what the sender is applying — the answer to "which caps are in effect on THIS group, and with which window".

The capability flags are negotiated on the wire (both ends must advertise); the window and margin values are this end's live router settings, which is what the send side is actually clamping to. Reporting them per group rather than only as visor-wide settings is the point: a sweep that moves --ecf-max-window mid-campaign otherwise leaves no record on the group it changed.

type MuxRecovery

type MuxRecovery struct {
	// Sender side. WriteSeq is the next outgoing DATA sequence; RetxHeld /
	// RetxMinSeq / RetxMaxSeq are the unacknowledged window still retransmittable
	// (a peer's stuck frontier BELOW RetxMinSeq can never be healed); RetxSent is
	// retransmit packets put on the wire (summed across legs);
	// RetxSkippedMissing counts resend requests for sequences no longer held;
	// RetxSendErrors counts resends that failed to reach any leg; TLPProbes counts
	// tail-loss probes; SACKsRecv / LastSACKRecvMsAgo / LastSACKRecvContig are the
	// inbound feedback (ms-ago is -1 when no SACK has ever arrived).
	WriteSeq           uint32 `json:"write_seq"`
	RetxHeld           int    `json:"retx_held"`
	RetxMinSeq         uint32 `json:"retx_min_seq"`
	RetxMaxSeq         uint32 `json:"retx_max_seq"`
	RetxSent           uint64 `json:"retx_sent"`
	RetxSkippedMissing uint64 `json:"retx_skipped_missing"`
	RetxReqSACK        uint64 `json:"retx_req_sack"`
	RetxReqHOL         uint64 `json:"retx_req_hol"`
	RetxReqFlush       uint64 `json:"retx_req_flush"`
	// RetxDeferredYoung counts the holes a SACK named that were NOT resent
	// because the frame is still younger than the delay basis of the leg it
	// rode — the duplicate bytes kept off the wire.
	RetxDeferredYoung uint64 `json:"retx_deferred_young"`
	RetxSendErrors    uint64 `json:"retx_send_errors"`
	// SendWindowWaits / SendWindowTimeouts: writers parked because every ready
	// leg was at its per-leg send window, and the parks that gave up after
	// sendWindowWaitMax and sent anyway.
	SendWindowWaits    uint64  `json:"send_window_waits"`
	SendWindowTimeouts uint64  `json:"send_window_timeouts"`
	TLPProbes          uint64  `json:"tlp_probes"`
	SACKsRecv          uint64  `json:"sacks_recv"`
	LastSACKRecvMsAgo  float64 `json:"last_sack_recv_ms_ago"`
	LastSACKRecvContig uint32  `json:"last_sack_recv_contig"`
	// Receiver side. ReorderNextSeq is the frontier this end waits on,
	// ReorderPending how many packets are dammed behind it, GapAgeMS how long it
	// has been stuck. SACKsSent / SACKSendErrors / LastSACKSentMsAgo are the
	// outbound feedback (ms-ago -1 when none ever sent). WedgeTicks is the
	// CURRENT wedge's stall-tick count (0 = not wedged), Wedges how many have
	// cleared on this group, LongestWedgeMS the worst one seen. ReorderDrops
	// counts arrivals the reorder buffer discarded because the gap already held
	// a full window — they are deliberately NOT SACKed, so each one is owed a
	// retransmit.
	ReorderNextSeq    uint32  `json:"reorder_next_seq"`
	ReorderPending    int     `json:"reorder_pending"`
	ReorderDrops      uint64  `json:"reorder_drops"`
	GapAgeMS          float64 `json:"gap_age_ms"`
	SACKsSent         uint64  `json:"sacks_sent"`
	SACKSendErrors    uint64  `json:"sack_send_errors"`
	LastSACKSentMsAgo float64 `json:"last_sack_sent_ms_ago"`
	WedgeTicks        int64   `json:"wedge_ticks"`
	Wedges            uint64  `json:"wedges"`
	LongestWedgeMS    int64   `json:"longest_wedge_ms"`
}

MuxRecovery is a route group's LOSS-RECOVERY state: the sender-side retransmit machinery and the receiver-side reorder frontier, side by side.

It exists because a reorder WEDGE is a two-ended failure with a one-ended witness. Observed live: a pinned two-hop proxy session went dark for two minutes while the EXIT logged "reorder-WEDGE: frontier stuck at seq=56 … sender retransmit not refilling the gap"; our end — the sender — had nothing to show at all, so the two candidate causes were indistinguishable. Either the exit's SACKs never reached us (SACKsRecv flat, LastSACKRecvMsAgo growing) or they reached us naming a sequence the retx buffer had already evicted (RetxSkippedMissing climbing, and RetxMinSeq above the frontier the peer is stuck on). Every field here is a cheap atomic read; nothing is added to a hot path.

type MuxShapeMove

type MuxShapeMove struct {
	Move   string    `json:"move"`
	From   string    `json:"from"`
	To     string    `json:"to"`
	Reason string    `json:"reason"`
	At     time.Time `json:"at"`
}

MuxShapeMove is the last shape move a session made, reported as MuxInfo.LastMove. Move is one of the four mux event names (pool_leg_taken / pool_leg_released / tunnel_promoted / tunnel_parked), From and To are the shapes it moved between, and Reason is the sentence the Info log carried.

type MuxWeightsView

type MuxWeightsView struct {
	DstPort uint16      `json:"dst_port"`
	SrcPort uint16      `json:"src_port"`
	Mode    string      `json:"mode"`
	Weights []LegWeight `json:"weights,omitempty"`
}

MuxWeightsView is the weights state of one route group after (or without) a change: the scheduler mode in force and, when it is the explicit one, the per-leg weights it is spreading by.

type Node

type Node struct {
	// contains filtered or unexported fields
}

Node performs routes setup operations over messaging channel.

func NewNode

func NewNode(conf *SetupConfig) (*Node, error)

NewNode constructs a new SetupNode.

func (*Node) Close

func (sn *Node) Close() error

Close closes the connection pool and underlying dmsg client.

func (*Node) DmsgClient

func (sn *Node) DmsgClient() *dmsg.Client

DmsgClient returns the setup node's DMSG client. This allows other components (e.g. DMSG HTTP health) to share the same client instead of creating a second one with the same PK, which causes "error 306 - no associated listener" when streams route to the wrong client.

func (*Node) InitCascade

func (sn *Node) InitCascade(conf *SetupConfig, tm *transport.Manager)

InitCascade initializes the cascade builder with a transport manager. Must be called after the RSN's transport manager is set up by the caller. This separation exists because the transport manager requires deployment- specific configuration (STCPR listen address, AR client, etc.) that the RSN's core doesn't control.

func (*Node) Pool

func (sn *Node) Pool() *ClientPool

Pool returns the setup node's connection pool.

func (*Node) Serve

func (sn *Node) Serve(ctx context.Context, m setupmetrics.Metrics) error

Serve starts transport listening loop.

type NoiseRouteGroup

type NoiseRouteGroup struct {
	net.Conn
	// contains filtered or unexported fields
}

NoiseRouteGroup is a route group wrapped with noise. Implements net.Conn.

func (*NoiseRouteGroup) BandwidthReceived

func (nrg *NoiseRouteGroup) BandwidthReceived() uint64

BandwidthReceived returns amount of bandwidth received (bytes).

func (*NoiseRouteGroup) BandwidthSent

func (nrg *NoiseRouteGroup) BandwidthSent() uint64

BandwidthSent returns amount of bandwidth sent (bytes).

func (*NoiseRouteGroup) Close

func (nrg *NoiseRouteGroup) Close() error

Close closes the connection, which chains through to close the underlying RouteGroup. The close chain properly stops the RouteGroup's keep-alive loop.

func (*NoiseRouteGroup) DownloadSpeed

func (nrg *NoiseRouteGroup) DownloadSpeed() uint32

DownloadSpeed returns upload speed (bytes/s).

func (*NoiseRouteGroup) GetError

func (nrg *NoiseRouteGroup) GetError() error

GetError gets the close error.

func (*NoiseRouteGroup) IsAlive

func (nrg *NoiseRouteGroup) IsAlive() bool

IsAlive checks if connection is alive.

func (*NoiseRouteGroup) Latency

func (nrg *NoiseRouteGroup) Latency() time.Duration

Latency returns latency till remote (ms).

func (*NoiseRouteGroup) LocalAddr

func (nrg *NoiseRouteGroup) LocalAddr() net.Addr

LocalAddr returns local address.

func (*NoiseRouteGroup) RemoteAddr

func (nrg *NoiseRouteGroup) RemoteAddr() net.Addr

RemoteAddr returns remote address.

func (*NoiseRouteGroup) RouteHopDetails

func (nrg *NoiseRouteGroup) RouteHopDetails() []RouteHopInfo

RouteHopDetails returns detailed information about each hop in the route.

func (*NoiseRouteGroup) RouteHops

func (nrg *NoiseRouteGroup) RouteHops() []cipher.PubKey

RouteHops returns the list of visor public keys that form the route path.

func (*NoiseRouteGroup) SetError

func (nrg *NoiseRouteGroup) SetError(err error)

SetError sets the close error.

func (*NoiseRouteGroup) SetForwardHops

func (nrg *NoiseRouteGroup) SetForwardHops(hops []routing.Hop)

SetForwardHops sets the complete forward route hops.

func (*NoiseRouteGroup) UploadSpeed

func (nrg *NoiseRouteGroup) UploadSpeed() uint32

UploadSpeed returns upload speed (bytes/s).

type RPCGateway

type RPCGateway struct {
	// contains filtered or unexported fields
}

RPCGateway is a RPC interface for router.

func NewRPCGateway

func NewRPCGateway(router Router, mLog *logging.MasterLogger, noTransit bool) *RPCGateway

NewRPCGateway creates a new RPCGateway.

func (*RPCGateway) AddEdgeRules

func (r *RPCGateway) AddEdgeRules(rules routing.EdgeRules, ok *bool) error

AddEdgeRules adds edge rules.

func (*RPCGateway) AddIntermediaryRules

func (r *RPCGateway) AddIntermediaryRules(rules []routing.Rule, ok *bool) error

AddIntermediaryRules adds intermediary rules. This RPC exists only to make this visor a transit hop on someone else's route, so refusing it outright is exactly what routing.no_transit means. Edge rules arrive by AddEdgeRules and are untouched, so routes that start or end here still work.

func (*RPCGateway) DelRules

func (r *RPCGateway) DelRules(routeIDs []routing.RouteID, ok *bool) error

DelRules removes the routing rules identified by routeIDs from this router. The setup-node calls this to tear down rules it installed on intermediary / edge hops when a later step of route-group setup fails, so a partially built route does not orphan rules (each orphan pins its NextTransportID against transport teardown until the ~10-min keepalive GC reaps it). Router.DelRules logs per-id "rule not found" internally and cannot fail, so this is best-effort by contract and always reports ok.

func (*RPCGateway) ReserveIDs

func (r *RPCGateway) ReserveIDs(n uint8, routeIDs *[]routing.RouteID) error

ReserveIDs reserves route IDs.

type RSNRelayCache

type RSNRelayCache struct {
	// contains filtered or unexported fields
}

RSNRelayCache caches relay peer lists received from the RSN. Used by visors to find a transport neighbor that can relay to the RSN.

func NewRSNRelayCache

func NewRSNRelayCache(log *logging.Logger) *RSNRelayCache

NewRSNRelayCache creates an empty relay cache.

func (*RSNRelayCache) FindRelayTransport

func (rc *RSNRelayCache) FindRelayTransport(
	ctx context.Context,
	rsnPK cipher.PubKey,
	tm *transport.Manager,
) (*transport.ManagedTransport, cipher.PubKey, error)

FindRelayTransport finds a transport to a relay peer that can reach the RSN. Returns the transport and relay peer PK, or an error if no relay path exists.

func (*RSNRelayCache) Get

func (rc *RSNRelayCache) Get(rsnPK cipher.PubKey) []cipher.PubKey

Get returns the cached relay peers for an RSN.

func (*RSNRelayCache) Update

func (rc *RSNRelayCache) Update(rsnPK cipher.PubKey, relayPeers []cipher.PubKey)

Update stores or refreshes the relay peer list for an RSN.

type RelayPeersArgs

type RelayPeersArgs struct{}

RelayPeersArgs is the request for the RelayPeers RPC.

type RelayPeersReply

type RelayPeersReply struct {
	Peers []cipher.PubKey `json:"peers"`
}

RelayPeersReply contains the RSN's transport peer PKs so visors can cache them for future relay-based route setup without DMSG.

type ReserveError

type ReserveError struct {
	PK  cipher.PubKey
	Err error
}

ReserveError names the remote whose route-ID reservation failed. The message is byte for byte what the id reserver used to format by hand, so log greps and the setup node's own-view parser keep working; what is new is that the PK is carried structurally, which is what lets the setupmetrics collector attribute the failure to the hop that actually failed instead of to the destination.

Without this the whole error was an untyped fmt.Errorf: failedDialPK found no PK, fell through to "blame the destination", and three reservations that failed at some INTERMEDIATE tripped the destination's breaker — observed live on 2026-09-18 against an exit the source visor held 32 live route groups to.

func (*ReserveError) DialFailedPK

func (e *ReserveError) DialFailedPK() cipher.PubKey

DialFailedPK reports the remote whose reservation failed, through the same anonymous interface setupmetrics probes for on DialError.

func (*ReserveError) Error

func (e *ReserveError) Error() string

Error preserves the legacy "reserve routeID from <pk> failed: <inner>" wording.

func (*ReserveError) Unwrap

func (e *ReserveError) Unwrap() error

Unwrap exposes the inner error for errors.Is / errors.As.

type Role

type Role uint8

Role identifies which direction's subkey this cipher serves. A DatagramRouteGroup holds two ciphers: one for outbound (seal with the direction matching the local end's role at handshake) and one for inbound (open with the peer's direction).

const (
	// RoleInitiator is the side that dialed the route.
	RoleInitiator Role = iota
	// RoleResponder is the side that accepted the route.
	RoleResponder
)

type RotationAction

type RotationAction struct {
	// DropLegs is the list of current-tick leg indices to close.
	// Indices reference the legs slice passed to the hook —
	// they're NOT stable across ticks because the prune path
	// compacts the slice when a leg closes.
	DropLegs []int

	// AddLeg requests one more aux forward leg with ExcludeHops
	// as the disjoint-intermediate filter.
	AddLeg      bool
	ExcludeHops []string

	// DemoteToStandby / PromoteFromStandby move legs between the active set
	// and warm standby WITHOUT teardown: a demoted leg keeps its rules (the
	// keepalive/liveness loops keep them alive) but is no longer selected for
	// sending; promoting clears that instantly, with no route setup. This is
	// the cheap alternative to Drop+re-dial for policy-driven leg moves — see
	// docs/warm_standby_legs_rfc.md. Indices are current-tick leg indices, like
	// DropLegs. Applied before DropLegs (which compacts) so the indices stay
	// valid.
	DemoteToStandby    []int
	PromoteFromStandby []int

	// AddForwardLeg requests one more FORWARD-ONLY aux leg — appended
	// addFwd=true / addRev=false so it adds upstream send capacity without
	// enlarging the reverse/download set. The rotation loop dials it via the
	// forward-only add callback (router.addOneAuxSendLeg). The forward-
	// direction mirror of AddLeg; emitted by the adaptive preset under
	// sustained upload (SentBytes) saturation. ExcludeHops applies to it too.
	AddForwardLeg bool
}

RotationAction is the structured return value from the policy's on_tick hook. The route group's rotation servicePacketLoop fires the hook periodically (cadence configured at dial time) and applies this action: drop the listed leg indices, then optionally add a fresh aux leg whose path avoids ExcludeHops. Zero value = no-op.

type RotationHook

type RotationHook interface {
	OnTick(info DialInfo, legs []LegInfo) RotationAction
}

RotationHook fires periodically per active route group, giving the routing policy a chance to mutate the leg set without re- dialing the whole group. Policies use it to spread bandwidth across the eligible-peer set (drop oldest leg, add new one excluding current hops, repeat each tick).

type RouteGroup

type RouteGroup struct {
	// contains filtered or unexported fields
}

RouteGroup should implement 'io.ReadWriteCloser'. It implements 'net.Conn'.

func NewRouteGroup

func NewRouteGroup(cfg *RouteGroupConfig, rt routing.Table, desc routing.RouteDescriptor, mLoggger *logging.MasterLogger) *RouteGroup

NewRouteGroup creates a new RouteGroup.

func (*RouteGroup) AppName

func (rg *RouteGroup) AppName() string

AppName returns the rg's stored app name (set via SetAppName). Empty when the rg was created without app context (accept-side).

func (*RouteGroup) BandwidthReceived

func (rg *RouteGroup) BandwidthReceived() uint64

BandwidthReceived returns amount of bandwidth received (bytes).

func (*RouteGroup) BandwidthSent

func (rg *RouteGroup) BandwidthSent() uint64

BandwidthSent returns amount of bandwidth sent (bytes).

func (*RouteGroup) Close

func (rg *RouteGroup) Close() error

Close closes a RouteGroup.

func (*RouteGroup) DownloadSpeed

func (rg *RouteGroup) DownloadSpeed() uint32

DownloadSpeed returns download speed (bytes/s).

func (*RouteGroup) GetError

func (rg *RouteGroup) GetError() error

GetError gets the close error.

func (*RouteGroup) Initiator

func (rg *RouteGroup) Initiator() bool

Initiator reports whether this visor dialed the remote end of the route (true) or accepted the route from a setup-node request (false).

func (*RouteGroup) IsAlive

func (rg *RouteGroup) IsAlive() bool

IsAlive checks whether connection is alive.

func (*RouteGroup) Latency

func (rg *RouteGroup) Latency() time.Duration

Latency returns latency till remote (ms).

func (*RouteGroup) LocalAddr

func (rg *RouteGroup) LocalAddr() net.Addr

LocalAddr returns destination address of underlying RouteDescriptor.

func (*RouteGroup) MeasureLatency

func (rg *RouteGroup) MeasureLatency(ctx context.Context, count int) (min, max, avg float64, err error)

MeasureLatency performs multiple ping/pong measurements and returns statistics. It sends 'count' pings, waits for pongs, and calculates min/max/avg latency. Returns the stats and any error. Partial results are returned if some pings fail.

func (*RouteGroup) MuxStats

func (rg *RouteGroup) MuxStats() MuxInfo

MuxStats returns a point-in-time snapshot of the rg's per-leg counters paired with each leg's transport identity.

func (*RouteGroup) Read

func (rg *RouteGroup) Read(p []byte) (n int, err error)

Read reads the next packet payload of a RouteGroup. The Router, via transport.Manager, is responsible for reading incoming packets and pushing it to the appropriate RouteGroup via (*RouteGroup).readCh.

func (*RouteGroup) RemoteAddr

func (rg *RouteGroup) RemoteAddr() net.Addr

RemoteAddr returns source address of underlying RouteDescriptor.

func (*RouteGroup) RouteHopDetails

func (rg *RouteGroup) RouteHopDetails() []RouteHopInfo

RouteHopDetails returns detailed information about each hop in the route, including transport IDs and types.

func (*RouteGroup) RouteHops

func (rg *RouteGroup) RouteHops() []cipher.PubKey

RouteHops returns the list of visor public keys that form the route path. The first element is the first hop from the source, and the last element is the destination visor.

func (*RouteGroup) SetAppName

func (rg *RouteGroup) SetAppName(name string)

SetAppName attaches app_name=<n> as a logrus field on the route group's logger AND records it on the rg for later lookups (mux info by app, etc.). Used by the router-side rg saver so 'cli proxy start --verbose' can scope rg-internal events to the originating app's session. No-op when name is empty.

func (*RouteGroup) SetCloseObserver

func (rg *RouteGroup) SetCloseObserver(fn func(age time.Duration, carried uint64))

SetCloseObserver installs a one-shot callback fired when this group closes, carrying the group's age and the total payload it received. The dialer uses it to record a route that died young without ever moving a byte (see dead_route_cache.go). Safe to call at most once per group; a nil fn clears it.

func (*RouteGroup) SetDeadline

func (rg *RouteGroup) SetDeadline(t time.Time) error

SetDeadline sets both read and write deadlines.

func (*RouteGroup) SetDirectionPin

func (rg *RouteGroup) SetDirectionPin(mode byte) error

SetDirectionPin applies the operator's MANUAL direction pin to this route group and signals it to the peer. mode is routing.DirectionAuto (release — the flip controller resumes on both ends), DirectionPinDefault (initiator sends on the direct leg / download on the multihop mux) or DirectionPinFlipped (the swapped mapping). Errors unless the group is directional (CapUniDir negotiated) — a pin on a symmetric mux would never be read.

The pin is coordinated over the wire (DirectionPacket) because a one-sided pin would DESYNC the ends: both could end up sending on the same leg class. It is sent on the PRIMARY leg (leg 0's rule/transport): the pin addresses the GROUP, not one leg, and leg 0 is the one leg that is never standby — the signal always rides an installed, carrying path. Best-effort: an old peer (no DirectionPacket) ignores the frame, leaving the pin local-only — its controller may then fight the pinned mapping, which is why the caller-facing docs call a pin against an old peer best-effort.

func (*RouteGroup) SetError

func (rg *RouteGroup) SetError(err error)

SetError sets the close error.

func (*RouteGroup) SetForwardHops

func (rg *RouteGroup) SetForwardHops(hops []routing.Hop)

SetForwardHops sets the complete forward route hops. This should be called after route setup to store the full route path.

func (*RouteGroup) SetLegChangeHook

func (rg *RouteGroup) SetLegChangeHook(hook LegChangeHook, info DialInfo)

SetLegChangeHook attaches a leg-change callback to the route group. Called once at construction time (from saveRouteGroupRules) by the dialing side; accept-side route groups are unaffected because they don't run policies.

func (*RouteGroup) SetReadDeadline

func (rg *RouteGroup) SetReadDeadline(t time.Time) error

SetReadDeadline sets read deadline.

func (*RouteGroup) SetRotation

func (rg *RouteGroup) SetRotation(hook RotationHook, applyAdd, applyAddForward func(excludeHops []string), interval time.Duration)

SetRotation attaches the rotation hook + apply-add callback + tick interval. Called once at construction time when the policy's decide_route returned a non-zero RotationIntervalSeconds. The actual rotation goroutine starts via startOffServiceLoops; calling SetRotation after that point is racy and unsupported.

applyAdd is the router-side callback that dials one more aux forward leg with the policy's ExcludeHops as the disjoint- intermediate filter. It runs in the rotation goroutine's own context so a slow setup-node dial doesn't block other route groups' rotation. applyAddForward is the FORWARD-ONLY analog (appendRouteAsymmetric addFwd=true/addRev=false), dialed for a RotationAction.AddForwardLeg; nil disables forward widening.

func (*RouteGroup) SetSelfHeal

func (rg *RouteGroup) SetSelfHeal(applyAdd func(excludeHops []string), target int)

SetSelfHeal wires the background degree-restoration callback for a multiplexed route group. When a leg is pruned and the live degree drops below target, the route group dials replacement aux legs (via applyAdd) without blocking traffic — surviving legs carry the load meanwhile. target is the requested mux degree (legs the group should maintain). A target of 0 or 1 disables self-heal (a single-leg group has nothing to spread to).

func (*RouteGroup) SetTunnelRole

func (rg *RouteGroup) SetTunnelRole(role string)

SetTunnelRole records the dialing app's label for this route group — "active" or "standby". No-op when role is empty, so a route group that is not one of a multi-tunnel app's tunnels keeps an empty role and the field is simply absent from its JSON. Guarded by rg.mu because MuxStats reads it from whichever goroutine is serving `visor state`.

func (*RouteGroup) SetWriteDeadline

func (rg *RouteGroup) SetWriteDeadline(t time.Time) error

SetWriteDeadline sets write deadline.

func (*RouteGroup) TunnelRole

func (rg *RouteGroup) TunnelRole() string

TunnelRole returns the dialing app's label for this route group ("active" / "standby"), or "" when it has none.

func (*RouteGroup) UploadSpeed

func (rg *RouteGroup) UploadSpeed() uint32

UploadSpeed returns upload speed (bytes/s).

func (*RouteGroup) Write

func (rg *RouteGroup) Write(p []byte) (n int, err error)

Write writes payload to a RouteGroup, splitting it into as many data frames as the wire format needs. A routing packet's payload length is a uint16, so one frame carries at most maxWritePayload bytes of application data. Before per-frame noise the stream-noise wrapper (EncryptConn) sliced every write into ≤4 KiB frames, so this path never saw a large payload; with per-frame noise the group receives the application's raw writes (net/rpc hands a >64 KiB gob reply down in a single call), and a payload over the frame limit has to be segmented here rather than rejected — the rejection burned a sequence number, and the peer's no-skip reorder buffer then waited on that hole forever (#4484 stage 2: hypervisor RPC over skynet stalled after the first 4 KiB of a Summary reply). Each segment goes through leg selection on its own, so a mux stripes them.

type RouteGroupConfig

type RouteGroupConfig struct {
	ReadChBufSize     int
	KeepAliveInterval time.Duration
	// FEC advertises CapFEC in the mux handshake so repair frames are striped
	// alongside data. Off by default (router Config.MuxFEC).
	FEC bool
}

RouteGroupConfig configures RouteGroup.

func DefaultRouteGroupConfig

func DefaultRouteGroupConfig() *RouteGroupConfig

DefaultRouteGroupConfig returns default RouteGroup config. Used by default if config is nil.

type RouteGroupDialer

type RouteGroupDialer interface {
	Dial(
		ctx context.Context,
		log *logging.Logger,
		dmsgC *dmsg.Client,
		setupNodes []cipher.PubKey,
		req routing.BidirectionalRoute,
	) (routing.EdgeRules, cipher.PubKey, error) // Returns rules and the connected setup node PK
}

RouteGroupDialer is an interface for RouteGroup dialers.

It lives in this net/http-free, untagged file (separate from the route-setup-side implementations in wrappers.go, which are //go:build !tinygo) so the Config field that references it compiles under the TinyGo js/wasm target. On TinyGo, NewSetupNodeDialer() returns a stub whose Dial reports that route dialing is unavailable (an edge-only browser visor receives routes but does not initiate route setup).

func NewSetupNodeDialer

func NewSetupNodeDialer() RouteGroupDialer

NewSetupNodeDialer returns a wrapper for (*Client).DialRouteGroup.

func NewSetupNodeDialerFull

func NewSetupNodeDialerFull(embedded EmbeddedSetupNode, relayCache *RSNRelayCache, tm *transport.Manager, forceLegacy bool) RouteGroupDialer

NewSetupNodeDialerFull returns a dialer with all capabilities: embedded RSN, transport relay, DMSG fallback, and source-driven cascade.

When forceLegacy is true the source-driven cascade is left nil, so every dial uses the legacy path: the setup node dials each hop directly over dmsg to reserve IDs and install rules. This is the default for now (the cascade has a multihop data-plane bug); operators opt back into the cascade via Routing.enable_cascade_route_setup once it is fixed.

func NewSetupNodeDialerWithEmbedded

func NewSetupNodeDialerWithEmbedded(embedded EmbeddedSetupNode) RouteGroupDialer

NewSetupNodeDialerWithEmbedded returns a dialer that uses the embedded setup-node when available, falling back to remote setup-nodes.

type RouteGroupQueue

type RouteGroupQueue struct {
	Desc     string `json:"desc"`
	App      string `json:"app,omitempty"`
	Queue    int    `json:"queue"`
	Capacity int    `json:"capacity"`
	// IntakeQueue/IntakeCapacity are the group's intake queue (see
	// RouteGroup.inCh), and IntakeDrops counts packets the router dropped
	// because it was full.
	IntakeQueue    int    `json:"intake_queue"`
	IntakeCapacity int    `json:"intake_capacity"`
	IntakeDrops    uint64 `json:"intake_drops"`
}

RouteGroupQueue is one route group's inbound queue depths.

type RouteHopInfo

type RouteHopInfo struct {
	TpID   string `json:"tp_id"`   // Transport ID
	From   string `json:"from"`    // Source public key (full, never truncated)
	To     string `json:"to"`      // Destination public key (full, never truncated)
	TpType string `json:"tp_type"` // Transport type (stcpr, sudph, dmsg)
	// LatencyMS is this hop's transport RTT in ms, when known. The first
	// hop is owned locally (tp.GetLatency). For a single-intermediate leg
	// the far hop is derived (route RTT − first-hop RTT). Deeper hops are
	// 0 here until sourced from TPD / the setup node's per-transport data.
	LatencyMS float64 `json:"latency_ms,omitempty"`
}

RouteHopInfo contains detailed information about a single hop in a route.

type RouteSelectingHook

type RouteSelectingHook interface {
	DialHook
	// SelectRoute is invoked synchronously after the route-finder
	// returns candidates and before route setup. forward and
	// reverse are the per-direction candidate lists (the same
	// path returned for both directions when the route-finder
	// query was symmetric). The hook may return Chosen / ReverseChosen
	// indices (into the respective lists) or -1 to defer per
	// direction. Drop=true refuses the dial with ErrDialPolicyDropped.
	SelectRoute(ctx context.Context, info DialInfo, forward, reverse []CandidateInfo) (RouteSelection, error)
}

RouteSelectingHook is an optional secondary interface that a DialHook can implement to influence route selection AFTER the route-finder has returned its candidates. The router type- asserts for this interface; implementers that don't need candidate filtering simply omit SelectRoute and continue using the simpler BeforeDial path.

Both hook points fire per dial:

  • BeforeDial (sync, before route-finder) — adjusts opts
  • SelectRoute (sync, after route-finder) — picks a route

type RouteSelection

type RouteSelection struct {
	// Chosen is the forward-direction index into the forward
	// candidates slice passed to SelectRoute, or -1 to defer.
	Chosen int
	// ReverseChosen is the reverse-direction index into the
	// reverse candidates slice, or -1 to defer (router uses its
	// built-in pickBestDirection latency rank for reverse).
	// Independent of Chosen — a script can override forward but
	// defer reverse, or vice versa.
	ReverseChosen int
	Drop          bool
	Distribution  DistributionConfig
}

RouteSelection is the SelectRoute return value. Chosen is an index into the candidates slice passed in; -1 means "no preference, fall back to the router's built-in disjoint-path pick." Drop=true overrides everything and refuses the dial.

Distribution lets a SelectRoute hook reconsider the per-packet distribution descriptor after seeing the chosen candidate's transport-kinds / latency / hops_geo. The hook can branch on the candidate it picked and return a tailored distribution (e.g. "if route has stcpr+sudph legs: weighted 3,1 else: auto"). Mode == DistributionUnset means "no override at SelectRoute time" — falls back to whatever BeforeDial set on DialOptions.

type RouteSetupHook

type RouteSetupHook func(cipher.PubKey, *transport.Manager) error

RouteSetupHook is an alias for a function that takes remote public key and a reference to transport manager in order to setup i.e: 1. If a transport of any type to the remote already exists, continue with route creation over it. 2. Otherwise, if the remote advertises stcpr or sudph, establish that transport and continue. 3. Otherwise try the remaining direct types in preference order, and a dmsg transport last.

type RouteSourceStats

type RouteSourceStats struct {
	// Requests is fetchBestRoutes calls; DirectHops is how many were served
	// by a transport this visor already holds.
	Requests   int64 `json:"requests"`
	DirectHops int64 `json:"direct_hops"`
	// OracleAttempts is 2-hop calculations from the destination's own list
	// through the setup node; OracleRoutes is how many found a route.
	OracleAttempts int64 `json:"oracle_attempts"`
	OracleRoutes   int64 `json:"oracle_routes"`
	// LocalAttached is routes built from the local graph before the route
	// finder was asked (Config.PreferLocalRouteTo): each one is a route-finder
	// query that did not happen.
	LocalAttached int64 `json:"local_attached"`
	// RouteFinderQueries is route-finder requests made (one per attempt).
	RouteFinderQueries int64 `json:"route_finder_queries"`
	// LocalFallback is routes built locally after the route finder missed or
	// ran out of retries.
	LocalFallback int64 `json:"local_fallback"`
	// DirectSetups is one-hop routes set up with the peer, without a setup node.
	DirectSetups int64 `json:"direct_setups"`
	// DirectSetupFallbacks is one-hop routes the peer could not set up
	// directly, sent to a setup node instead.
	DirectSetupFallbacks int64 `json:"direct_setup_fallbacks"`
	// ListAttempts is 3-hop calculations from other visors' own transport
	// lists; ListRoutes is how many found a route. The ListNo* counters say
	// why the rest did not: no usable first hop, no list from the destination,
	// no list from any neighbor, or no path through the lists that came back.
	ListAttempts       int64 `json:"list_attempts"`
	ListRoutes         int64 `json:"list_routes"`
	ListNoFirstHop     int64 `json:"list_no_first_hop"`
	ListNoDstList      int64 `json:"list_no_dst_list"`
	ListNoNeighborList int64 `json:"list_no_neighbor_list"`
	ListNoPath         int64 `json:"list_no_path"`
	// ListExcluded is list routes found but every candidate was a held or
	// recently dead first hop.
	ListExcluded int64 `json:"list_excluded"`
	// ListFetches is neighbor lists asked for; ListFetchFails is how many
	// did not come back.
	ListFetches    int64 `json:"list_fetches"`
	ListFetchFails int64 `json:"list_fetch_fails"`
}

RouteSourceStats counts fetchBestRoutes outcomes since the router started.

type RouteStatus

type RouteStatus struct {
	// Connection identity
	LocalPK    cipher.PubKey `json:"local_pk"`
	RemotePK   cipher.PubKey `json:"remote_pk"`
	LocalPort  routing.Port  `json:"local_port"`
	RemotePort routing.Port  `json:"remote_port"`

	// Initiator is true when this visor dialed the route (called
	// router.DialRoutes). False when this visor accepted the route
	// from a remote setup-node request.
	Initiator bool `json:"initiator"`

	// Status
	Alive   bool          `json:"alive"`
	Latency time.Duration `json:"latency_ns"`

	// Traffic stats
	BandwidthSent     uint64 `json:"bandwidth_sent"`
	BandwidthReceived uint64 `json:"bandwidth_received"`
	UploadSpeed       uint32 `json:"upload_speed"`
	DownloadSpeed     uint32 `json:"download_speed"`

	// Mux info
	MuxEnabled bool             `json:"mux_enabled"`
	Transports []RouteTransport `json:"transports"`

	// Hops is the full forward path for this route group, populated from
	// rg.forwardHops. Empty if the path wasn't recorded (older routes or
	// the fallback reconstruction).
	Hops []RouteHopInfo `json:"hops,omitempty"`
}

RouteStatus contains detailed status of a single active route connection, including all mux transports and live traffic statistics.

type RouteTransport

type RouteTransport struct {
	ID        uuid.UUID       `json:"id"`
	Type      string          `json:"type"`
	RemotePK  cipher.PubKey   `json:"remote_pk"`
	Latency   float64         `json:"latency_ms"`
	FwdRuleID routing.RouteID `json:"fwd_rule_id"`
	// RvsRuleID is the matching reverse (Consume) rule on this side.
	// Paired by index with FwdRuleID.
	RvsRuleID routing.RouteID `json:"rvs_rule_id"`
}

RouteTransport describes one transport within a muxed route.

type Router

type Router interface {
	io.Closer

	// DialRoutes dials to a given visor of 'rPK'.
	// 'lPort'/'rPort' specifies the local/remote ports respectively.
	// A nil 'opts' input results in a value of '1' for all DialOptions fields.
	// A single call to DialRoutes should perform the following:
	// - Find routes via RouteFinder (in one call).
	// - Setup routes via SetupNode (in one call).
	// - Save to routing.Table and internal RouteGroup map.
	// - Return RouteGroup if successful.
	DialRoutes(ctx context.Context, rPK cipher.PubKey, lPort, rPort routing.Port, opts *DialOptions) (net.Conn, error)
	PingRoute(ctx context.Context, rPK cipher.PubKey, lPort, rPort routing.Port, opts *DialOptions) (net.Conn, error)

	// DialRoutesDatagram dials a faithful-UDP (#2607) route. It performs a
	// normal DialRoutes (forcing opts.Datagram=true so the reliable route's
	// Noise handshake also keys a DatagramRouteGroup sibling), then returns
	// both the reliable conn (which the caller must keep alive — closing it
	// tears down the route and the sibling) and the datagram sibling. Errors
	// if the peer did not also register datagram intent for the port (no
	// sibling registered).
	DialRoutesDatagram(ctx context.Context, rPK cipher.PubKey, lPort, rPort routing.Port, opts *DialOptions) (net.Conn, *DatagramRouteGroup, error)

	// RegisterDatagramPort / UnregisterDatagramPort mark a local port as
	// faithful-UDP-capable (#2607). The accept side builds a datagram
	// sibling only for routes whose local port is registered.
	RegisterDatagramPort(port routing.Port)
	UnregisterDatagramPort(port routing.Port)

	// AcceptDatagram blocks until an accept-side datagram sibling is
	// available (or ctx/visor is done), returning it and the local port
	// it targets. Drained by the forwarded_ports.udp server loop.
	AcceptDatagram(ctx context.Context) (*DatagramRouteGroup, routing.Port, error)

	// AcceptRoutes should block until we receive an AddRules packet from SetupNode
	// that contains ConsumeRule(s) or ForwardRule(s).
	// Then the following should happen:
	// - Save to routing.Table and internal RouteGroup map.
	// - Return the RoutingGroup.
	AcceptRoutes(context.Context) (net.Conn, error)
	SaveRoutingRules(rules ...routing.Rule) error
	ReserveKeys(n int) ([]routing.RouteID, error)
	IntroduceRules(rules routing.EdgeRules) error
	Serve(context.Context) error
	SetupIsTrusted(cipher.PubKey) bool
	SetMinHop(uint16)

	// EffectiveMinHops resolves a dial's min-hops constraint from the
	// per-dial options and the visor-global setting together. Exposed
	// because callers OUTSIDE the router take dial shortcuts that bypass
	// route setup — appnet's direct dial above all — and cannot honor the
	// operator's constraint without being able to ask what it is.
	EffectiveMinHops(opts *DialOptions) uint16
	SetExistingTPOnly(bool)
	SetForceLocalRoutes(bool)
	// SetDstTransportOracle wires the RSN-oracle 2-hop route path's
	// destination-transport oracle (see rsn_oracle_routes.go). nil leaves the
	// path inert.
	SetDstTransportOracle(DstTransportOracle)
	SetMuxMode(WeightMode)
	// SetMuxFEC / GetMuxFEC mirror Config.MuxFEC at runtime; the setting is
	// read when a mux route group is BUILT, so it reaches new groups only.
	SetMuxFEC(bool)
	GetMuxFEC() bool
	GetExistingTPOnly() bool
	GetForceLocalRoutes() bool
	GetLastRouteCalcTime() time.Duration
	ActiveRouteStatuses() []RouteStatus
	RoutingTableStats() routing.RoutingTableStats
	AddMuxRouteByHops(desc routing.RouteDescriptor, fwd, rev []routing.Hop) error
	GrowMuxRoute(desc routing.RouteDescriptor, target, minHops int) (int, error)
	// GrowMuxFromPool grows the tunnel route group this visor dialed from
	// localPort by up to legs additional mux legs, taking the plans from the
	// app's STANDBY tunnels to the same exit (already ranked, first-hop
	// transport already up) and falling back to GrowMuxRoute's route-finder
	// path for whatever the pool cannot cover. See pool_legs.go.
	GrowMuxFromPool(localPort routing.Port, legs, minHops int) (int, error)
	RemoveMuxRouteByTransport(desc routing.RouteDescriptor, tpID uuid.UUID) error

	// SetMuxDirectionForApp applies a manual unidirectional direction pin
	// (routing.DirectionAuto / DirectionPinDefault / DirectionPinFlipped) to
	// EVERY active directional route group tagged with the named app (a proxy
	// session can hold several concurrent rg's). Returns how many groups the
	// pin was applied to; errors when the app has no active directional group.
	SetMuxDirectionForApp(appName string, mode byte) (int, error)

	// RouteGroupHops returns the stored forward route hops for the route group
	// matching the given descriptor (or its inversion). Returns nil if no
	// matching route group is found.
	RouteGroupHops(desc routing.RouteDescriptor) []RouteHopInfo

	// DialHook returns the configured routing-policy hook (nil
	// if none configured). Visor wiring uses this to bridge the
	// policy into non-router dial paths like VStreamMux's direct
	// dial — the policy script's BeforeDial fires there too with
	// ctx.is_direct_dial=true.
	DialHook() DialHook

	// RouteGroupMuxInfo returns a snapshot of per-leg mux state for
	// the rg matching desc (or its inversion). Returns false when no
	// matching active rg is found. Used by 'cli proxy mux-info' to
	// surface per-leg bandwidth + latency for diagnostic purposes.
	RouteGroupMuxInfo(desc routing.RouteDescriptor) (MuxInfo, bool)

	// RouteGroupMuxInfoForApp returns mux snapshots for every active
	// rg whose source port maps (via AppLookup) to the named app.
	// One app may have multiple rg's at once (e.g. each SOCKS5
	// client connection to skysocks-client gets its own rg). Returns
	// an empty slice if no rg's are active for the app.
	RouteGroupMuxInfoForApp(appName string) []MuxInfo

	// RouteGroupMuxInfoAll returns mux snapshots for EVERY active
	// route group, regardless of the app it is tagged with. Powers the
	// whole-runtime `cli visor state` view where the per-leg mux shape
	// of all route groups (not just one app's) is wanted at once.
	RouteGroupMuxInfoAll() []MuxInfo

	// CloseRouteGroupsForApp closes and de-registers every route group
	// tagged with appName — the app's own tunnels plus the sibling groups
	// a multi-tunnel dial (--tunnels N) created alongside them. Returns
	// the number of groups closed. Called when the app stops, so a group
	// the app will never read from again does not keep refreshing its
	// rules by keep-alive and does not show up in `proxy mux info` (or
	// the --route reconcile count) for a stopped app.
	CloseRouteGroupsForApp(appName string) int

	// MuxEvents returns the bounded history of route-group and mux-leg
	// changes — each add, removal, park, re-home and group close with the
	// reason the code gave and who initiated it. This is where to look when
	// a leg that was in `proxy mux info` is gone: the visor log ring holds
	// minutes, this holds the events.
	MuxEvents() []MuxEvent

	// MuxCounters returns the whole-router cumulative counters (tunnel
	// promotions, leg re-homes sent/received/acked/failed, forward fan-out
	// engage/release) that the bounded MuxEvents ring above does not retain
	// once it wraps.
	MuxCounters() MuxCounters

	// NoteTunnelEvent records an event the APP that holds a tunnel decided —
	// a standby tunnel promoted into the active set, an active one parked, a
	// dead one retired — on the route group dialed from localPort, and
	// updates that group's TunnelRole when role is non-empty.
	//
	// It exists because the two halves of the answer live in different
	// processes. Only the app knows which of its tunnels carries streams;
	// only the router knows which transport and route the tunnel leaves over,
	// and only the router owns the event ring `visor state --select diag`
	// reads. The app names the tunnel by the local port it dialed from — the
	// one identifier it is handed at dial time (DialResp.LocalPort) — and the
	// router fills in the rest.
	//
	// Reports whether a route group with that local port was found. Both
	// sides of the descriptor are tried, so it works for a group keyed by
	// either the dialing or the mirrored descriptor.
	NoteTunnelEvent(localPort routing.Port, event, reason, role string) bool

	// Routing table related methods
	RoutesCount() int
	Rules() []routing.Rule
	Rule(routing.RouteID) (routing.Rule, error)
	SaveRule(routing.Rule) error
	DelRules([]routing.RouteID)
}

Router is responsible for creating and keeping track of routes. Internally, it uses the routing table, route finder client and setup client.

func New

func New(dmsgC *dmsg.Client, config *Config, routeSetupHooks []RouteSetupHook) (Router, error)

New constructs a new Router.

type RulesMap

type RulesMap map[string][]routing.Rule

RulesMap associates a slice of rules to a visor's string of `public key:port“.

func GenerateRules

func GenerateRules(idR IDReserver, routes []routing.Route) (fwdRules, revRules, interRules RulesMap, err error)

GenerateRules generates rules for given forward and reverse routes. The outputs are as follows: - maps that relate slices of forward, consume and intermediary routing rules to a given visor's public key. - an error (if any).

func (RulesMap) String

func (rm RulesMap) String() string

String implements fmt.Stringer

type SelfHealTargeter

type SelfHealTargeter interface {
	SelfHealTarget() (target int, ok bool)
}

SelfHealTargeter is an OPTIONAL interface a RotationHook may implement to override the route group's self-heal degree at runtime. A group's self-heal target is otherwise fixed at dial time (SetSelfHeal), but an adaptive preset's pool size is a LIVE tunable (mux width / warm-standby reserve, retuned over the mux-control RPC). A hook that implements this pushes the CURRENT target on every tick, so lowering the pool at runtime actually re- caps the self-heal instead of re-dialing back toward the stale dial-time value. ok=false means "leave the dial-time target unchanged" (a non-adaptive preset, whose target is a fixed policy width).

type SetupClient

type SetupClient struct {
	// contains filtered or unexported fields
}

SetupClient is an RPC client for setup node.

func NewSetupClient

func NewSetupClient(ctx context.Context, log *logging.Logger, dmsgC *dmsg.Client, setupNodes []cipher.PubKey) (*SetupClient, error)

NewSetupClient creates a new SetupClient.

func (*SetupClient) Capabilities

func (c *SetupClient) Capabilities(ctx context.Context) ([]string, error)

Capabilities asks the setup node which request shapes it understands, and caches the answer for the life of this client (one client is one connection, and a node's capabilities do not change under a live connection).

A setup node that predates the Capabilities RPC answers with net/rpc's "can't find method"; that is not an error here, it is the answer — an empty capability list, meaning only the original DialRouteGroup. This is what lets the batched form ship without a flag on either side.

func (*SetupClient) Close

func (c *SetupClient) Close() error

Close closes a Client.

func (*SetupClient) ConnectedNode

func (c *SetupClient) ConnectedNode() cipher.PubKey

ConnectedNode returns the public key of the setup node that was successfully connected.

func (*SetupClient) DialRouteGroup

DialRouteGroup generates rules for routes from a visor and sends them to visors.

func (*SetupClient) DialRouteGroupBatch

DialRouteGroupBatch sets up every route in the batch in ONE request. Only call it when Capabilities advertised CapBatchRouteSetup.

A nil error does NOT mean every route installed: the reply carries a result per route and partial success is the normal outcome of a batch whose members leave over different intermediates.

func (*SetupClient) FetchRelayPeers

func (c *SetupClient) FetchRelayPeers(ctx context.Context) ([]cipher.PubKey, error)

FetchRelayPeers queries the RSN for its transport peers. The visor caches these to enable relay-based route setup without DMSG.

func (*SetupClient) RPCClient

func (c *SetupClient) RPCClient() *rpc.Client

RPCClient returns the underlying net/rpc client so the source-driven cascade orchestrator can issue the CascadeSign* RPCs over the same DMSG connection used for the legacy DialRouteGroup path.

func (*SetupClient) SignTransportQuery

func (c *SetupClient) SignTransportQuery(ctx context.Context, src, dst cipher.PubKey) (*TransportQuery, error)

SignTransportQuery asks the RSN to sign a transport-query capability targeting dst on behalf of src (see the RSN-oracle 2-hop route path). The signed query is carried by the source to dst, which verifies it against its trusted-RSN allowlist before returning its transport list.

type SetupConfig

type SetupConfig struct {
	PK                 cipher.PubKey    `json:"public_key"`
	SK                 cipher.SecKey    `json:"secret_key"`
	Dmsg               dmsgc.DmsgConfig `json:"dmsg"`
	TransportDiscovery string           `json:"transport_discovery"`
	LogLevel           string           `json:"log_level"`

	// Cascade configures the cascade route setup protocol.
	// When set, the RSN attempts to install routing rules via transport-level
	// cascade (route ID 0) before falling back to DMSG-based setup.
	Cascade *CascadeConfig `json:"cascade,omitempty"`

	// Transport configures the RSN's own transport manager.
	// When set, the RSN creates STCPR transports with label "setup" and
	// uses them for cascade route setup.
	Transport *SetupTransportConfig `json:"transport,omitempty"`
}

SetupConfig defines configuration parameters for setup Node.

type SetupPathStats

type SetupPathStats struct {
	// Batched is how many routes left inside a batched setup request.
	Batched uint64 `json:"batched"`
	// Singles is how many left on their own because no sibling dial to the
	// same exit was in the collection window.
	Singles uint64 `json:"singles"`
	// Fallback is how many fell back to a single request because the setup
	// node did not advertise batch-route-setup.
	Fallback uint64 `json:"fallback"`

	// OracleQueries is how many signed destination-transport queries were
	// actually issued, OracleShared how many dials rode one another's query,
	// and OracleHits how many read a cached candidate set outright. A fill of
	// N tunnels should show ONE query and N-1 shared/hit.
	OracleQueries uint64 `json:"oracle_queries"`
	OracleShared  uint64 `json:"oracle_shared"`
	OracleHits    uint64 `json:"oracle_hits"`
	// OracleClaimed is how many dials were handed a candidate no sibling held;
	// OracleCrowded how many found every candidate claimed and fell back to the
	// best one. A healthy fill is all claimed.
	OracleClaimed uint64 `json:"oracle_claimed"`
	OracleCrowded uint64 `json:"oracle_crowded"`
	// OracleExits / OracleLegs are the cache's current size: exits with a live
	// candidate set, and total candidates across them.
	OracleExits int `json:"oracle_exits"`
	OracleLegs  int `json:"oracle_legs"`

	// WarmHits / WarmMisses / WarmStored are the shared plan pool's counters —
	// the aux-leg side of the same planning work.
	WarmHits   uint64 `json:"warm_hits"`
	WarmMisses uint64 `json:"warm_misses"`
	WarmStored uint64 `json:"warm_stored"`

	// Cascade outcomes, split by whether the request reached the setup node
	// over dmsg or over a transport. Legacy is a DialRouteGroup, where the
	// setup node installs the rules on each hop itself.
	CascadeOKDmsg          uint64 `json:"cascade_ok_dmsg"`
	CascadeOKTransport     uint64 `json:"cascade_ok_transport"`
	CascadeFailedDmsg      uint64 `json:"cascade_failed_dmsg"`
	CascadeFailedTransport uint64 `json:"cascade_failed_transport"`
	LegacyDmsg             uint64 `json:"legacy_dmsg"`
	LegacyTransport        uint64 `json:"legacy_transport"`
}

SetupPathStats is the visor-side companion to the setup node's requests_by_kind: how this visor's own route setups were issued.

type SetupRPCGateway

type SetupRPCGateway struct {
	Metrics setupmetrics.Metrics
	Ctx     context.Context
	Conn    net.Conn
	ReqPK   cipher.PubKey
	Dialer  network.Dialer
	Pool    *ClientPool     // optional: reuse connections across requests
	Cascade *CascadeBuilder // optional: cascade route setup
	Timeout time.Duration
}

SetupRPCGateway is a RPC interface for setup node.

func (*SetupRPCGateway) Capabilities

func (g *SetupRPCGateway) Capabilities(_ *CapabilitiesArgs, reply *CapabilitiesReply) error

Capabilities is the handshake a client uses before sending a newer request shape. A setup node that predates this method answers with net/rpc's "can't find method" error, which the client reads as "no capabilities beyond the original DialRouteGroup" — so the negotiation needs no version number and no flag.

func (*SetupRPCGateway) CascadeSignInstall

func (g *SetupRPCGateway) CascadeSignInstall(args *CascadeSignInstallArgs, reply *CascadeSignInstallReply) error

CascadeSignInstall is the RSN-side half of the cascade install phase. It reconstructs the IDReserver from the source-collected route IDs, recomputes the routing rules deterministically (GenerateRules), and builds+signs the nested per-hop install cascades. It returns the install bytes for the source to inject, plus the initiating EdgeRules.

func (*SetupRPCGateway) CascadeSignReserve

func (g *SetupRPCGateway) CascadeSignReserve(args *CascadeSignReserveArgs, reply *CascadeSignReserveReply) error

CascadeSignReserve is the RSN-side half of the cascade reserve phase. The RSN is a pure signing oracle: it builds and signs the nested per-hop reserve cascades for the bidirectional route and returns the bytes plus the session IDs. It never dials hops or sends anything itself.

func (*SetupRPCGateway) DialRouteGroup

func (g *SetupRPCGateway) DialRouteGroup(route routing.BidirectionalRoute, rules *routing.EdgeRules) (err error)

DialRouteGroup dials RouteGroups for route and rules.

func (*SetupRPCGateway) DialRouteGroupBatch

DialRouteGroupBatch sets up every route in the batch in ONE request, with the per-hop work coalesced: one id reservation per distinct hop covering every route that traverses it, one intermediary-rule install per distinct hop carrying that hop's rules for every route.

Partial success is normal and is NOT an RPC error: a member whose intermediate is unreachable comes back with a per-route Error while its siblings come back installed. An error return means the batch as a whole was refused (malformed, or oversized).

func (*SetupRPCGateway) HealthCheck

func (g *SetupRPCGateway) HealthCheck(_ *HealthCheckArgs, reply *HealthCheckReply) error

HealthCheck to test if the setup node is responsive.

func (*SetupRPCGateway) RelayPeers

func (g *SetupRPCGateway) RelayPeers(_ *RelayPeersArgs, reply *RelayPeersReply) error

RelayPeers returns the PKs of visors that have direct transports to this RSN. Visors call this to populate their relay cache so subsequent route setup requests can be relayed through these peers instead of using DMSG.

func (*SetupRPCGateway) SignTransportQuery

func (g *SetupRPCGateway) SignTransportQuery(args *SignTransportQueryArgs, reply *SignTransportQueryReply) error

SignTransportQuery is the RSN-side signing endpoint for the RSN-oracle 2-hop route path. It mints a nonce, signs a TransportQuery targeting args.TargetPK on behalf of args.RequesterPK, and returns it. Reuses the RSN keypair the cascade builder already holds (g.Cascade.rsnPK / rsnSK), so the same trusted-RSN authorization that gates cascade route setup gates the query.

type SetupTransportConfig

type SetupTransportConfig struct {
	// STCPRAddr is the listen address for STCPR (e.g. ":7780").
	STCPRAddr string `json:"stcpr_addr,omitempty"`
	// AddressResolver is the AR URL. Empty = no AR registration (RSN-only outbound).
	AddressResolver string `json:"address_resolver,omitempty"`
	// MaxTransports limits the number of transports the RSN maintains.
	MaxTransports int `json:"max_transports,omitempty"`
	// DefaultLabel is the transport label (default: "setup").
	DefaultLabel string `json:"label,omitempty"`
}

SetupTransportConfig configures transport connectivity for the RSN.

type Shape

type Shape struct {
	// Legs is per-tunnel, widest first, so two sessions holding the same
	// multiset of tunnels print and compare the same.
	Legs []int
}

Shape is a session's multiplexing shape: one entry per ACTIVE tunnel, holding that tunnel's leg count. The zero Shape is "no active tunnel".

func (Shape) Chains

func (s Shape) Chains() int

Chains is Σ n_i — the chains the shape spends on tunnels. The session's full chain budget is this plus its standby pool, and every shape MOVE conserves that total.

func (Shape) Equal

func (s Shape) Equal(o Shape) bool

Equal compares two shapes as multisets (newShape has already ordered them).

func (Shape) String

func (s Shape) String() string

String renders the shape the way parseShape takes it back: "<k>x<n>" when every tunnel is the same width, "n1,n2,…" otherwise, empty when there is no active tunnel.

func (Shape) Tunnels

func (s Shape) Tunnels() int

Tunnels is k.

type SignTransportQueryArgs

type SignTransportQueryArgs struct {
	RequesterPK cipher.PubKey // source S making the request
	TargetPK    cipher.PubKey // destination D whose transports are requested
}

SignTransportQueryArgs is the request for SignTransportQuery: the source asks the RSN to sign a transport-query capability targeting a destination.

type SignTransportQueryReply

type SignTransportQueryReply struct {
	Query *TransportQuery
}

SignTransportQueryReply carries the RSN-signed query. The RSN is a pure signing oracle here too: it signs the (RSN, target, requester, nonce) tuple and returns it; it does not contact the destination. The SOURCE carries the signed query to the destination (phase-1: dmsg-direct) and the destination verifies it against its trusted-RSN allowlist before responding.

type TransportListFetcher added in v1.3.100

type TransportListFetcher interface {
	FetchTransportList(ctx context.Context, pk cipher.PubKey) (*transport.SignedList, error)
}

TransportListFetcher fetches another visor's signed transport list over a skywire transport (GET /transports on its :80). The visor implements it.

type TransportQuery

type TransportQuery struct {
	RSNPK       cipher.PubKey `json:"rsn_pk"`       // RSN that authorized this query
	TargetPK    cipher.PubKey `json:"target_pk"`    // destination D whose transports are requested
	RequesterPK cipher.PubKey `json:"requester_pk"` // source S making the request
	Nonce       uint64        `json:"nonce"`        // anti-replay, unique per query
	RSNSig      cipher.Sig    `json:"rsn_sig"`      // RSN signature over SignablePayload
}

TransportQuery is an RSN-signed capability that authorizes the requester S to learn destination D's transport list. The RSN signs (RSNPK, TargetPK, RequesterPK, Nonce); D verifies the signature against its OWN PK and its trusted-RSN allowlist before responding. The signature binds the query to a single destination and requester, so it cannot be replayed against another visor or claimed by another requester.

func UnmarshalTransportQuery

func UnmarshalTransportQuery(data []byte) (*TransportQuery, error)

UnmarshalTransportQuery deserializes a query from JSON.

func (*TransportQuery) Marshal

func (q *TransportQuery) Marshal() ([]byte, error)

Marshal serializes the query as JSON.

func (*TransportQuery) Sign

func (q *TransportQuery) Sign(rsnSK cipher.SecKey) error

Sign signs the query with the RSN's secret key. Reuses the cascade signing primitive: SHA256 of the signable payload, signed by the RSN.

func (*TransportQuery) SignablePayload

func (q *TransportQuery) SignablePayload() []byte

SignablePayload returns the bytes signed/verified for this query: the RSN, target and requester PKs plus the nonce. The layout mirrors CascadeSetup.SignablePayload so the same cipher.Sign/Verify discipline applies.

func (*TransportQuery) Verify

func (q *TransportQuery) Verify(localPK cipher.PubKey, trustedRSNs map[cipher.PubKey]struct{}) error

Verify checks the query against the responding visor's own PK and its trusted-RSN allowlist. It enforces, in order: (1) TargetPK is us, (2) the RSN is trusted, (3) the signature verifies. Same authorization model as CascadeHandler.handleSetup.

type TransportQueryDeliverer

type TransportQueryDeliverer interface {
	DeliverTransportQuery(ctx context.Context, dst cipher.PubKey, q *TransportQuery) (*TransportQueryResponse, error)
}

TransportQueryDeliverer carries a signed transport-query to the destination visor and returns its response. PHASE-1 delivery is dmsg-direct to D (the mechanism `tp --remote` already uses to reach a remote visor); PHASE-2 is carrying the query over transports via intermediates like the cascade itself. The interface keeps computeDisjoint2HopRoutes' caller (fetchDstTransportsViaOracle) independent of the transport used, so the delivery seam can be filled in without touching the route computation.

func NewDmsgTransportQueryDeliverer

func NewDmsgTransportQueryDeliverer(dmsgC *dmsg.Client, log *logging.Logger) TransportQueryDeliverer

NewDmsgTransportQueryDeliverer builds a dmsg-direct query deliverer.

type TransportQueryRPCGateway

type TransportQueryRPCGateway struct {
	LocalPK cipher.PubKey
	// TrustedRSNs returns the current trusted-RSN public keys (typically
	// conf.EffectiveRouteSetupNodes) so allowlist edits at runtime are honored.
	TrustedRSNs func() []cipher.PubKey
	TM          *transport.Manager
	Log         *logging.Logger
}

TransportQueryRPCGateway is the DESTINATION-side (D) RPC gateway that answers RSN-oracle transport-list queries. It verifies the RSN signature against the visor's trusted-RSN allowlist (via TrustedRSNs) and returns D's own transports in compact form.

func (*TransportQueryRPCGateway) Query

Query is the net/rpc method D exposes. It verifies the signed query and returns D's transport list. Errors (untrusted RSN, bad signature, wrong target) propagate to the requester as RPC errors.

type TransportQueryResponse

type TransportQueryResponse struct {
	TargetPK cipher.PubKey            `json:"target_pk"`
	Entries  []transport.CompactEntry `json:"entries"`
}

TransportQueryResponse carries destination D's OWN transport list in the compact wire form (remote edge + type; see transport.CompactEntry). The requester reconstructs full entries with transport.EntryFromCompact(TargetPK, entry) — the reporter being D itself.

func BuildTransportQueryResponse

func BuildTransportQueryResponse(
	q *TransportQuery,
	localPK cipher.PubKey,
	trustedRSNs map[cipher.PubKey]struct{},
	tm *transport.Manager,
) (*TransportQueryResponse, error)

BuildTransportQueryResponse is the DESTINATION-side (D) handler. Given a signed query, the responding visor's PK, its trusted-RSN allowlist and its transport manager, it verifies the RSN authorization and returns D's own live data-plane transports in compact form.

It applies the same exclusions the local route calculation uses: closed / black-holed transports and LabelSetup (RSN control-plane) transports are never advertised as route-able edges. The response is built by walking D's transports and compacting each relative to D's own PK, so every entry reconstructs (via EntryFromCompact) to the canonical Entry both endpoints agree on.

func UnmarshalTransportQueryResponse

func UnmarshalTransportQueryResponse(data []byte) (*TransportQueryResponse, error)

UnmarshalTransportQueryResponse deserializes a response from JSON.

func (*TransportQueryResponse) Marshal

func (r *TransportQueryResponse) Marshal() ([]byte, error)

Marshal serializes the response as JSON.

type WeightMode

type WeightMode int

WeightMode controls how the transport selector distributes packets.

const (
	// WeightModeAuto uses latency-based weighting (faster transports get more packets).
	// Falls back to equal round-robin when no latency data is available.
	WeightModeAuto WeightMode = iota
	// WeightModeEqual distributes packets equally across all transports (round-robin).
	WeightModeEqual
	// WeightModeExplicit uses operator-supplied fractional
	// weights stored in explicitWeights. Normalized into an
	// integer schedule at Rebuild time. Set via the routing-
	// policy DSL's `distribution="weighted: f1, f2, ..."`.
	WeightModeExplicit
	// WeightModeSizeThreshold routes packets by payload size,
	// not by a pre-built schedule: > sizeThreshold goes to leg
	// 0, smaller packets RR across the rest. The Select()
	// fallback path returns 0 for callers that don't supply a
	// size (handshake / control packets); SelectForSize is the
	// primary entry point.
	WeightModeSizeThreshold
	// WeightModeSticky5Tuple hashes the IPv4 5-tuple of the
	// payload (src/dst IP + src/dst port + protocol) and picks
	// a leg by hash % live-leg-count. Same flow → same leg
	// always, deterministic. SelectForPayload is the entry
	// point; non-IPv4 payloads fall back to a payload-prefix
	// FNV hash.
	WeightModeSticky5Tuple
	// WeightModeLatencyAdaptive picks the live leg with the
	// lowest current GetLatency() per packet. Differs from
	// WeightModeAuto (latency-weighted schedule, rebuilt
	// periodically): adaptive evaluates per-packet, so a leg
	// whose RTT spikes mid-session loses traffic immediately.
	// Cost is one linear scan over the leg array per packet
	// (small constant for typical mux counts).
	WeightModeLatencyAdaptive
	// WeightModeDSCPPriority reads the IPv4 DSCP (upper 6 bits
	// of payload[1]) and routes packets >= threshold to leg 0,
	// others round-robin across the rest. Non-IPv4 payloads
	// fall back to round-robin.
	WeightModeDSCPPriority
	// WeightModeCapacity weights each leg by its recently-measured
	// throughput (capacityWeights, set by the mux from per-leg
	// byte deltas), with a per-leg exploration floor so every live
	// leg keeps a share. Bootstraps as equal round-robin until the
	// mux has throughput samples. Contrast WeightModeAuto, which
	// weights by inverse LATENCY and so starves a fat-but-slow leg;
	// capacity fills each leg toward its bandwidth — the mode that
	// actually aggregates throughput across a disjoint mux.
	WeightModeCapacity
	// WeightModeECF is a PREDICTIVE hold-back scheduler adapted from ECF
	// (Earliest Completion First, Lim et al., CoNEXT 2017). Capacity/auto
	// weighting keeps assigning a share of in-order frames to slower legs; in
	// a reorder buffer that must deliver strictly in order, every frame on a
	// slow leg is a head-of-line stall, so goodput/latency-proportional
	// spraying does NOT aggregate under path heterogeneity (~25% of ideal in
	// the MPTCP/MP-QUIC literature). ECF instead sends on the fastest leg
	// while it has send capacity, and spills onto a slower leg ONLY when the
	// slow leg would deliver its frame sooner than the fast leg can drain its
	// own backlog — otherwise it holds the frame on the fast leg. skywire has
	// no TCP cwnd, so the cwnd/"has capacity" test is adapted to a per-leg
	// bandwidth-delay-product (rate*RTT) and a selector-maintained in-flight
	// byte estimate; see ecfPick and SelectECF. This is the aggregation mode
	// that stops a heterogeneous mux from HoL-stalling on its slow legs.
	WeightModeECF
	// WeightModeOTIAS is the Out-of-order Transmission for In-order Arrival
	// scheduler (Yang et al.). It reasons over the SAME per-leg estimator
	// snapshot as ECF (ecfLegState), but instead of ECF's fast-leg-with-hold-back
	// rule it assigns each segment to the leg whose ESTIMATED ARRIVAL is soonest:
	// queueing delay (current backlog / send rate) + one-way propagation (RTT/2).
	// Charging the chosen leg makes successive picks fan out by arrival time, so
	// OTIAS deliberately hands a later segment to a slower-but-idle leg once the
	// fast leg's queue would make it arrive later. See otiasPick / SelectOTIAS.
	WeightModeOTIAS
	// WeightModeSTMS is the Slide Together Multipath Scheduler (Shi et al.,
	// USENIX ATC 2018). It keeps the head of the stream on the fast leg — filling
	// that leg's BDP send window first so EARLIER data always rides the fast path
	// — and only once the fast window is full places the following (later) data
	// on the soonest-arriving slower leg, so the pieces converge in order. Unlike
	// ECF, STMS does not decline a slow leg once it is in the active set (ECF's
	// hold-back predicate); it commits later data to it and lets the reorder
	// buffer absorb the gap. See stmsPick / SelectSTMS.
	WeightModeSTMS
)

func (WeightMode) String

func (m WeightMode) String() string

String renders the weight mode as the same lowercase token the routing- policy DSL's DistributionMode uses, so an operator reading `visor state` sees exactly the value they would set via a policy's `distribution=`.

Source Files

Directories

Path Synopsis
Package emu pkg/router/emu/conn.go
Package emu pkg/router/emu/conn.go
Package policy pkg/router/policy/bridge.go c2-net-routing RoutingContext / Candidate / RouteSpec to and from Starlark values, and assembles the pre-declared module set the policy script sees on load.
Package policy pkg/router/policy/bridge.go c2-net-routing RoutingContext / Candidate / RouteSpec to and from Starlark values, and assembles the pre-declared module set the policy script sees on load.
preset
Package preset pkg/router/policy/preset/engines.go c2-net-routing
Package preset pkg/router/policy/preset/engines.go c2-net-routing
presethook
Package presethook pkg/router/policy/presethook/presethook.go c2-net-routing adapts a built-in routing-policy preset (pkg/router/policy/preset) to the router.DialHook / RouteSelectingHook / RotationHook interfaces WITHOUT the starlark evaluator or the wazero runtime.
Package presethook pkg/router/policy/presethook/presethook.go c2-net-routing adapts a built-in routing-policy preset (pkg/router/policy/preset) to the router.DialHook / RouteSelectingHook / RotationHook interfaces WITHOUT the starlark evaluator or the wazero runtime.
presets
Package presets pkg/router/policy/presets/presets.go c2-net-routing embedded in the skywire binary, selectable by name without writing or compiling a policy file.
Package presets pkg/router/policy/presets/presets.go c2-net-routing embedded in the skywire binary, selectable by name without writing or compiling a policy file.
wasm
Package wasm pkg/router/policy/wasm/abi.go c2-net-routing for the WASM-host ABI.
Package wasm pkg/router/policy/wasm/abi.go c2-net-routing for the WASM-host ABI.
wasm/presets
Package presets pkg/router/policy/wasm/presets/presets.go c2-net-routing is the WASM analog of pkg/router/policy/presets: a registry of precompiled TinyGo routing-policy modules embedded in the skywire binary and selectable by name via a config value of the form "preset:<name>" (see pkg/visor/policy_loader.go), exactly like the Starlark presets.
Package presets pkg/router/policy/wasm/presets/presets.go c2-net-routing is the WASM analog of pkg/router/policy/presets: a registry of precompiled TinyGo routing-policy modules embedded in the skywire binary and selectable by name via a config value of the form "preset:<name>" (see pkg/visor/policy_loader.go), exactly like the Starlark presets.
Package routersettings pkg/router/routersettings/catalog.go c2-net-routing
Package routersettings pkg/router/routersettings/catalog.go c2-net-routing
Package setupmetrics pkg/router/setupmetrics/empty.go c2-net-routing
Package setupmetrics pkg/router/setupmetrics/empty.go c2-net-routing

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL