RFC-0016: Bus LAN Peering #171
Closed
kn4oqw-clint
announced in
RFCs
Replies: 1 comment
|
This has already been implemented. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
A peered bus is an RFC-0003 mode bus whose attachments live on more than one Waypoint node on the same LAN. One node owns the bus (its home node); other nodes join it over a dedicated, authenticated point-to-point link and contribute their local modes as if they were attachments on the home node. Voice reframed on any participating node is fanned to the others, subject to the same loop-prevention and single-source arbitration RFC-0003 already defines — now carried across the wire with an origin/hop-count envelope so a frame can never loop back to its origin or re-enter a bus it has already crossed.
This is a LAN feature only. It adds no WAN/Internet peering, no NAT traversal, no owner failover, and no per-user ACLs (§Design 6). It changes nothing about the reframe envelope — the AMBE+2 family, no vocoder — it only lets the hub's inputs and outputs sit on different boxes.
The transport decision (dedicated TCP over mTLS, not the MQTT broker) is backed by a latency spike measured on the bench pair, not estimated; the table is in §Design 1.
Motivation
RFC-0003 put the whole bus on one node because that is where MMDVM-Host and the gateways are. But operators run more than one node — a DMR hotspot in the shack, a YSF hotspot in the garage, a spare on the bench — and the natural question (issue #65 q4) is "can these hear each other without a reflector round-trip to the Internet?" Today the only answer is to point both at the same upstream talkgroup and pay the WAN latency and the dependency on a third-party reflector staying up. For two boxes ten metres apart on the same switch, that is absurd.
Peering makes the bus a LAN object. The home node's bus gains remote attachments; a keyed-up transmission on the garage YSF node is reframed locally, streamed to the shack node over the LAN, and emitted on DMR there — never leaving the building, never touching a reflector. The cost is a new authenticated link on the media path and the loop-prevention that N nodes require; the motivation is that this is the single most-requested bus extension and the LAN case is genuinely simple if we refuse to let it sprawl into a WAN mesh (§6).
Design
1. Transport — dedicated TCP framing over mTLS, not the broker
Decision: media flows over a persistent, length-prefixed TCP connection per peer, secured with mutual TLS. The MQTT broker carries status and control only; voice never transits it. This mirrors RFC-0008's finding for the single-node case (MMDVM-Host's voice plane is UDP, never MQTT) and extends it across the LAN.
The premise was tested, not assumed. A throwaway spike (
experiments/peerspike/) measured round-trip latency and jitter for 20 ms-cadence 55-byte (DMRD-sized) frames between the bench pair — the Pi 3 (pi-star@172.16.50.13, running the full stack) as the echo/broker node and the session host (172.16.50.24) as the client, on the same LAN switch — over (a) a persistent TCP+TLS connection and (b) the node's own mosquitto at QoS 0 and QoS 1. RTT was measured client-side against a single monotonic clock (no cross-host time sync); one-way = RTT/2. Results (representative run per transport; TCP and QoS 0 each held across three runs of 500–1000 frames, QoS 1 across two):(Client:
paho.mqtt.golang v1.5.1— the library Waypoint already ships. Broker: mosquitto with a temporary LAN listener, reverted after; the full stack ran throughout.)Two things the numbers settle:
Frame format on the wire: a 2-byte big-endian length prefix + the envelope (§5) + the mode's existing loopback frame bytes (the same DMRD/YSFD/NXDND the local hub already speaks). The connection is persistent per peer; TLS handshake cost is one-time and off the per-frame path (the spike measured steady-state framed latency, not handshakes).
2. Node count — N-node protocol, optional v1 validator cap
Decision: the wire protocol and frame envelope are N-node from the first byte; there is no two-node assumption anywhere in the framing, the envelope, or the store shape. The origin/hop-count envelope (§5) already generalises to any number of nodes, and
peers[]is a list. A future third node needs no protocol bump.The v1 UI and validator MAY cap a bus to two participating nodes if that simplifies the first release (fewer pairing screens to test, a simpler "who is online" indicator). That cap is a validator policy, not a framing limit — stated in one place (
ValidateBuses-adjacent, like RFC-0003's mode-set rules) and liftable by changing a constant. The RFC is explicit so nobody later mistakes the v1 cap for a protocol constraint.3. Pairing — explicit, mutual, fingerprinted; mDNS with manual fallback
Decision: peering is established by explicit mutual pairing, on the RFC-0002 claim-pattern lineage — a short numeric code shown on both dashboards, entered on one, confirmed on the other. No pure trust-on-first-use.
_waypoint._tcpover mDNS; the pairing screen lists discovered nodes by name. A manualhost:portentry is always available for networks where mDNS is filtered.internal/peeringhandshake/manager suite pins to it. The per-peer-key road is recorded in Alternatives.)waypointd reset-peer-identityregenerates the node's peering keypair and thereby invalidates every existing pairing at once (every peer's next handshake fails until re-paired). It is the "my node key may be compromised / I'm re-homing this box" escape hatch, distinct from per-peer revocation. Implementation lands in Prompt 15.4. Token ownership — the home node owns the token; no failover
Decision: the bus's home node (the node that owns the
Busrow) owns the single talk token for the whole cluster. If the owner is offline, the bus is down. No leader election, no failover, no token hand-off in v1. Predictable beats clever.Arbitration is unchanged from RFC-0003 §5 — one source at a time — but the token lives only on the owner. A remote node's inbound voice is a request the owner grants or drops exactly as if it were a local attachment; the owner's
bus_busy/voice events (RFC-0003) are the cluster's source of truth.Failure modes, stated plainly:
Owner failover is an explicit non-goal (§6); an operator who needs the bus to survive the owner rebooting should home it on the node with the best uptime.
5. Frame envelope — origin, hop count, and a play-out deadline
Every peered frame carries, ahead of the mode bytes:
Loop prevention across peers, extending RFC-0003 §5 rules 1 and 3:
max_hops(default equal to the peer count, so a valid frame never approaches it) is dropped and counted — a belt-and-suspenders backstop against a mis-paired ring.Play-out deadline / jitter buffer. Peered voice is play-out-scheduled at the receiving node with a small jitter buffer; late frames are dropped, not queued (this is voice). The defaults are derived from §1's measurements: the TCP transport tail is p99 ≈ 1.7 ms, max ≈ 2.6 ms, so the wire contributes ~2 ms of variation against a 20 ms cadence. The dominant delay is play-out choice, not the link. Defaults:
Both are single constants, tunable once field data on weaker nodes exists (§Open questions).
6. Scope fences (non-goals — verbatim from issue #65)
Store shape (RFC-0001 rows)
Two new sections plus a reference:
peers[]— one row per paired node:{ id, name, host, port, fingerprint, cert_ref, enabled }.cert_refnames a write-only secret entry holding the pinned peer certificate (and, for the local keypair, the node's own peering key); the certificate bytes never appear in a view. Disabling a peer (enabled=false) drops the link and preserves the pairing, exactly like RFC-0003 disabling a bus.remote_attachments[]— one row per local mode joined to a peer's bus:{ peer_id, remote_bus_id, mode, <the RFC-0003 §3 translation params for that mode> }. This is the joining node's declaration; the mode's addressing/TG translation is applied on emit exactly as a local attachment's is.Busgains apeers[]link (the paired peer ids allowed to join). A bus with no peer links renders exactly as today (RFC-0003); a bus with ≥1 peer link additionally renders the peer-facing listener.A mode may still appear in at most one attachment (local or remote) across all buses (RFC-0003 §5 rule 3), which the validator enforces across both sections.
What renders on each side
waypoint-bus-<id>.json(RFC-0003) gains apeersblock — the listen address and the set of paired peer ids permitted to connect, with their pinned-cert refs. The daemon opens the mTLS listener in addition to the local loopback endpoints.host:port, theremote_bus_id, the local mode and its translation params, and the local mTLS client cert ref. The daemon dials the peer and presents the local mode as an attachment on the remote bus.Both are pure functions of the store (RFC-0001 property 1); disabling a peer or a remote attachment yields a byte-identical render to before it was added, and re-enabling restores it (the RFC-0003 §6.2 property, extended).
Security posture
Peering uses mutual TLS with a keypair separate from RFC-0012's HTTPS device certificate. They are not shared, for reasons of trust anchor and lifecycle:
Sharing one keypair across both would couple browser-facing rotation to the peer mesh (rotating the HTTPS cert would break every pairing) and would leak media-plane trust to anything that trusts the web cert. Separate keypairs, separate write-only store entries, separate rotation; revoking a pairing never touches HTTPS and vice versa. Both live under the RFC-0002 write-only secret rule.
Test contract
CI-internal properties (pure/simulated, RFC-0003 §6 style):
{origin node, origin attachment, hop count}parses back to the same envelope;max_hopsoverflow is dropped, never panics.peers[]+remote_attachments[]render deterministically; disabling then re-enabling a peer yields a byte-identical render; a bus with no peer links renders exactly as RFC-0003.CI-external bench checks (recorded in
docs/on-hardware-report.md, not gated in CI):ip link set down) mid-QSO: the owner releases the token, both nodes mark the bus offline, and neither daemon crash-loops — the connection loss is an ordinary event, not a fault. On reconnect the bus resumes.experiments/peerspikemeasurement (or its productionised equivalent) on the bench pair and confirm one-way TCP+TLS latency stays within the §5 jitter-buffer budget; record the table with the stack pins, as done for the spike in this RFC.Alternatives considered
Open questions
host:portfallback covers reachability, but discovery UX on such networks is an open question (a "can't see the other node?" help path).Migrated from
docs/rfcs/0016-bus-lan-peering.md; the drafting history is in the git log.All reactions