SPORE Bridge Reference
Every medium SPORE can ride, and how to bridge it. This is both the index (a
compact status matrix) and the manual (a deep dive per protocol). A bridge is
a thin adapter between one medium and the medium-independent SPORE router; this
document is written for engineers implementing those adapters in Rust (or as a
browser send/receive shim).
- Brief overview? Jump to the Bridge index — one row per protocol, status shown by the emoji in its name.
- Implementing one? Read Bridge architecture once, then
the protocol's deep-dive section. Each separates the protocol specification
(the wire, as standardised elsewhere) from the SPORE bridge mapping (our
recommendation for
recv/send,U, and MTU). - Reference tables live in the Appendices.
Status legend
Understanding the emoji status indicators for each protocol
The emoji in each protocol's title tells you how far it is:
| Icon | Meaning |
|---|---|
| ✅ | Implemented & tested — a Rust bridge or JS transport with automated tests in this repo. |
| 🧪 | Implemented, not hardware-verified — the code exists and passes what can be tested off-device (codec roundtrips, syntax), but the real link (radio, serial, BLE, live peer) has not been exercised in CI. Treat as a template to confirm against your hardware/firmware; the repeatable procedure per path is in Hardware verification. |
| 🟡 | Partial — a codec, framer, or IP-underlay path is present, but the end-to-end runner is not finished. |
| ⚪ | Planned — a thin shim to write; the shared routing is already done. |
Throughout, U is the underlay address type (how a medium names a peer) and
Form is the driver form (dgram / stream / store, see below).
Bulk budget. Since files became manifest trees they can be arbitrarily large,
so any link can be conscripted into hauling one. Each interface may therefore
register a bulk budget — bytes per second of other people's file chunks it
is willing to relay (Hub::register_limited) — as a
leaky bucket that accrues a few seconds of burst.
Only chunks count as bulk. Messages, announces, receipts and manifests always
pass, so a paced link stays a full member of the mesh: it still carries the
conversation, and still tells everyone what exists. It declines only to be the
pipe, and because chunks are named by content the fetch simply asks again and
another path answers. The default for each medium is a constant in that bridge's
module, so the number and its reasoning stay together; override at runtime with
Hub::set_bulk_budget. Media fast enough not to care (UDP, TCP, WebRTC, the web
overlays) set no budget at all and are unchanged.
Bridge security — what a bridge is trusted with
Security principles: What bridges can and cannot do
Nothing. A bridge moves bytes; it is never trusted for authenticity, secrecy or honesty. Envelopes are signed and sealed end to end (§2, §7), ids are content hashes, and path learning binds only from signed frames. A hostile link can drop, delay, replay or reorder — all of which the router already survives — but it cannot forge, read a sealed payload, or make a node believe a false address.
That last one is load-bearing and was, until recently, not actually enforced.
Bridges learn SPORE address → underlay address by snooping, and both learning
paths — Neighbors::snoop and the node's own path table — accepted the SIGNED
flag as proof. A flag is one bit chosen by whoever wrote the frame. Copy a
victim's public key into src, set the bit, attach 64 zero bytes, and the
victim's address bound to your underlay address: every directed send for them
unicast to you instead. Sealed payloads stayed unreadable, so this stole no
content — it redirected delivery, which is the same guarantee by a different
door. Both paths now verify the signature, and Src::Short (8 bytes of address,
no key, nothing to verify against) teaches nothing at all. A bridge that
declines to learn falls back to broadcast, which always reaches the peer; a
bridge that learns a forged binding does not.
What a bridge can do wrong is spend our resources. Every bridge parses input from something it does not control, and the failure mode is not a forged message but an out-of-memory. So the rule for every bridge in this repo is:
Every read from a peer is bounded, and every length that comes off the wire is checked before it is believed.
The limits, and where they live:
| Bound | Value | Why |
|---|---|---|
kiss_stream::MAX_FRAME |
64 KiB | A peer that opens a KISS frame and never closes it. 46× the default MTU. |
bag request header |
16 KiB | A client that never sends the header terminator → 431. |
bag request body |
8 MiB | Content-Length is a number the client picks → 413. |
copyparty response |
8 MiB body, 16 KiB/line, 100 headers | A share can be compromised or simply broken. |
i2p control line |
8 KiB | Anything on port 7656 that streams without a newline. |
| store adoption | 1 MiB/file | A spill directory is on disk, where anything can drop a file. |
spool::MAX_SPOOL_FILE |
1 MiB/file | A spool is written by someone else by definition — that is what a spool is. |
Two of these bounds are on a read, not on a size the sender declared, and the distinction is the point:
spoolstats the file and then caps the read anyway. The size check is a fast reject; it is not the bound. Between thestatand thereadthe mover can replace or extend the file, so a plainfs::readwould buffer whatever is there at the later moment. The cap lives on the read itself; an over-cap file is left for the next sweep, whosestatnow sees the real size and clears it.reticulum::run_udpaccepts datagrams only from the companion. It is the one datagram bridge that carries framing state across datagrams, so a frame may be half-assembled when the next arrives. Whereudp::run_groupcan ignore a stranger's datagram as one bad envelope, here a stranger's bytes would interleave into a frame the companion is midway through — corrupting a good frame rather than merely adding a bad one. Single-sourcing the framer is what removes it.
Two further properties worth stating because they are easy to lose:
- An overrun resynchronises rather than disconnecting. A KISS frame that exceeds the cap is abandoned and the next delimiter starts a clean one; a corrupt or hostile stream costs one frame, not the link.
- Escaping buys no extra room. KISS escape pairs are two bytes on the wire and one in the buffer, and the cap counts the decoded byte — so a peer cannot double the memory cost by escaping everything.
Each of these is covered by a test that fails if the bound is removed.
Deployer note: what a stamp has to cost
Understanding stamp costs and deployment considerations
congestion::STAMP_QUOTA_BYPASS_BITS is 16. Mail stamped to at least that
class skips the per-source quota (§10d) and a busy peer's backpressure (§10c);
below it, mail still flows but is charged to its source's budget like anything
else, and stamp still orders eviction and TX priority (§10.3).
This was stamp > 0, which bounded nothing. A stamp is the leading zero bits of
the envelope id, and the id is a hash, so class 1 costs about two tries and half
of all envelopes have it by accident — measured, 12 of 20 arbitrary junk envelopes
were exempt from the quota by luck alone. SPEC is explicit that "priority is
bought, not claimed" (§2) and that a stamp is "proof of work" (§10); the exemption
has to cost something for either sentence to be true.
Consequences a deployer should know:
- A node running this is stricter than an older one. It throttles low-class stamped traffic that older relays pass. That is a local policy difference, not a wire incompatibility — nothing about the envelope changed, and the two interoperate.
- 16 bits is ~65k tries: milliseconds on a laptop, seconds on a microcontroller. Affordable once for a genuinely urgent message, ruinous for a flooder paying it per envelope. Raise it on fast networks; lower it (or set it to 1) to restore the old permissive behaviour if you need bit-identical admission against a 1.0 relay fleet.
sosstill outranks policy by convention — that is a routing preference, not a quota exemption, and is unaffected.
Bridge privacy — who sees what
Understanding privacy implications of different bridge types
A bridge cannot forge and cannot read a sealed payload. It can always see that traffic happened, how big it was, and when — and on some media it can see rather more than that. This section is per-bridge because the honest answer differs a lot, and "SPORE is encrypted" is not a useful thing to tell an operator choosing between a folder in someone's cloud account and a serial cable.
The part people get wrong first: a public flood is not encrypted. Sealing is
something a sender does. An envelope to ZERO_DEST or to a plaintext topic
carries its payload in the clear, by design — and the reason is that there is no
key to encrypt it to. A public topic's whole job is to be readable by a node that
joined afterwards, that you have never met, receiving it third-hand; anything
needing a prior key exchange is not that. (It is not that relays must read the
payload to route: forwarding runs entirely on the header, and sealed unicast
floods perfectly well with an opaque body.) Signed does not mean secret.
If a message must stay private on a medium anyone can watch, seal it to a
recipient prekey, or use an encrypted topic (§7) — which is a real private group,
with topic::rotate for forward secrecy and topic::rekey_seal to evict a member.
Otherwise assume it is a postcard, because it is one.
| Bridge | Who can observe | What they get beyond metadata |
|---|---|---|
serial, csma |
whoever holds the cable | everything on it; a wire is as private as the room |
spool (NNCP/UUCP/USB) |
the courier or mailer host | the files at rest, for as long as they hold them |
store / foldersync |
everyone with access to that folder — including a whole cloud account, and its provider | every envelope, plus filenames that are content ids, plus retention you do not control (versioned sync keeps deleted files) |
copyparty |
the share's operator and its logs | every envelope you post or fetch, plus your access pattern |
bag (HTTP server) |
anyone who can reach the port | whatever is in the store it serves; run it behind NAT, an onion, or auth |
udp broadcast / run_group |
every host on the segment | all public floods in the clear; join a multicast group and you are an observer |
audio modem |
anyone within earshot, and any recording | the same, and a recording replays later |
ax25, meshtastic, LoRa |
anyone with a receiver, at radio range | the same, and in most jurisdictions ham traffic is required to be unencrypted |
icmp |
every router on the path, and any IDS | payloads look like ping and are trivially logged wholesale |
tcp |
the peer, plus anyone on the path without a tunnel | the traffic; see the TLS section below |
reticulum |
the companion host and the RNS network | as much as its configured interfaces expose |
ssb |
the SSB feed's followers | envelopes published into a gossip log are effectively public and hard to unpublish |
nfc |
anyone who can hold a reader within a few centimetres | in practice nobody, which is the point: proximity is the access control, and the gesture is deliberate |
tor, i2p |
the introduction points | the least of any bridge here; this is what to reach for when the observer matters |
Three consequences worth stating outright:
- A gateway links identities across media. A node bridging Wi-Fi and LoRa tells
anyone watching either side that those two populations are connected, and relays
timing between them. That is the job — but it means running a gateway is not a
neutral act, and
mixmode exists for when it must not be inferable. - "Works offline" is not "works privately". The media that need no infrastructure — broadcast, radio, sound — are the ones where observation is cheapest. A blackout does not remove eavesdroppers.
- Mix mode is not Tor. It nests sealed envelopes and pads to three size classes
(
mix::SIZE_CLASSES), which frustrates casual correlation. Nobody has analysed it against a global observer, and it has had no external review. Do not stake anything on it that Tor would be the right tool for.
TLS — deliberately not linked in
TLS termination outside the process - Design decision
Several media on this page speak only TLS: Matrix, XMPP, e-mail, wss:// Nostr
relays, most public HTTP shares. SPORE links no TLS stack, and that is a
decision rather than an omission.
SPORE does not need TLS for security. Envelopes are signed and sealed end-to-end (§2, §7); a link is assumed hostile whatever it is made of. TLS here buys exactly one thing — permission to talk to a server that insists on it. Paying for that with a TLS implementation and its certificate machinery, in a project whose premise is that one person can audit and rebuild the whole thing from a printed spec, is a bad trade.
So TLS is terminated outside the process, the same way sound cards, serial line settings and Reticulum already are:
# HTTPS share -> plaintext on localhost, then point a bridge at the tunnel
socat TCP-LISTEN:8080,fork,reuseaddr OPENSSL:files.example.org:443
spore copyparty:http://127.0.0.1:8080/bag/
# or with stunnel, which verifies certificates properly
stunnel -c -d 127.0.0.1:8080 -r files.example.org:443
Three consequences worth being explicit about:
- Certificate verification is the tunnel's job.
socat OPENSSL:verifies by default;openssl s_clientdoes not unless told to. Configure it as carefully as you would any TLS client, because SPORE cannot check it for you. - The plaintext hop is real. Bind the tunnel to loopback. On a shared machine that hop is visible to other users — though it carries signed, sealed envelopes, so what leaks is traffic analysis, not content.
- TLS alone does not deliver Matrix or XMPP. Those also need their protocol (Matrix's client-server API and JSON, XMPP's XML streams). The tunnel removes the transport blocker; the protocol work is still unwritten. What it does unlock today is every HTTP-shaped store: copyparty, WebDAV, an HTTPS bag.
If a single-binary story ever matters more than the dependency budget, the clean way in is an optional cargo feature that swaps the tunnel for a pure-Rust TLS client — not a default dependency, and not something hand-rolled. Nobody should write their own TLS.
Bridge architecture
The core components of every SPORE bridge
Because the router is medium-independent, every bridge reduces to the same three
things, and all the shared logic already lives in src/bridge/:
- An address type
U— how that medium names a peer (a MAC, anIP:port, a Meshtastic node number, or()for broadcast-only media). Learned and resolved bybridge::Neighbors<U>(SPORE's ARP), which is generic overU: it snoops the source of inbound frames to bindspore_addr → U, resolves a SPOREdestto aUfor directed sends, and ages bindings out (expire) or drops them on disconnect (forget). - A payload budget (MTU) — the bridge clamps the shared node's
mtu, and SPORE's fountain fragmentation (Node::send) auto-splits to fit. There is no per-medium chunking code to write. recv/send— the only platform-specific part. A datagram medium implementsbridge::driver::DatagramTransport(justrecv,send, optionalmtu) and gets neighbour learning, resolution, relay, and MTU clamping for free fromdriver::run_datagram.
So adding a medium is: pick U, set the MTU, write recv/send. Everything
else is in the lib.
The three driver forms
| Form | What it is | Implement | Reference bridges |
|---|---|---|---|
dgram |
fire-and-forget frames to a U (or broadcast) |
driver::DatagramTransport (recv/send) |
bridge::udp (~40 lines), bridge::meshtastic |
stream |
a byte stream needing framing | a reader/writer + KISS framing | bridge::tcp, bridge::kiss_stream::KissStream |
store |
a shared container polled for new items | poll + write *.spore items |
bridge::store (files), bridge::bag (HTTP), bridge::ssb |
The error model (where FEC lives)
A bridge hands up an intact envelope or nothing — never a damaged one. The core has no repair path and wants none: it dedups by content id, so a corrupted frame that arrived is strictly worse than one that did not, and every recovery mechanism above this line assumes loss rather than corruption.
That makes forward error correction a bridge detail, decided per medium, because the error characteristics are a property of the medium and nothing else:
- Most media already do it below us. 802.11 (convolutional/LDPC + FCS), LoRa (coding rate 4/5–4/8 + CRC), Meshtastic and RNode all run FEC in their own PHY and drop what fails. Damage arrives as a whole missing frame. A bridge on one of these adds nothing — coding over a coded channel pays overhead for little gain.
- A bridge that owns a raw channel owns the problem. Audio modem is
the case in the tree: SPORE supplies the modulation, the framing and the
integrity check, so it must at minimum detect corruption (it appends
SHA-256(frame)[0..4]and rejects on mismatch) and must decide for itself whether correcting is worth it. Whether it should is #179. - Recovery above the frame is already solved, once. Losing whole frames is what the fountain erasure code exists for (Spec §3): any sufficient subset of fragments rebuilds the object, so a bridge never needs a retransmit scheme of its own. What a bridge owes is an honest boundary — deliver it whole or drop it — not reliability.
So the question every new bridge answers is not "does SPORE do FEC" but "does this medium already hand me clean frames, and if not, what am I adding to make it?"
State model (all handled by Neighbors<U>)
- Stateless (LoRa, ESP-NOW, Ethernet, most store carriers): no connection —
hand bytes to
U. A neighbour is "gone" only when its signed heartbeats stop and its binding ages out (Neighbors::expire). - Stateful (WebSocket, BLE GATT, serial, Wi-Fi):
Uis a live connection object. When it drops, the bridge callsNeighbors::forgetso the router stops sending into a dead handle. - Null (
U = (): audio, raw LoRa, QR): the hardware has no target field — everyone in range hears everything. Every SPORE address maps to(),resolvetrivially succeeds, and the envelope's owndestfilters mail for others.
Shapes vs. forms. The spec (Page 2) names five medium shapes — message pipe, byte stream, text channel, shared bus, shared store. In this reference they collapse to the three driver forms above, because message pipes and shared buses both ride the datagram driver, and text channels and shared stores both ride the store pattern. Tables here list the driver form; "shape" always means the five spec categories, never the forms.
Bridge index
Status is the emoji on each name. Follow the link for the deep dive.
1. Direct links & radios
| Protocol | Form | U |
MTU | One-line |
|---|---|---|---|---|
| UDP / IPv4 broadcast ✅ | dgram | SocketAddr |
1400 | |
| WebTransport 🧪 | dgram | Url |
1400 | |
| Ethernet 802.3 ⚪ | dgram | [u8;6] |
1500 | raw L2 frames, EtherType-tagged |
| Wi-Fi 802.11 ⚪ | dgram | [u8;6] |
2304 | raw/monitor-mode frames |
| Wi-Fi Direct 🟡 | dgram | Ipv4Addr |
1500 | UDP bridge over the P2P group interface |
| ESP-NOW ⚪ | dgram | [u8;6] |
250 | connectionless 2.4 GHz on ESP32 |
| BLE GATT ⚪ | stream | [u8;6] |
~247 | serial-over-BLE via a GATT characteristic |
| BLE Mesh ⚪ | dgram | u16 |
380 | managed-flood BLE mesh |
| NFC (ISO 14443) 🧪 | dgram | [u8;7] |
~255 | tap-to-transfer NDEF records (Web NFC) |
| LiFi (802.15.7) ⚪ | dgram | [u8;6] |
var | visible-light modem |
| IrDA ⚪ | dgram | u32 |
2048 | infrared IrCOMM |
| Zigbee ⚪ | dgram | u64 |
104 | 802.15.4 mesh with APS fragmentation |
| Z-Wave ⚪ | dgram | u8 |
54 | sub-GHz home-automation mesh |
| LoRaWAN ⚪ | dgram | u32 |
51–222 | long-range via a network server |
| LoRa P2P ⚪ | dgram | () |
255 | raw LoRa, no network server |
| Ham AX.25 / KISS 🧪 | stream | () |
256 | packet radio over a TNC (TCP or serial) |
| APRS ⚪ | dgram | call-ssid | ~200 | messages over AX.25 / APRS-IS |
| DMR ⚪ | dgram | u32 |
var | IP-over-DMR data |
| goTenna ⚪ | dgram | u32 |
~200 | consumer mesh radio |
| Audio modem ✅ | dgram | () |
4 K/frame | data-over-sound, 16-FSK |
| ICMP echo (ping) 🧪 | dgram | Ipv4Addr |
1400 | envelopes in ping payloads (Linux raw socket) |
| JANUS (sonar) ⚪ | dgram | u8 |
32 | underwater acoustic (NATO STANAG 4748) |
| QR stream 🟡 | dgram | () |
~1 K | armored envelopes as scanned codes |
| Iridium SBD ⚪ | dgram | u32 |
340 | satellite short-burst data |
2. Meshtastic — one codec, several pipes
All four share the bridge::meshtastic MeshPacket
codec, so U and MTU never change. See Meshtastic.
| Pipe | Form | U |
MTU | One-line |
|---|---|---|---|---|
| Meshtastic — WiFi-UDP ✅ | dgram | u32 |
237 | multicast on the LAN (bridge::meshtastic::run) |
| Meshtastic — USB serial 🧪 | stream | u32 |
237 | same protobuf, framed on the serial stream |
| Meshtastic — Web Serial 🧪 | stream | u32 |
237 | browser → node over USB (web/transports/meshtastic.mjs) |
| Meshtastic — Bluetooth 🧪 | stream | u32 |
237 | browser → node over BLE |
3. Reticulum — RNS destinations, several pipes
SPORE rides Reticulum as a payload addressed by the 16-byte RNS destination hash. See Reticulum.
| Pipe | Form | U |
MTU | One-line |
|---|---|---|---|---|
| Reticulum — RNS payload 🧪 | dgram | [u8;16] |
383 | envelopes on a shared RNS destination (native, via companion) |
| Reticulum — companion TCP/UDP 🧪 | dgram/stream | — | 383 | reach the companion over the network, not a pipe |
| Reticulum — RNode serial ⚪ | stream | [u8;16] |
500 | LoRa RNode over USB (native) |
| Reticulum — Web Serial 🧪 | stream | [u8;16] |
500 | RNode over USB from a browser tab |
| Reticulum — Bluetooth 🧪 | stream | [u8;16] |
500 | RNode over BLE (Nordic UART) |
4. Internet overlays
Mesh-routing and anonymity networks. Most carry IP, so the UDP bridge rides them unchanged — point it at the right address on the overlay's interface.
| Overlay | Form | U |
MTU | One-line |
|---|---|---|---|---|
| BATMAN-adv 🧪 | dgram | [u8;6] |
1500 | L2 mesh; udp::run_group pinned to bat0 |
| Yggdrasil / cjdns 🧪 | dgram | Ipv6Addr |
1280 | IPv6 overlay; udp::run_group on ff02::7373 |
| Thread 🧪 | dgram | Ipv6Addr |
1280 | 6LoWPAN mesh; udp::run_group on the mesh iface |
| Tor (onion service) 🧪 | stream | .onion |
64 K | hidden-service rendezvous via SOCKS5 |
| I2P 🧪 | stream | b32 dest | 1200 | garlic-routed streams via SAM v3 |
| iroh (QUIC) 🧪 | stream | EndpointId | var | QUIC p2p by public key; hole-punch + relay fallback (bridge-iroh feature) |
| Veilid ⚪ | dgram | node id | var | private-routed DHT |
| libp2p (gossipsub) ⚪ | stream | PeerId | var | pub/sub overlay; IPFS swarm |
| WebSocket ✅ | stream | conn | 64 K | binary frames to a relay or peer |
| WebTransport ⚪ | stream | conn | var | QUIC datagrams/streams in the browser |
| WebRTC DataChannel 🧪 | stream | String |
16 K | direct browser P2P, serverless signaling |
| WebTorrent swarm 🧪 | stream | String |
16 K | tracker rendezvous, then WebRTC P2P |
| Web Serial / USB 🧪 | stream | conn | var | KISS to a TNC/RNode/ESP32 from a tab |
| Web Bluetooth 🧪 | stream | String |
~247 | Nordic UART from a tab, KISS-framed |
5. Store-and-forward & app carriers
Systems that already store and pass on messages — the pattern SPORE is.
| Carrier | Form | U |
MTU | One-line |
|---|---|---|---|---|
| Folder / USB / Syncthing ✅ | store | — | — | *.spore files in a synced directory |
| HTTP bag ✅ | store | conn | 64 K | pull envelopes from an HTTP endpoint |
| Copyparty 🧪 | store | URL | 64 K | envelopes in a copyparty share (HTTP/WebDAV) |
| Text armor ✅ | store | — | ~150 | SMS/paper/voice-safe base32 with a checksum |
| Nostr 🟡 | store | relay | var | events on any relay (kind-30078) |
| SSB (Secure Scuttlebutt) 🟡 | store | feed | var | spore-v1 content in an append log |
| Matrix ⚪ | store | room id | large | envelopes as room events |
| XMPP / Jabber ⚪ | stream | JID | large | message stanzas or PubSub |
| DeltaChat (email) ⚪ | store | address | large | envelopes as e-mail (IMAP/SMTP) |
| Session (Oxen) ⚪ | store | session id | var | onion store-and-forward |
| Briar ⚪ | stream | contact | var | Tor + BLE friend-to-friend |
| Tox ⚪ | dgram | ToxID | ~1200 | P2P DHT messaging |
| DTN / Bundle Protocol v7 ⚪ | store | EID | var | RFC 9171 delay-tolerant bundles |
| NNCP 🧪 | store | node id | var | bridge::spool moved by nncp-xfer / areas |
| UUCP 🧪 | store | host | var | bridge::spool moved by uucp / uucico |
| Serval Rhizome ⚪ | store | SID | var | mesh store-and-forward |
| Hypercore / Hyperswarm ⚪ | stream | key | var | append-log replication |
| Earthstar / Willow ⚪ | store | share | var | offline-first sync protocol |
Direct links & radios
UDP / IPv4 broadcast
Summary. UDP is the connectionless datagram service of the Internet protocol suite: a source/dest port pair, a length, a checksum, and a payload, with no handshake and no delivery guarantee. SPORE puts one envelope in one datagram and broadcasts it to the LAN, so every node on the segment hears every frame — exactly SPORE's flood model, with zero infrastructure. It is the reference datagram bridge and the fastest way to bring a local mesh up.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
SocketAddr |
| MTU | 1400 (leaves room under the 1500-byte Ethernet MTU) |
| State | stateless |
| Status | ✅ implemented & tested |
| Code | bridge::udp::run (255.255.255.255), bridge::udp::run_primary (auto subnet bcast) |
Deep dive
Protocol wire format (RFC 768). The SPORE envelope is the UDP payload verbatim — one envelope per datagram, no extra framing.
0 15 16 31
+-----------------+-----------------+
| Source Port | Destination Port|
+-----------------+-----------------+
| Length | Checksum |
+-----------------+-----------------+
| data = SPORE envelope |
+-----------------------------------+
SPORE bridge mapping. send(None, env) sends env to the broadcast address;
send(Some(sa), env) unicasts to a learned SocketAddr. recv reads one datagram
and reports the sender's SocketAddr as U, which Neighbors<SocketAddr> snoops
to bind spore_addr → ip:port. Broadcast is native: 255.255.255.255 (limited) or
the primary interface's directed broadcast (192.168.x.255), which run_primary
derives from the interface netmask for zero-config LAN. MTU is clamped to 1400 so
the fountain fragmenter splits larger objects into datagram-sized chunks. No
connection lifecycle — a neighbour ages out when its heartbeats stop.
Security. UDP itself has no authentication or encryption (its checksum is
integrity-only and optional on IPv4). SPORE does not rely on the medium: every
envelope carries its own Ed25519 signature and an optional sealed-box payload, so a
forged or replayed datagram is rejected by the router (hops=0 content-ID dedup +
signature check), not by UDP. On open Wi-Fi, treat the segment as public.
References. RFC 768 (UDP); RFC 919 / RFC 922 (broadcasting datagrams).
Ethernet 802.3
Summary. The dominant wired L2. A bridge would send raw frames under a private EtherType so SPORE rides the segment below IP — useful where there is no IP configuration at all (a crossover cable, a dumb switch, a field patch).
| Field | Value |
|---|---|
| Driver form | dgram |
U |
[u8;6] (MAC) |
| MTU | 1500 (or 9000 jumbo) |
| State | stateless |
| Status | ⚪ planned |
| Code | raw AF_PACKET/BPF socket per OS |
Deep dive
Protocol wire format (IEEE 802.3). dest MAC(6) · src MAC(6) · EtherType(2) · payload(46–1500) · FCS(4). The bridge picks an unregistered EtherType (e.g.
0x88B5/0x88B6, reserved for experimental use) and places the SPORE envelope in
the payload; frames shorter than 46 bytes are zero-padded (SPORE ignores the pad —
the envelope carries its own length).
SPORE bridge mapping. U = [u8;6]; recv yields the source MAC,
Neighbors<[u8;6]> snoops it, and broadcast is ff:ff:ff:ff:ff:ff. Needs
CAP_NET_RAW (Linux) or a BPF device (macOS/BSD); belongs in a platform crate, not
the portable core.
Security. None at L2; rely on the envelope signature. VLAN/segment isolation is the only medium-level boundary.
References. IEEE 802.3; EtherType registry (IEEE RA).
Wi-Fi 802.11
Summary. Wi-Fi in raw/monitor mode can send action or data frames without association, letting nearby devices exchange SPORE frames with no access point — a true ad-hoc broadcast medium.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
[u8;6] (MAC) |
| MTU | 2304 (802.11 MSDU) |
| State | stateful (monitor/IBSS setup) |
| Status | ⚪ planned |
| Code | monitor-mode inject/capture per OS/driver; on ESP32-S3, esp_wifi_80211_tx + a promiscuous RX callback (Roadmap M8/E2) |
Deep dive
Protocol. 802.11 MAC frames carry up to four address fields and a payload; a bridge would use a data or vendor-specific action frame with the envelope as the frame body. In practice most deployments run IP over Wi-Fi and reuse the UDP bridge — raw injection is only worth it for AP-less operation and needs a card/driver that supports monitor mode + injection.
SPORE bridge mapping. U = [u8;6] (the source MAC), dgram form, one envelope
per frame body. The receive side is the part that differs from every other bridge:
monitor mode delivers every frame in the air, not just ours, so the callback must
reject foreign traffic before doing any work on it. Envelope::probe exists for
exactly this — it walks the v1 header and returns the envelope's length, or None
to discard, without allocating. Only what survives it is worth handing to
Envelope::decode and then hub.on_rx. A probe hit is a structural match and
nothing more; the signature check still decides whether a frame is real.
Frame format (normative — both implementations must match). The board and the Linux daemon are two implementations of one wire format, so it is specified here rather than left to whichever gets written first. Board-to-board interop happens by accident when both ends run our code; board-to-laptop only happens if this is written down.
SPORE rides a vendor-specific Action frame — management type, subtype Action, category 127. That is the same mechanism ESP-NOW is built on, which is the reason to choose it: it is the one frame shape an ESP32 is known to inject and receive without association, and it needs no LLC/SNAP header or registered EtherType.
802.11 MAC header (24 bytes)
0 Frame Control 2 0xD0 0x00 — type=Management(0), subtype=Action(13)
2 Duration 2 0x0000 — set by hardware
4 Address 1 6 FF:FF:FF:FF:FF:FF (destination; always broadcast)
10 Address 2 6 transmitter MAC (learned as `U`)
16 Address 3 6 02:53:50:4F:52:45 (BSSID, fixed — see below)
22 Seq Control 2 0x0000 — set by hardware
Action body
24 Category 1 0x7F — Vendor Specific
25 OUI 3 02:53:50 — locally administered, see below
28 Magic 1 0x53 — 'S'
29 Version 1 0x01 — this format
30 Payload n one SPORE envelope, verbatim
FCS 4 — appended and checked by hardware
- Broadcast only. Address 1 is always the broadcast MAC. SPORE floods and dedups by content id; unicast at this layer would duplicate routing the core already does.
- The BSSID is a constant, not an access point.
02:53:50:4F:52:45is02+"SPORE"in ASCII, with the locally-administered bit set. Nothing joins it and no beacons are sent — it exists so a capture filter can select SPORE traffic cheaply, and so two boards from the same box share it with no configuration. - The OUI is locally administered, not IEEE-registered.
02:53:50sets the local bit for the same reason. Collision with another vendor using the same value is possible and harmless: the magic byte, the version, and thenEnvelope::probereject anything that is not ours before it costs a decode. - Channel 1 by default, because the locked "flash it and it works" rule means two boards must find each other with no configuration. Both ends must be on the same channel; scanning is not part of this bridge.
- No FEC of our own, deliberately. 802.11's PHY already does forward error correction, and the FCS means a corrupted frame never reaches us — losses arrive as whole missing frames, not flipped bits. That is exactly the shape SPORE's fountain erasure coding already handles at the object layer (Spec §3), so adding bit-level coding here would pay overhead to solve a problem the medium has solved.
- MTU is unvalidated. The 2304-byte MSDU is the standard's ceiling, not what a given driver will inject; ESP-IDF and mac80211 both impose their own lower limits. Treat the table's figure as the theoretical maximum and fix a conservative real one during E2 — 🧪 until a device run establishes it.
Security. WPA2/WPA3 secure the link when associated; raw/ad-hoc frames are open. SPORE's signature is the trust anchor either way.
Regulatory. Injecting frames outside normal association is regulated differently across jurisdictions, and some bands carry rules this bridge does not try to satisfy on your behalf — amateur bands prohibiting encrypted content being the most common. SPORE documents what the bridge does and does not weaken or strip encryption to comply with a band's rules for you; whether a given deployment is permitted is the operator's call, and theirs to answer. This is the same disclosure posture as a mix mode's stated limits, not an endorsement or a warning against use.
References. IEEE 802.11.
Wi-Fi Direct
Summary. Wi-Fi Direct (P2P) forms a group with one device as a soft-AP, giving the group an IP subnet with no infrastructure. Because it is just IP, the existing UDP bridge rides it unchanged.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
Ipv4Addr |
| MTU | 1500 |
| State | stateful (group lifecycle) |
| Status | 🧪 implemented — udp::run_group pinned to the P2P interface's address |
| Code | bridge::udp::run_primary pointed at the p2p0 interface |
Deep dive
SPORE bridge mapping. Bring up the P2P group with the platform API
(wpa_supplicant p2p_connect, Android WifiP2pManager), then run the UDP
broadcast bridge on the group interface's directed broadcast. No SPORE-specific
code is needed beyond selecting the interface.
References. Wi-Fi Alliance — Wi-Fi Direct (specification is member-gated); relies on IEEE 802.11 P2P.
ESP-NOW
Summary. Espressif's connectionless 2.4 GHz protocol: short, low-latency frames between ESP32/ESP8266 devices with no association or IP stack — ideal for cheap, battery-friendly sensor meshes.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
[u8;6] (peer MAC) |
| MTU | 250 bytes/frame |
| State | stateless |
| Status | ⚪ planned |
| Code | esp-idf shim (esp_now_send/esp_now_recv_cb) |
Deep dive
Protocol. A vendor action frame carries up to 250 bytes of payload to a unicast peer MAC or the broadcast MAC. The SPORE envelope is that payload; larger objects fountain-fragment to ≤250 bytes.
SPORE bridge mapping. U = [u8;6]; register a receive callback that feeds
hub.on_rx and snoops the source MAC; send(None, …) uses the broadcast peer.
Runs on the ESP32 core build (esp-idf), not the desktop daemon.
References. ESP-NOW User Guide (Espressif).
BLE GATT
Summary. Bluetooth Low Energy exposes a serial-like pipe through a GATT service with a write characteristic (host→device) and a notify characteristic (device→host). It is the near-universal way phones and hobby boards talk over short range. (The browser side is implemented — see Web Bluetooth; this row is the native per-OS driver.)
| Field | Value |
|---|---|
| Driver form | stream |
U |
[u8;6] (device address) |
| MTU | ~247 (ATT MTU − 3; often 20 before negotiation) |
| State | stateful (GATT connection) |
| Status | ⚪ planned (native); 🧪 in the browser |
| Code | bridge::kiss_stream over a per-OS BLE stack |
Deep dive
Protocol. GATT writes/notifications carry opaque bytes capped at the negotiated
ATT MTU. There is no framing, so a stream framer is required: SPORE uses KISS
(bridge::kiss_stream), matching the serial bridge, and chunks each frame to the
characteristic size. The de-facto profile is the Nordic UART Service (NUS,
6e400001-…) with RX …0002 and TX …0003.
SPORE bridge mapping. U is the connected device; on disconnect call
Neighbors::forget. Discovery is the platform scan/pair flow. See
web/transports/webbluetooth.mjs and
kiss.mjs for a complete, byte-compatible reference.
Security. BLE pairing (LE Secure Connections) encrypts the link; SPORE's signature holds regardless of pairing mode.
References. Bluetooth Core Specification (GATT/ATT, Vol 3); Nordic UART Service (Nordic Semiconductor docs).
BLE Mesh
Summary. A managed-flooding mesh layered on BLE advertising, addressing nodes by 16-bit unicast/group addresses — designed for many-hop building-scale networks.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u16 (element address) |
| MTU | ~380 (segmented access payload) |
| State | stateless (managed flood) |
| Status | ⚪ planned |
| Code | vendor model over a BLE-Mesh stack |
Deep dive
Protocol. SPORE would ride a vendor model message; the stack handles
segmentation/reassembly and relay. U = u16; group address = broadcast.
Security. BLE Mesh has its own network/app keys; SPORE's signature is independent.
References. Bluetooth Mesh Protocol Specification.
NFC (ISO/IEC 14443)
Summary. Tap-to-transfer at a few centimetres. An envelope becomes an NDEF record, moved by touching two phones or a phone to a tag — a deliberate, physical "hand it over" gesture ideal for seeding a device.
| Field | Value |
|---|---|
| Driver form | dgram (one shot) |
U |
[u8;7] (UID) or () |
| MTU | ~130 B (NTAG213) … ~850 B (NTAG216); phone-to-phone larger |
| State | stateful (field present) |
| Status | 🧪 NDEF codec implemented + tested; the tap itself needs a phone |
| Code | web/transports/webnfc.mjs |
Deep dive
Protocol. An NDEF message wraps the envelope in a MIME record
(application/x-spore). encodeNdef emits the short-record form whenever the
payload fits in a byte — the common case for a fragment — and the 4-byte length
field beyond that. Larger objects span multiple taps as fountain fragments, and
any ~K of N suffice, so a mistimed tap costs a repeat rather than a restart.
Why the browser and not the daemon. A Rust NFC bridge needs libnfc or
PC/SC — a C library. That is the one thing the bridge selection rule excludes, and
it is the same rule that kept TLS out (see the TLS section). Web NFC needs no
dependency at all, and the realistic gesture — two phones touching — is a phone
scenario anyway. There is therefore no Rust twin and nothing for
web/codec-test.mjs to check parity against; it tests the codec on its own.
Verification, honestly. The codec is pure and covered by twelve checks in
web/codec-test.mjs: short and long records round-trip, and a URL tag, a foreign
MIME type, a length that runs past the buffer, and every truncation of a valid
message are all refused. The NDEFReader plumbing is not tested — Web NFC is
Chrome on Android over HTTPS with a user gesture, and there is no headless way to
present a tag. Same split as the ICMP bridge: the framing is tested, the hardware
loop is the operator's.
Bounded like every other bridge. A tag is written by whoever held it last, so
the decoder checks every declared length against what actually arrived rather than
believing it. Outbound envelopes queue until a tag enters the field, and that queue
is capped (MAX_QUEUED, oldest dropped) — a phone in a pocket must not accumulate
a backlog because nothing has been tapped for an hour.
Security. Proximity is the channel; the envelope signature authenticates. The range is the honest guarantee here — a few centimetres is a real access-control property in a way no radio bridge can claim.
References. ISO/IEC 14443; NFC Forum NDEF; W3C Web NFC.
LiFi (IEEE 802.15.7)
Summary. Visible-light communication — data modulated onto an LED, received by a photodiode or camera. Air-gapped and directional; useful where RF is jammed, forbidden, or monitored.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
[u8;6] or () |
| MTU | variable |
| State | stateless |
| Status | ⚪ planned |
| Code | VLC modem shim (like the audio modem, over light) |
Deep dive
SPORE bridge mapping. Conceptually identical to the audio modem:
modulate the envelope to a symbol stream, demodulate on the far side; U = ()
(broadcast, directional). A software approach can reuse the FSK framing over an LED
driver and a camera/photodiode.
References. IEEE 802.15.7.
IrDA
Summary. Infrared point-to-point (IrCOMM emulates a serial port). Legacy, but still present on some industrial and embedded gear; a short-range, line-of-sight link with no RF footprint.
| Field | Value |
|---|---|
| Driver form | dgram/stream |
U |
u32 (device address) |
| MTU | 2048 |
| State | stateful |
| Status | ⚪ planned |
| Code | IrCOMM as a serial stream + KISS |
Deep dive
SPORE bridge mapping. Treat IrCOMM as a byte stream: KISS-frame envelopes as on
Web Serial. U = the discovered device address.
References. IrDA specifications (archived); IrLAP/IrLMP/IrCOMM.
Zigbee
Summary. An 802.15.4 low-power mesh common in home automation; small frames, many hops. A bridge rides application-layer (APS) messages with APS fragmentation.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u64 (IEEE address) or u16 (network) |
| MTU | ~104 (with APS fragmentation, larger) |
| State | stateful (network join) |
| Status | ⚪ planned |
| Code | vendor cluster over a Zigbee stack/coordinator |
Deep dive
Protocol. 802.15.4 MAC (127-byte PHY) → NWK → APS. SPORE rides a manufacturer-
specific cluster; APS fragmentation carries envelopes over the tiny MTU (the
fountain fragmenter also helps). U = u64; broadcast = 0xFFFF network address.
References. IEEE 802.15.4; Zigbee/Matter specs (CSA).
Z-Wave
Summary. A sub-GHz home-automation mesh with a small, fixed frame and a serial controller. Low bandwidth but excellent building penetration.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u8 (node id) |
| MTU | ~54 |
| State | stateful (controller) |
| Status | ⚪ planned |
| Code | serial controller (Z-Wave Serial API) |
Deep dive
SPORE bridge mapping. Talk to the controller over serial; send/receive raw
application payloads. The ~54-byte MTU makes fountain fragmentation essential.
U = u8 node id; broadcast node = 0xFF.
References. Z-Wave specifications (Silicon Labs / Z-Wave Alliance); ITU-T G.9959 (PHY/MAC).
LoRaWAN
Summary. Long-range, low-power WAN: devices reach gateways that forward to a network server. Great range and battery life, but duty-cycle-limited and mediated by a server — best for sparse, infrequent envelopes.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u32 (DevAddr) |
| MTU | 51–222 (data-rate dependent) |
| State | stateful (session) |
| Status | ⚪ planned |
| Code | via a network server's app API (MQTT/HTTP) |
Deep dive
Protocol. Envelopes ride the application payload of confirmed/unconfirmed uplinks/downlinks. Because a network server sits in the path, the bridge is really a store/queue client of that server (e.g. The Things Network MQTT). Respect the region's duty cycle; keep envelopes tiny and rare.
References. LoRaWAN L2 1.0.4 / 1.1 (LoRa Alliance).
LoRa P2P
Summary. Raw LoRa radio with no network server — chirp-spread-spectrum frames between SX127x/SX126x modems in range. This is the medium Reticulum's RNode and Meshtastic use underneath; a direct SPORE bridge would broadcast envelopes with no addressing at all.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
() (broadcast-only) |
| MTU | ~255 (SF/BW dependent) |
| State | null |
| Status | ⚪ planned (raw); see Reticulum/Meshtastic for framed radios |
| Code | SPI to an SX127x, or via an RNode (Reticulum) |
Deep dive
SPORE bridge mapping. U = (): every modem in range hears every frame, so
resolve is trivial and the envelope dest filters. Set MTU from the modem's
configured SF/BW/CR. Instead of driving the SX127x directly, the practical path is
an RNode in host mode — already implemented as
web/transports/reticulum.mjs.
Security. Raw LoRa is unencrypted broadcast; the envelope signature is the trust anchor.
References. Semtech SX1276 datasheet; LoRa PHY is proprietary to Semtech.
Ham AX.25 / KISS
Summary. AX.25 is the amateur-radio packet protocol; KISS is the minimal framing
between a host and a Terminal Node Controller (TNC). Together they move data over HF/
VHF/UHF radio across regional distances with no infrastructure — the classic
off-grid long-haul link. A TNC speaks KISS, which is already SPORE's stream
framing, so the bridge is only a matter of reaching one: over TCP (Direwolf's
KISSPORT, most networked TNCs) or over a serial port (hardware TNCs on USB).
| Field | Value |
|---|---|
| Driver form | stream (KISS) |
U |
() — the TNC decides who hears it |
| MTU | 256 (typical AX.25 paclen) |
| Bulk budget | 0 B/s — 1200-baud packet is ~150 B/s shared; carries messages, not files |
| State | stateful (the TNC link) |
| Status | 🧪 implemented — run_tcp / run_serial, not hardware-verified here |
| Code | bridge::ax25 (+ bridge::kiss_stream::KissStream) |
Deep dive
Protocol wire format. SPORE rides an AX.25 UI (unnumbered information) frame, and the host↔TNC link uses KISS:
AX.25 UI frame:
Flag(0x7E) · Dest addr(7) · Src addr(7) [· digipeaters…] · Control(0x03=UI) ·
PID(0xF0=no L3) · Info(= SPORE envelope) · FCS(2) · Flag(0x7E)
KISS host↔TNC frame (bytes on the serial line):
FEND(0xC0) · cmd(0x00=data, port 0) · …payload, 0xC0/0xDB escaped… · FEND(0xC0)
bridge::kiss_stream::KissStream and web/transports/kiss.mjs
implement the KISS layer byte-for-byte (src/kiss.rs is the source of truth).
SPORE bridge mapping. The TNC does the AX.25 framing/FCS; the bridge speaks KISS
to the TNC over serial/TCP. U = () for a broadcast beacon channel, or the sender's
call-ssid string if you parse the AX.25 header. UI frames are stateless — fit
SPORE's flood directly. Keep paclen ≤256; fountain-fragment above it.
Security. FCC/most regulators forbid encryption on amateur bands — so send
signed but unencrypted envelopes here (SPORE's SIGNED flag without
ENCRYPTED). The signature still authenticates; no message secrecy on-air by law.
References. AX.25 v2.2 (TAPR); KISS TNC protocol.
APRS
Summary. The Automatic Packet Reporting System layers position, telemetry, and short messages on AX.25 (and the APRS-IS internet backbone). Its addressed message format is a ready-made carrier for small signed envelopes.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
call-ssid |
| MTU | ~200 (message text) |
| State | stateless |
| Status | ⚪ planned |
| Code | APRS message frames over AX.25 / APRS-IS |
Deep dive
Protocol. An APRS message is :ADDRESSEE :text{msgNo. Base-91/base-64 the
envelope into the text (tiny — expect heavy fragmentation), or attach as a chunked
payload. Rides the AX.25 KISS path on RF, or a TCP socket to APRS-IS.
Security. Same amateur-band no-encryption rule as AX.25 — sign, don't encrypt.
References. APRS Protocol Reference 1.0.1.
DMR
Summary. Digital Mobile Radio (ETSI TS 102 361) is a common commercial/ham digital voice standard with a data plane. IP-over-DMR or short data services can carry envelopes over DMR repeaters and networks.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u32 (DMR ID) |
| MTU | variable (short data) |
| State | stateless |
| Status | ⚪ planned |
| Code | IP-over-DMR or DMR short-data via a modem/hotspot |
Deep dive
Protocol. DMR carries data in confirmed/unconfirmed short-data or packet-data
services; a hotspot (MMDVM) exposes it. U = u32 DMR ID. Bandwidth is small — keep
envelopes minimal.
Security. Amateur DMR: no encryption (sign only). Commercial DMR may encrypt at the radio layer.
References. ETSI TS 102 361 (DMR Air Interface, parts 1–4).
goTenna
Summary. A consumer LoRa-based mesh radio paired to a phone over BLE. Popular for off-grid group messaging; a bridge would relay envelopes through its SDK.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u32 (GID) |
| MTU | ~200 |
| State | stateless |
| Status | ⚪ planned |
| Code | goTenna SDK over the phone's BLE link |
Deep dive
Protocol. Proprietary — no public on-air wire format. The practical bridge
is through the vendor SDK on a paired phone: send/receive app payloads carrying the
envelope. U = u32 GID; group messages = broadcast.
References. goTenna developer SDK (vendor documentation; the radio protocol itself is closed).
Audio modem
Summary. Data-over-sound: the envelope is modulated to an audio tone stream and played through a speaker, then demodulated from a microphone. Two laptops on a table, a radio's speaker into another radio's mic, a phone across a room — an air-gapped, human-audible link that needs no radio licence and no network. Fully implemented and tested, with a browser twin bit-compatible with the native modem.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
() (broadcast-only) |
| MTU | 4 KB/frame |
| Bulk budget | 0 B/s — carries messages, announces and manifests; refuses file chunks |
| State | null |
| Status | ✅ implemented & tested |
| Code | bridge::audio (native), web/transports/audio.mjs (browser twin) |
Deep dive
Wire format (SPORE-defined). A 16-tone FSK modem, 4 bits/symbol, 48 kHz, 1024 samples/symbol:
per frame: SYNC(6 symbols) · LEN(2 bytes) · PAYLOAD(LEN bytes) · CRC(4 bytes)
SYNC = [15,0,15,0,12,3] (fixed sync word)
bytes = high nibble then low nibble, one 4-bit tone each
tone f = (32 + 4·symbol) · 48000/1024 Hz (1500 … 4312.5 Hz)
CRC = SHA-256(LEN‖PAYLOAD)[0..4]
The Rust and JS implementations are byte-identical: the browser twin's SHA-256
CRC and tone plan were verified against the native modem, and frames roundtrip
under noise. So a browser tab and a native spore-audio pipe exchange real
envelopes over the air.
Throughput and what it means for files. 16 tones × 4 bits at ~47 symbols/s is about 23 bytes per second, so a single 1336-byte file chunk would occupy the channel for a minute. Hence a bulk budget of zero: a sound link relays messages, announces and manifests at full speed — telling the mesh you exist and what you have, which is what an audio link is for — and declines to haul chunks. Because chunks are content-addressed, whoever wants them asks again and a faster path answers; nothing fails, it just routes around.
SPORE bridge mapping. U = (), broadcast-only; the streaming demodulator scans
for the sync word at sub-symbol resolution to tolerate lead-in silence and timing
offset. Native I/O is deliberately sound-card-agnostic: bridge::audio::run_pipe
reads/writes f32 PCM on stdin/stdout so any backend (sox, ffmpeg, pw-cat)
supplies the samples. The browser uses a ScriptProcessor on the mic and an
AudioBufferSource to the speakers.
Security. The channel is public (anyone in earshot). Confidentiality, if needed, comes from an encrypted envelope payload; authenticity always from the signature.
References. SPORE-native; see src/bridge/audio.rs and the ggwave project for
prior art in data-over-sound.
ICMP echo (ping)
Summary. Every IP host answers ping and almost every firewall passes it, so the echo payload is a carrier that reaches where a new port cannot: a captive portal that allows only "diagnostics", a host you can reach but not connect to. An envelope rides in the echo payload; a one-byte marker keeps the world's ordinary pings out of the router.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
Ipv4Addr |
| MTU | 1400 (one echo payload, no IP fragmentation) |
| State | stateless |
| Status | 🧪 codec tested; raw-socket runner is a Linux CAP_NET_RAW template |
| Code | bridge::icmp |
Deep dive: the covert-channel family (ping, DHCP, ARP, …)
bridge::icmp is the worked example of a broader idea: any protocol with a
field that holds arbitrary bytes is a carrier. SPORE only needs to move bytes,
and it assumes every link is hostile, so smuggling those bytes through a protocol
never meant for data costs nothing in security — the envelope is still signed and
sealed.
The module is split so the honest part is verifiable: the codec
(encode_echo / decode_echo, checksum and all) is pure and unit-tested,
including the odd-length and rejected-ordinary-ping cases; the runner needs a
raw socket, which needs CAP_NET_RAW and Linux, and cannot be exercised in CI —
so it is a template whose framing is tested and whose socket call is not. Grant it
without root: sudo setcap cap_net_raw+ep ./spore.
One detail the template gets right because it is easy to get wrong: a raw
IPPROTO_ICMP socket hands back the IP header, and that header is 20 bytes
only when it carries no options. ipv4_payload_offset reads the IHL field instead
of assuming 20 — otherwise every packet carrying record-route or timestamp options
(exactly what a "diagnostics only" network is prone to adding, which is the kind of
network this bridge exists for) would decode from four bytes off and fail the
checksum, invisibly. IHL is also a number chosen by whoever sent the packet, so it
is range-checked against the datagram rather than trusted as an index.
The same pattern extends to the rest of the family, each differing only in which field carries the bytes, and each a raw-socket (often L2, often root) runner over a tested codec:
- DHCP — a vendor/private option (e.g. 224–254) on the broadcast that every LAN already floods; no association needed.
- ARP — the sparest carrier: a broadcast that crosses no router, with only the frame's minimum-length padding to hide in (~18 bytes), so a fountain fragment at a time.
- DNS — a query name (base32 label) to a resolver you don't control, the classic egress from a filtered network.
These share one caution beyond the usual: they are conspicuous. A covert channel is a transport, not a cloak — traffic analysis sees a host that pings a lot. Use them to get out of somewhere restrictive, not to hide that you are communicating; §9 mix mode is the tool for the latter.
JANUS (underwater sonar)
Summary. JANUS is the NATO standard for underwater acoustic communication — the only practical way to move bits through water, where RF does not propagate. Extremely low bandwidth, high latency; for a beacon-sized signed envelope.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u8 or () |
| MTU | ~32 bytes |
| State | stateless |
| Status | ⚪ planned |
| Code | acoustic modem shim |
Deep dive
SPORE bridge mapping. Like the audio modem but through a water-coupled transducer at JANUS symbol rates. The ~32-byte MTU means only the smallest envelopes (or fountain fragments of them) fit.
References. NATO STANAG 4748 / JANUS; ANEP-87.
QR stream
Summary. Envelopes rendered as QR codes and read back by a camera — an optical, air-gapped, one-way channel that crosses a screen-to-camera or paper-to-camera boundary with no electronics in common. SPORE's text armor (the payload) is implemented; the camera/screen animation runner is the remaining glue.
| Field | Value |
|---|---|
| Driver form | dgram (one-way) |
U |
() |
| MTU | ~1 KB/code (version/ECC dependent) |
| State | null |
| Status | 🟡 partial — armor present, animated runner TODO |
| Code | armor::wrap/unwrap + a QR encoder/decoder |
Deep dive
Wire format. armor::wrap renders the envelope to checksummed base32 text
(SMS/paper-safe), which is then encoded into one or more QR symbols; a long
envelope becomes an animated sequence of fountain fragments the camera
reassembles. armor::unwrap validates the checksum and recovers the bytes.
SPORE bridge mapping. U = (), one-way broadcast (screen → camera). No
addressing: the reader takes whatever it can decode and hands it to the router,
which dedups by content-ID. Pair with the Seed Sheet for the
printed, fountain-coded variant.
References. ISO/IEC 18004 (QR Code);
SPORE armor module for the text layer.
Iridium SBD
Summary. Short Burst Data over the Iridium satellite constellation — global coverage, including the poles and mid-ocean, in small messages via a gateway. The last-resort long-haul link when nothing terrestrial is reachable.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
u32 (IMEI-derived) |
| MTU | 340 bytes (MO), 270 (MT) |
| State | stateless |
| Status | ⚪ planned |
| Code | SBD via a modem (AT commands) or the gateway API |
Deep dive
Protocol. A mobile-originated SBD message carries up to 340 bytes; the envelope (or a fountain fragment) is the payload, delivered through the Iridium gateway to an email/HTTP endpoint — so the far side is effectively a store carrier. Costly per message; use sparingly.
References. Iridium SBD Developers Guide (vendor); modem AT command set per manufacturer.
Meshtastic
Meshtastic — one codec, four pipes
Summary. Meshtastic is an open LoRa mesh firmware for cheap ESP32/nRF boards,
widely used for off-grid group text over kilometres. SPORE rides it by wrapping an
envelope in a Meshtastic MeshPacket on a private application port; the firmware
then floods it across the LoRa mesh like any other packet, and unwraps arriving
packets back into envelopes. One MeshPacket codec serves all four pipes — LAN
Wi-Fi-UDP, USB serial, browser Web Serial, and Bluetooth — so U (u32 node
number) and MTU (237) never change; only the transport under the codec differs.
| Field | Value |
|---|---|
| Driver form | dgram (UDP) / stream (serial, BLE) |
U |
u32 (Meshtastic node number) |
| MTU | 237 bytes (LoRa payload budget) |
| Bulk budget | 32 B/s (conservative default; raise per region/preset) |
| State | stateless (UDP) / stateful (serial, BLE) |
| Status | ✅ WiFi-UDP (bridge::meshtastic::run) · 🧪 USB serial (run_serial / run_pipe) · 🧪 Web Serial & BLE |
| Code | bridge::meshtastic (codec + UDP), web/transports/meshtastic.mjs |
Deep dive
Protocol wire format (mesh.proto, hand-rolled protobuf). The envelope rides a
Data sub-message on portnum 256 (PRIVATE_APP) inside a MeshPacket:
MeshPacket { Data (field 4, "decoded") {
1: from (varint, u32) 1: portnum = 256 (varint)
2: to (varint, u32) 2: payload = SPORE envelope (bytes)
4: decoded (Data, bytes) }
6: id (fixed32)
9: hop_limit (varint)
}
Serial pipe adds a stream frame: 0x94 0xC3 <len:u16 BE> <ToRadio|FromRadio proto>
host→device: ToRadio { 1: packet = MeshPacket }
device→host: FromRadio{ 2: packet = MeshPacket }
BLE pipe: write ToRadio to the ToRadio characteristic; read FromRadio on a
FromNum notification. Service 6ba1b218-…; ToRadio f75c76d2-…; FromRadio 2c55e69e-….
The MeshPacket encode/decode is a JS port of bridge::meshtastic, verified
byte-identical (a roundtrip in Node reproduces the exact Rust field layout: 0x08
from-varint, 0x22 decoded-len, 0x08 0x80 0x02 portnum=256, then the payload).
SPORE bridge mapping. U = u32; broadcast is node 0xFFFFFFFF. On Wi-Fi-UDP
the packet is a bare MeshPacket in a multicast datagram (224.0.0.69:4403), so it
uses the datagram driver directly; on serial/BLE it is wrapped in ToRadio/FromRadio
and framed as above. The node number is derived from the first four bytes of the
SPORE address. MTU 237 forces fountain fragmentation for anything larger. On serial/
BLE the connection is stateful — drop the neighbour on disconnect.
Security. This bridge handles only the unencrypted decoded variant; an
encrypted Meshtastic channel puts ciphertext in field 5 (AES-CTR with the channel
key), which the bridge would need that key to open — use an unencrypted channel,
and rely on SPORE's own signature/encryption inside the payload. Field numbers
follow mesh.proto; confirm against the firmware you target.
References. Meshtastic mesh.proto;
Meshtastic developer docs;
serial stream API.
Reticulum
Reticulum (RNS) — RNode host mode
Summary. Reticulum (RNS) is a cryptography-first networking stack that runs over almost any medium and addresses peers by a 16-byte destination hash, with its own end-to-end encryption and no dependence on IP or authorities. SPORE can ride Reticulum two ways: as an RNS payload addressed to a destination (the native interfaces, planned), or — implemented today — by driving an RNode LoRa modem in host/KISS mode directly, putting each envelope in a raw LoRa DATA frame. The RNode path is an honest radio driver, not a full RNS transport node: it moves envelopes over the same LoRa air Reticulum uses, but does not route RNS packets.
| Field | Value |
|---|---|
| Driver form | dgram (IP iface) / stream (RNode serial/BLE) |
U |
[u8;16] (RNS destination) or () (raw RNode broadcast) |
| MTU | 500 (RNS) / ~255 (LoRa PHY) |
| Bulk budget | 32 B/s (conservative default; the slowest interface on the path is what suffers) |
| State | stateless (RNS) / stateful (serial, BLE) |
| Status | 🧪 RNS payload (bridge::reticulum + companion, via stdio/TCP/UDP) · 🧪 Web Serial & BLE (RNode host mode) |
| Code | bridge::reticulum + tools/reticulum_companion.py; web/transports/reticulum.mjs |
Deep dive
Two models.
RNS payload (implemented, via companion). SPORE rides Reticulum as data on a shared PLAIN destination
spore.mesh— a broadcast bus every SPORE-over-RNS node listens on (a PLAIN destination's hash derives only from its name+aspects, so everyone computes the same one and hears every packet). RNS provides transport, path-finding, and every interface it is configured with (LoRa, TCP, I2P, packet radio). Because Reticulum's canonical implementation is the Pythonrnslibrary and its packet/crypto format is defined by that code, the bridge is split: the portable half (bridge::reticulum::run_pipe) exchanges KISS-framed envelopes on stdin/stdout exactly like the audio modem, and a small companion (tools/reticulum_companion.py) does the real RNS work with the library — so nothing security-critical is re-implemented. The node's MTU is clamped to a single RNS packet (383 B); larger objects fountain- fragment.Uis effectively()(the bus is broadcast; the envelopedestfilters). Verified off-network at the framing layer (the companion's KISS matchessrc/kiss.rsbyte-for-byte); the live RNS path needs a Reticulum network to exercise, hence 🧪.RNode host mode (implemented). An RNode is a LoRa modem speaking a KISS host protocol. Configure the radio, then every SPORE envelope is a KISS DATA frame:
KISS host frame: 0xC0 <cmd> …payload (0xC0/0xDB escaped)… 0xC0 cmd 0x00 = DATA payload = SPORE envelope cmd 0x01 = FREQUENCY (u32 BE, Hz) cmd 0x04 = SPREADING FACTOR (u8) cmd 0x02 = BANDWIDTH (u32 BE, Hz) cmd 0x05 = CODING RATE (u8) cmd 0x03 = TX POWER (u8, dBm) cmd 0x06 = RADIO STATE (u8, 1=on)A dedicated de-framer keeps the command byte and surfaces only DATA (0x00) frames as inbound envelopes; status frames are ignored. Over BLE the same frames ride the Nordic UART Service.
SPORE bridge mapping. For RNode, U = () (broadcast — every modem in range
hears the DATA frame; the envelope dest filters). The radio must be brought up
first (configure() sends FREQUENCY/BANDWIDTH/SF/CR/TXPOWER then RADIO_STATE=1) —
so the bridge exposes those as parameters; match your region and the other radios.
The connection is stateful (serial/BLE); drop the neighbour on disconnect. MTU
tracks the LoRa PHY (SF/BW dependent).
Security. RNS provides its own strong end-to-end encryption on the payload path; the RNode host path is a raw radio pipe with no medium encryption, so SPORE's signature (and optional encrypted payload) is the trust anchor. Either way the envelope is self-authenticating.
References. Reticulum Manual & protocol spec; RNS reference implementation; RNode firmware (host/KISS command set).
Internet overlays
These deliver packets over (or instead of) IP. The key insight: most carry IP, so the UDP bridge rides them unchanged — you only point it at the right address on the overlay's interface. Only the non-IP or browser-native ones need their own shim.
BATMAN-adv
Summary. B.A.T.M.A.N.-adv is a Linux kernel L2 mesh: nodes join a virtual switch
(bat0) that spans many radio hops as if it were one Ethernet segment. SPORE floods
it with the existing UDP broadcast bridge on bat0.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
[u8;6] |
| MTU | 1500 |
| State | stateless |
| Status | 🧪 implemented — udp::run_group bound to bat0's address |
| Code | bridge::udp::run_primary bound to bat0 |
Deep dive
SPORE bridge mapping. BATMAN-adv presents a normal L2 interface, so directed or
limited broadcast over UDP floods the whole mesh. No SPORE-specific code — select
the bat0 interface. U is the batman node MAC as seen by UDP.
References. batman-adv (Open Mesh); BATMAN V protocol.
Yggdrasil / cjdns
Summary. End-to-end encrypted IPv6 overlays where your address is derived from
your public key, self-organising into a global mesh. Because they present a normal
IPv6 tun, SPORE's UDP bridge rides them directly.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
Ipv6Addr |
| MTU | 1280 |
| State | stateless |
| Status | 🧪 implemented — udp::run_group binds the tun and floods ff02::7373 |
| Code | bridge::udp on the Yggdrasil/cjdns interface |
Deep dive
SPORE bridge mapping. Send UDP to peers' overlay IPv6 addresses (multicast/
link-local for discovery). The crypto-address means U is already key-bound at the
overlay layer, but SPORE still authenticates with its own signature. Keep to the
1280-byte IPv6 minimum MTU.
References. Yggdrasil; cjdns / Hyperboria.
Thread
Summary. Thread is a low-power 802.15.4 mesh presenting IPv6 (6LoWPAN) — common in smart-home devices. Envelopes ride UDP over the Thread network.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
Ipv6Addr |
| MTU | 1280 (IPv6; fragmented over 802.15.4) |
| State | stateless |
| Status | 🧪 implemented — udp::run_group on the mesh interface (IPv6 multicast) |
| Code | bridge::udp over the Thread interface |
Deep dive
SPORE bridge mapping. UDP to Thread mesh-local or link-local addresses; the border router bridges to wider IP if present. 802.15.4's tiny frames mean heavy 6LoWPAN fragmentation beneath — keep envelopes small.
References. Thread Specification (Thread Group) (membership-gated); OpenThread.
Tor (onion service)
Summary. Tor onion services give a location-hidden, end-to-end encrypted
rendezvous reachable by a .onion address with no public IP. A bridge would run a
SPORE relay behind an onion service so distant peers reach it without exposing a
network location.
| Field | Value |
|---|---|
| Driver form | stream |
U |
.onion address |
| MTU | 64 K (stream) |
| State | stateful |
| Status | 🧪 implemented — dial-out via SOCKS5 (reconnecting); inbound is a torrc onion service in front of bridge::tcp |
| Code | bridge::tor |
Deep dive
SPORE bridge mapping. Treat it as a byte stream: run the WebSocket
or a TCP bridge, but dial through Tor's SOCKS5 port and publish an onion service for
inbound. U = the peer's .onion. Framing is KISS or length-prefix as for any
stream.
Security. Tor provides transport anonymity and encryption; SPORE's signature is still required for authenticity.
References. Tor Rendezvous Spec v3; Tor SOCKS extensions.
I2P
Summary. The Invisible Internet Project: a garlic-routed anonymity overlay with its own datagram service (via SAM). A natural fit for SPORE's datagram model over an anonymous network.
| Field | Value |
|---|---|
| Driver form | stream (KISS over a SAM stream) |
U |
b32 destination |
| MTU | 1200 |
| State | stateful (SAM session + stream) |
| Status | 🧪 implemented — STREAM CONNECT out, STREAM ACCEPT in, reconnecting |
| Code | bridge::i2p |
Deep dive
SPORE bridge mapping. Open a SAM v3 DATAGRAM session; send/recv map
directly to I2P repliable datagrams, and the sender's b32 destination is U.
Fountain-fragment above ~1200 bytes.
References. I2P SAM v3; I2P datagrams.
iroh (QUIC)
Summary. iroh gives QUIC connections between endpoints identified by a public key, with hole punching and relay fallback when a direct path can't be found. That fills the gap between LAN UDP and Tor/I2P: internet-reachable peer paths without a stable public IP. Envelopes are KISS-framed on one bi-directional QUIC stream, exactly like the TCP/serial bridges.
| Field | Value |
|---|---|
| Driver form | stream (KISS over a QUIC bi stream) |
U |
iroh EndpointId (a public key) |
| MTU | large (QUIC stream); fountain-fragment applies as everywhere |
| State | stateful (endpoint + connection, reconnecting) |
| Status | 🧪 implemented — two-endpoint localhost round-trip in CI (iroh workflow); real NAT paths not yet exercised |
| Code | bridge::iroh, behind the bridge-iroh Cargo feature |
Enable it. Off by default. Build with --features bridge-iroh (needs Rust ≥ 1.91;
it is off the MSRV and default matrices and has its own CI job). Config lines:
bridges:
- iroh # listen; relay + discovery on, so NATed peers reach you
- iroh: <endpoint-id> # dial that peer, letting relay/discovery find it
- iroh: <endpoint-id>@127.0.0.1:5000 # dial with an explicit address, relay+discovery OFF (LAN)
The endpoint id is printed on start.
Deep dive
SPORE bridge mapping. An iroh Endpoint is bound once; the dialer opens a bi
stream to the peer's id, the listener accepts one, and the same
stream_link KISS framing and best-effort store-and-forward
semantics apply as on TCP. iroh is async (tokio); the seam is a private multi-thread
runtime whose stream halves are wrapped as blocking Read/Write, so the shared pump
drives them without knowing they are async. A dropped link reconnects with backoff; the
outbound queue drains when it returns.
Trust — read before pointing this at the internet.
EndpointIdis not a SPORE address. iroh authenticates the transport peer; SPORE's own seal/sign authenticates the message. Keep the layers separate — do not derive one identity from the other.- Relays are a phone-home. In the default (non–direct-only) mode the endpoint uses
the n0 relay + discovery infrastructure to hole-punch and to fall back to relaying.
A relay sees ciphertext only (envelopes are already sealed) but still sees your
IP, your peer's, and traffic timing. If that metadata matters, use
Tor/I2Pinstead, run your own relay, or use the direct-onlyid@addrform on a trusted LAN. - Not a substitute for the mesh. iroh is for when both ends can reach the network. Offline peers still use store-and-forward; iroh just injects envelopes into the same hub once a path exists.
References. iroh; iroh discovery & relays.
Veilid
Summary. A newer private-routed P2P framework (from the Cult of the Dead Cow) with a DHT and encrypted app-to-app messaging, designed for privacy without servers.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
node/route id |
| MTU | variable |
| State | stateless |
| Status | ⚪ planned |
| Code | Veilid app-message API |
Deep dive
SPORE bridge mapping. Use Veilid private routes for send/recv; the route or
node id is U. Veilid encrypts and anonymises the path; SPORE authenticates.
References. Veilid documentation; Veilid source.
libp2p (gossipsub)
Summary. The modular P2P stack behind IPFS and Ethereum. Its gossipsub pub/sub protocol is a ready-made flood/mesh overlay: publish to a topic, every subscriber receives it — very close to SPORE's own model.
| Field | Value |
|---|---|
| Driver form | stream (transports) / pub-sub |
U |
PeerId |
| MTU | variable |
| State | stateful |
| Status | ⚪ planned |
| Code | rust-libp2p gossipsub behaviour |
Deep dive
SPORE bridge mapping. Map a SPORE topic to a gossipsub topic and publish
envelopes as messages; inbound messages feed the router. libp2p handles peer
discovery (mDNS/DHT), transport (TCP/QUIC/WebRTC), and multiplexing. U = PeerId.
Because gossipsub already dedups and floods, this is one of the cleaner overlay
fits.
References. libp2p specs; gossipsub v1.1; rust-libp2p.
WebSocket
Summary. WebSocket upgrades an HTTP connection to a bidirectional binary frame channel — the lingua franca for browser-to-server real-time links, and reachable from Node and native clients too. SPORE's browser transport uses it to reach a relay or a peer, and it is the most-tested non-loopback link.
| Field | Value |
|---|---|
| Driver form | stream |
U |
connection handle |
| MTU | 64 K (practical frame) |
| State | stateful |
| Status | ✅ implemented & tested (JS; native shim TODO) |
| Code | web/transports/websocket.mjs |
Deep dive
Protocol wire format (RFC 6455). After the HTTP Upgrade handshake, each
message is a framed record: FIN+opcode(1) · MASK+len(1) · [ext len 2/8] · [mask key 4] · payload. SPORE uses one binary message (opcode 0x2) per
envelope — no additional framing, since WebSocket messages are already delimited.
SPORE bridge mapping. U is the socket; outbound queues until open, inbound
messages feed hub._rx. A relay is just a server that rebroadcasts each message to
the other sockets; a direct peer is a single socket. On close, forget the neighbour.
Tested end-to-end in web/test.mjs (loopback) and web/ws-test.mjs (a real ws
relay, A→relay→B).
Security. wss:// gives TLS transport security; SPORE's signature authenticates
the sender independent of the relay, so a malicious relay can drop or reorder but
not forge.
References. RFC 6455 (WebSocket); WHATWG WebSocket API.
WebTransport
Summary. A newer browser API over HTTP/3 (QUIC) offering both reliable streams and unreliable datagrams — the closest browser match to SPORE's datagram model, with lower latency and no head-of-line blocking.
| Field | Value |
|---|---|
| Driver form | dgram (datagrams) / stream |
U |
connection/session |
| MTU | ~1200 (QUIC datagram) |
| State | stateful |
| Status | ⚪ planned |
| Code | browser WebTransport shim |
Deep dive
SPORE bridge mapping. Use the datagram API for a true datagram bridge (envelope
per datagram) or a bidirectional stream with KISS framing. Needs an HTTP/3 server
endpoint. U is the session.
Note on references. WebTransport is not a numbered RFC yet — it is a W3C Working Draft plus IETF drafts, transported over HTTP/3. (Do not confuse it with RFC 9297, Proxying UDP in HTTP, which is a different mechanism.)
References. W3C WebTransport; draft-ietf-webtrans-http3; RFC 9000 (QUIC).
WebRTC DataChannel
Summary. WebRTC gives browsers a direct, encrypted peer-to-peer data channel after an out-of-band signaling handshake — no server in the path once connected. SPORE uses it for serverless links: two people exchange a short offer/answer blob by any channel (chat, QR, voice) and get a direct pipe.
| Field | Value |
|---|---|
| Driver form | stream |
U |
channel label / peer id |
| MTU | 16 K (safe SCTP message) |
| State | stateful |
| Status | 🧪 implemented, not automated-tested (needs a live peer) |
| Code | web/transports/webrtc.mjs (manualOffer/manualAnswer) |
Deep dive
Protocol wire format (RFC 8831). A data channel is SCTP over DTLS over ICE/
UDP. SPORE sends one binary message per envelope on an unordered, unreliable
channel ({ordered:false}) — matching flood semantics — capped at a safe SCTP
message size (~16 K; fountain-fragment above it). Signaling uses SDP offer/answer
(JSEP), which SPORE packs into a base64 blob for manual exchange; ICE is gathered
non-trickle so a single blob carries every candidate.
SPORE bridge mapping. U is the peer/channel; queue sends until open, feed
inbound to the hub, forget on close/failure. STUN (stun.l.google.com:19302) is
used for NAT traversal; no signaling server is required. The standalone node
also drives a WebTorrent swarm which negotiates WebRTC channels
automatically via trackers.
Security. DTLS-SRTP encrypts the channel; the DTLS fingerprint is exchanged in the SDP. SPORE's signature authenticates the application payload regardless.
Browser only, deliberately — there is no daemon WebRTC bridge. This is the
most-asked-for missing bridge, so the answer is recorded here rather than
rediscovered. A native Rust WebRTC stack means ICE, DTLS and SCTP; none of those
are small, and the lightest credible option (a sans-IO crate such as str0m) is
still the largest dependency this project would have taken. That is the same
budget that keeps TLS out of the tree and put NFC in the browser, and it is
the rule that selected every bridge on this page. Spending it here was considered
and declined.
What you lose is NAT traversal between two daemons with no reachable address. What
you have instead: Tor and I2P both traverse NAT and do it with
better metadata properties, and either is a torrc/SAM config away.
The path to a native half is QUIC, not WebRTC. The one place a native WebRTC
bridge would matter is browser ↔ native node — a browser has WebRTC but a daemon
does not, so the two cannot speak directly today. That gap is better closed with
WebTransport (QUIC datagrams in the browser) on the browser side
and the iroh QUIC path already merged in the core (src/direct/iroh.rs,
bridge-iroh) on the native side: the browser's QUIC becomes one more Direct
medium rather than a special case that pulls in an ICE/DTLS/SCTP stack. Native
WebRTC is therefore declined outright; a native WebTransport/QUIC adapter is the
planned answer to the browser↔native conformance gap (see Roadmap
M2). Until that adapter exists, the SPEC page-2 "native nodes run ice-lite" line
reads as more than the tree does, and says so.
The browser and the Android WebView keep WebRTC because there it costs nothing —
the platform ships the stack. Note that the Android app does not currently load
this transport (its WebView is given websocket, nostr and webtorrent), though
WebRTC demonstrably works there, since webtorrent negotiates data channels
itself. What is missing on Android is not the runtime but signaling: this transport
takes an already-open RTCDataChannel, and there is no human present to paste an
offer into a headless WebView.
References. RFC 8831 (data channels); RFC 8832 (DCEP); RFC 8829 (JSEP); W3C WebRTC.
WebTorrent swarm
Summary. WebTorrent is BitTorrent for the browser: peers find each other through WebSocket trackers and connect over WebRTC. SPORE reuses exactly that discovery to join a named swarm and gossip envelopes P2P — no torrent file, just a shared name hashed to an "infohash", and the same public trackers WebTorrent uses.
| Field | Value |
|---|---|
| Driver form | stream |
U |
peer id / channel |
| MTU | 16 K (WebRTC message) |
| State | stateful |
| Status | 🧪 implemented, not automated-tested (needs trackers + peers) |
| Code | web/transports/webtorrent.mjs |
Deep dive
Protocol. The bittorrent-tracker WebSocket protocol: announce with a batch
of WebRTC offers under a 20-byte infohash + peer id; the tracker relays a remote
peer's offer/answer between browsers; on data-channel open the peers exchange
SPORE envelopes over WebRTC directly. The infohash is SHA-256(name)[..20]
so any two nodes naming the same swarm meet; it will not collide with real torrents.
SPORE bridge mapping. Each open data channel is one neighbour; send broadcasts
an envelope to all channels, inbound feeds the hub. Peer discovery touches the
trackers, but traffic is peer-to-peer, so the swarm survives a tracker going down.
Default trackers: tracker.openwebtorrent.com, tracker.webtorrent.dev.
Security. WebRTC (DTLS) encrypts each link; the tracker only introduces peers. SPORE's signature authenticates.
References. WebTorrent; bittorrent-tracker (ws protocol); BEP 3 (BitTorrent); RFC 8831.
Web Serial / USB
Summary. The Web Serial API lets a browser tab open a USB/serial port with the user's permission — so a page can drive a physical TNC, RNode, or ESP32 with no native app. SPORE frames envelopes with KISS, identical to the native serial bridge.
| Field | Value |
|---|---|
| Driver form | stream |
U |
port / connection |
| MTU | variable (KISS frame) |
| State | stateful |
| Status | 🧪 implemented, not hardware-tested |
| Code | web/transports/webserial.mjs + kiss.mjs |
Deep dive
Wire format. KISS (0xC0 0x00 …escaped… 0xC0), byte-for-byte with
src/kiss.rs, so a browser tab and a physical board interoperate. See
AX.25 / KISS for the frame detail. (Meshtastic and RNode use their own
serial framings — see Meshtastic and Reticulum.)
SPORE bridge mapping. A stateful stream: a KissDeframer reassembles frames
across reads; U is the open port; closing the port forgets the neighbour. The user
gesture (navigator.serial.requestPort()) is required to open, so it cannot
auto-reconnect silently.
References. W3C Web Serial API; KISS.
Web Bluetooth
Summary. Web Bluetooth lets a tab talk to a BLE device over GATT. SPORE targets the Nordic UART Service — the de-facto serial-over-BLE profile on hobby radios — and KISS-frames envelopes, chunked to the BLE payload size.
| Field | Value |
|---|---|
| Driver form | stream |
U |
device / connection |
| MTU | ~247 (chunked to ~20 pre-negotiation) |
| State | stateful |
| Status | 🧪 implemented, not hardware-tested |
| Code | web/transports/webbluetooth.mjs |
Deep dive
Wire format. KISS over the Nordic UART Service (6e400001-…, RX …0002
write, TX …0003 notify); frames are chunked to ≤20 bytes and reassembled by the
KissDeframer. See BLE GATT for the GATT detail.
SPORE bridge mapping. U is the connected device; notifications deliver inbound
frames, writes carry outbound (serialised, chunked). Requires a user gesture to pair;
disconnect forgets the neighbour.
References. W3C Web Bluetooth; Bluetooth GATT.
Store-and-forward & app carriers
Systems that already store a message and pass it on — the pattern SPORE is. An envelope becomes a file, an event, a chat message, or a bundle; the container replicates it, and the other side unwraps it. These compose especially cleanly because SPORE's content-addressing dedups whatever the carrier duplicates.
Folder / USB / Syncthing
Summary. The simplest carrier: write each envelope as a *.spore file in a
directory, and let anything that replicates directories — Syncthing, a shared drive,
a USB stick carried between machines ("sneakernet") — move them. Delay-tolerant,
infrastructure-free, and works across an air gap.
| Field | Value |
|---|---|
| Driver form | store |
U |
— (no addressing) |
| MTU | — (whole envelopes as files) |
| State | store |
| Status | ✅ implemented & tested |
| Code | bridge::store |
Deep dive
Wire format. One envelope per file, named by its content-ID (<id>.spore); the
file body is the raw envelope bytes. No framing — the filesystem delimits.
SPORE bridge mapping. Poll the directory for new files → feed the router;
write outbound forwards as new files. There is no U and no MTU: a whole envelope
is one file. Deduplication is free (content-ID = filename), so re-syncing the same
folder never reprocesses. Syncthing/rsync/Dropbox/a USB stick are all just
"the directory replicator".
Security. Filesystem permissions bound access; the envelope signature authenticates. An encrypted payload keeps contents private on a shared drive.
References. SPORE-native (bridge::store); Syncthing
as one replicator.
HTTP bag
Summary. A "bag" is an HTTP endpoint that holds a pile of envelopes; nodes pull what they don't have. Any web host, object store, or static file server becomes a SPORE drop-box — trivial to deploy and to reach through firewalls that only allow HTTP.
| Field | Value |
|---|---|
| Driver form | store |
U |
connection |
| MTU | 64 K |
| State | stateful (per request) |
| Status | ✅ implemented & tested |
| Code | bridge::bag |
Deep dive
Protocol. Plain HTTP: GET the bag to list/fetch envelopes, POST/PUT to
deposit. Each envelope is an opaque blob keyed by content-ID, so a static file
server (one file per id) or a tiny dynamic endpoint both work. Pull-based: a node
fetches ids it lacks (mirroring SPORE's INV/WANT sync).
SPORE bridge mapping. store form — poll the bag, diff against the local store,
fetch missing, deposit new. No addressing beyond the URL. Combine with the
content-ID dedup so repeated pulls are cheap.
Security. https:// for transport; the signature authenticates each envelope.
A public bag is a public channel — encrypt payloads that need privacy.
References. RFC 9110 (HTTP semantics);
SPORE-native (bridge::bag).
Copyparty
Summary. Copyparty is a portable file server with HTTP/WebDAV upload, a browser UI, and no database. Pointed at a share, it becomes a self-hosted HTTP bag with a nice UI and resumable uploads.
| Field | Value |
|---|---|
| Driver form | store |
U |
URL |
| MTU | 64 K |
| State | stateless (poll + PUT) |
| Status | 🧪 implemented — http:// shares; put TLS in a tunnel (see below) |
| Code | bridge::copyparty |
Deep dive
SPORE bridge mapping. Identical to the HTTP bag, using copyparty's upload/list endpoints (or WebDAV). Envelopes are files in the share.
References. copyparty.
Text armor (SMS / paper / voice)
Summary. When the only channel is text a human can copy — an SMS, a printed page, a read-aloud string — an envelope is rendered as checksummed base32 that survives transcription. The lowest-tech carrier there is, and the substrate for the QR stream and the printed Seed Sheet.
| Field | Value |
|---|---|
| Driver form | store (one-shot text) |
U |
— |
| MTU | ~150 chars (SMS-friendly) |
| State | null |
| Status | ✅ implemented & tested |
| Code | armor::wrap / armor::unwrap |
Deep dive
Wire format. armor::wrap emits the envelope as base32 with a short checksum
and human-readable delimiters; armor::unwrap validates and recovers the bytes.
Long envelopes split into numbered fountain fragments so several SMS / lines / cards
reassemble. Chosen to be case- and whitespace-tolerant for voice/paper transcription.
SPORE bridge mapping. No addressing; the reader hands whatever it decodes to the
router (content-ID dedups). This is the store pattern degenerated to a single text
blob.
Security. The channel is whatever carried the text (often public); the checksum guards transcription errors, the signature guards authenticity.
References. SPORE-native (armor module); base32 RFC 4648.
Nostr
Summary. Nostr is a minimal relay-based protocol: clients publish signed JSON events to relays, and subscribers pull them by filter. Any relay becomes a SPORE drop-box, and there are hundreds of open ones — a low-friction public carrier. The browser transport is implemented (inbound works with no key; outbound needs a signer).
| Field | Value |
|---|---|
| Driver form | store |
U |
relay |
| MTU | variable |
| State | stateless |
| Status | 🧪 implemented (JS), not live-tested against relays |
| Code | web/transports/nostr.mjs |
Deep dive
Protocol (NIP-01). A SPORE envelope is base64'd into the content of a
kind-30078 (parameterised replaceable app-data) event tagged d: spore-v1;
["REQ", …, {kinds:[30078], "#d":["spore-v1"]}] subscribes, ["EVENT", …]
publishes. Any relay that accepts kind-30078 works.
SPORE bridge mapping. store form over a WebSocket to the relay. Inbound needs
no key (just subscribe and unwrap the base64); outbound requires a Nostr signer
(NIP-07 window.nostr, or a local key) to sign the wrapping event — the envelope's
own SPORE signature is separate and inside. U is the relay.
Security. The wrapping Nostr event is signed by the poster's Nostr key; the SPORE envelope inside is signed by the SPORE identity. A public relay is a public channel.
References. NIP-01; NIP-78 (app data, kind 30078); NIP-07 (signer).
SSB (Secure Scuttlebutt)
Summary. SSB is an offline-first social protocol built on per-identity append- only logs that gossip peer-to-peer (and via pubs/rooms). It is delay-tolerant and authority-free by design — a natural SPORE carrier. The codec and a folder-log runner are implemented.
| Field | Value |
|---|---|
| Driver form | store |
U |
feed id |
| MTU | variable |
| State | stateless |
| Status | 🟡 partial — bridge::ssb codec + folder-log runner |
| Code | bridge::ssb |
Deep dive
Protocol. Each envelope becomes a message with spore-v1 typed content appended
to the local feed; SSB's gossip replicates the log to peers, who extract the
envelope. U is the feed id.
SPORE bridge mapping. store form: append outbound envelopes to the log, scan
inbound messages of type spore-v1 for envelopes to feed the router. The folder-log
runner reads/writes an on-disk log so it works without a full SSB stack.
Security. SSB messages are signed and hash-chained by the feed identity; the SPORE envelope carries its own signature independently.
References. SSB Protocol Guide; SSB specs.
Matrix
Summary. Matrix is a federated real-time messaging protocol; a room is a replicated event log across homeservers. Envelopes ride as custom room events, reaching anyone in the room across the federation.
| Field | Value |
|---|---|
| Driver form | store |
U |
room id |
| MTU | large |
| State | stateless |
| Status | ⚪ planned |
| Code | Client-Server API against a homeserver |
Deep dive
SPORE bridge mapping. Send a custom event type (e.g. m.spore) with the base64
envelope as content via PUT /rooms/{id}/send/...; sync to receive. U is the room
id. The homeserver stores and federates.
References. Matrix Client-Server API; Matrix specification.
XMPP / Jabber
Summary. The long-standing federated messaging standard. Envelopes ride message stanzas (or PubSub nodes) between accounts across any XMPP servers.
| Field | Value |
|---|---|
| Driver form | stream |
U |
JID |
| MTU | large |
| State | stateful |
| Status | ⚪ planned |
| Code | XMPP client (stanzas or PEP/PubSub) |
Deep dive
SPORE bridge mapping. Base64 the envelope into a <message> body or a custom
child element, or publish to a PubSub node for one-to-many. U is the JID. The
XMPP server stores-and-forwards to offline contacts (MAM/offline storage).
References. RFC 6120 (XMPP Core); RFC 6121 (IM); XEP-0060 PubSub.
DeltaChat (email)
Summary. DeltaChat turns ordinary e-mail (IMAP/SMTP) into a chat network with Autocrypt end-to-end encryption. Riding it means envelopes travel as e-mail — the most universally deployed store-and-forward system on Earth.
| Field | Value |
|---|---|
| Driver form | store |
U |
e-mail address |
| MTU | large |
| State | stateless |
| Status | ⚪ planned |
| Code | IMAP/SMTP client (or the DeltaChat core) |
Deep dive
SPORE bridge mapping. Attach the envelope (or base64 it in the body) to a mail
with a marker header/subject; poll IMAP for incoming, send via SMTP. U is the
address. Mail servers provide global store-and-forward and offline delivery.
References. DeltaChat; Autocrypt; RFC 5321 SMTP / RFC 3501 IMAP.
Session (Oxen)
Summary. Session is an anonymous messenger routing through the Oxen service-node network with onion routing and offline storage — no phone number, no central server.
| Field | Value |
|---|---|
| Driver form | store |
U |
session id |
| MTU | variable |
| State | stateless |
| Status | ⚪ planned |
| Code | Session/Oxen storage-server client |
Deep dive
SPORE bridge mapping. Store envelopes to a recipient's swarm via the storage
server API; poll to receive. U is the session id (a public key). Onion routing
anonymises; SPORE authenticates.
References. Session protocol / whitepaper; Oxen docs.
Briar
Summary. Briar is a censorship-resistant messenger that syncs directly between contacts over Tor, Wi-Fi, or Bluetooth — no servers at all. Its friend-to-friend sync is a SPORE carrier that works even in a blackout.
| Field | Value |
|---|---|
| Driver form | stream |
U |
contact |
| MTU | variable |
| State | stateful |
| Status | ⚪ planned |
| Code | Briar transport plugin |
Deep dive
SPORE bridge mapping. Ride Briar's contact sync as a client (or a custom
transport plugin), moving envelopes between mutually-added contacts over whichever
link (Tor/Wi-Fi/BLE) is available. U is the contact.
References. Briar; Bramble protocols.
Tox
Summary. Tox is a serverless, encrypted P2P messenger over a DHT. Friends connect directly; a bridge would carry envelopes over Tox lossless packets.
| Field | Value |
|---|---|
| Driver form | dgram |
U |
ToxID |
| MTU | ~1200 |
| State | stateful |
| Status | ⚪ planned |
| Code | toxcore client (custom lossless packets) |
Deep dive
SPORE bridge mapping. Send envelopes as custom lossless packets to connected
friends; U is the ToxID. The DHT handles discovery and NAT traversal.
References. toxcore; Tox spec (archived).
DTN / Bundle Protocol v7
Summary. The Delay/Disruption-Tolerant Networking Bundle Protocol is the IETF standard for networks with long delays and no end-to-end path — designed for deep space and disaster zones. It is store-carry-forward at the network layer, so SPORE maps onto it almost exactly.
| Field | Value |
|---|---|
| Driver form | store |
U |
EID (endpoint id) |
| MTU | variable (bundles) |
| State | stateless |
| Status | ⚪ planned |
| Code | a BPv7 agent (e.g. ION, µD3TN) client |
Deep dive
Protocol. A bundle carries a primary block (source/destination EID, lifetime,
timestamps) plus payload and extension blocks (CBOR-encoded). SPORE puts the envelope
in the payload block; the DTN agent handles custody transfer and forwarding
across intermittent links. U is the destination EID.
SPORE bridge mapping. store form over a bundle agent's API: submit envelopes as
bundle payloads, register to receive. SPORE's own expiry maps onto bundle lifetime;
its content-ID dedups replays. This is arguably the most natural fit in the whole
matrix — two store-carry-forward systems stacked.
References. RFC 9171 (Bundle Protocol v7); RFC 9172 (BPSec); RFC 4838 (DTN architecture).
NNCP
Summary. NNCP (Node-to-Node Copy) is a modern suite for secure, offline store-and-forward: encrypted packets moved by any transport (files, USB, email, online) between named nodes. A clean, security-first carrier.
| Field | Value |
|---|---|
| Driver form | store (spool) |
U |
node id |
| MTU | variable |
| State | stateless |
| Status | 🧪 via bridge::spool — the SPORE side is tested; the NNCP wiring is config |
| Code | bridge::spool |
Deep dive
SPORE writes outbound envelopes into a tx/ directory and consumes inbound ones
from rx/ (bridge::spool); NNCP is what carries tx
to the peer and fills rx. That split is exactly NNCP's own model, so the wiring
is NNCP config, not code:
# outbound: package the tx directory to a node, on a schedule or when it fills
nncp-file ./spool/tx/ node:spore-in
# inbound: nncp-toss unpacks received packets into the peer's rx directory
spore "spool: ./spool/tx -> ./spool/rx"
nncp-xfer (USB), nncp-daemon/nncp-caller (online) or nncp-file over any
medium then move the packets. NNCP adds its own strong crypto on top of SPORE's,
and the spool bridge validates every inbound file against its content id, so a
tampered packet is dropped rather than trusted.
References. NNCP.
UUCP
Summary. The original Unix-to-Unix Copy — the store-and-forward system that ran early e-mail and Usenet over dial-up. Still runnable, and a fittingly robust, low-tech carrier.
| Field | Value |
|---|---|
| Driver form | store (spool) |
U |
host name |
| MTU | variable |
| State | stateless |
| Status | 🧪 via bridge::spool — the SPORE side is tested; the UUCP wiring is config |
| Code | bridge::spool |
Deep dive
The same bridge::spool as NNCP, moved by UUCP instead:
uucp the tx/ files to the peer's rx/, over serial, TCP or sneakernet.
# outbound: copy tx to the peer (a cron, or uucico on a schedule)
uucp ./spool/tx/* peer!~/spore-in/
spore "spool: ./spool/tx -> ./spool/rx"
U is the UUCP host name. The spool bridge is medium-blind — it never learns
whether UUCP moved the files over a modem or a fibre, which is the point.
References. Taylor UUCP.
Serval Rhizome
Summary. Rhizome is the store-and-forward layer of the Serval Project's mesh (for phones without infrastructure): bundles propagate opportunistically as devices meet. Purpose-built for the same scenarios SPORE targets.
| Field | Value |
|---|---|
| Driver form | store |
U |
SID (Serval identity) |
| MTU | variable |
| State | stateless |
| Status | ⚪ planned |
| Code | Rhizome bundle store client |
Deep dive
SPORE bridge mapping. Publish envelopes as Rhizome bundles (manifest + payload);
the mesh carries them device-to-device. U is the SID. Content-addressing lines up
with SPORE's own.
References. Serval Project — Rhizome.
Hypercore / Hyperswarm
Summary. Hypercore is a secure append-only log with peer-to-peer replication over Hyperswarm's DHT (the "Dat"/Holepunch stack). A SPORE feed maps neatly onto a Hypercore log.
| Field | Value |
|---|---|
| Driver form | stream |
U |
public key |
| MTU | variable |
| State | stateful |
| Status | ⚪ planned |
| Code | Hypercore append + Hyperswarm replication |
Deep dive
SPORE bridge mapping. Append each envelope as a Hypercore block; replicate over
Hyperswarm to subscribers who read the log. U is the core's public key. The log's
integrity is built-in; SPORE adds sender authenticity.
References. Hypercore (Holepunch); Hyperswarm.
Earthstar / Willow
Summary. Willow (and its predecessor Earthstar) is an offline-first sync protocol with fine-grained capabilities and eventual consistency — data lives in "shares" that peers reconcile when they meet. A modern, privacy-conscious store carrier.
| Field | Value |
|---|---|
| Driver form | store |
U |
share / namespace |
| MTU | variable |
| State | stateless |
| Status | ⚪ planned |
| Code | Willow/Earthstar replica sync |
Deep dive
SPORE bridge mapping. Write envelopes as documents in a share; peers reconcile
the share on contact and extract them. U is the share/namespace. Willow's range-
based set reconciliation is efficient for large stores.
References. Willow Protocol; Earthstar.
Appendices
A. MTU reference
The bridge clamps the node's mtu to this; SPORE's fountain fragmenter splits
larger objects automatically. Values are the practical per-frame budget.
| MTU | Media |
|---|---|
| ~32 B | JANUS (underwater) |
| 51–222 B | LoRaWAN |
| ~54 B | Z-Wave |
| ~104 B | Zigbee |
| ~200 B | APRS, goTenna |
| ~247 B | BLE GATT / Web Bluetooth |
| 250 B | ESP-NOW |
| ~255 B | LoRa P2P, NFC |
| 256 B | AX.25 (paclen) |
| 237 B | Meshtastic |
| 340 B | Iridium SBD |
| 380 B | BLE Mesh |
| 500 B | Reticulum |
| ~1200 B | I2P, Tox, QUIC datagrams (WebTransport) |
| 1280 B | Yggdrasil, cjdns, Thread (IPv6 minimum) |
| 1400 B | UDP (SPORE default under Ethernet) |
| 1500 B | Ethernet, BATMAN-adv, Wi-Fi Direct |
| 2304 B | Wi-Fi 802.11 |
| 4 KB/frame | Audio modem |
| 16 K | WebRTC, WebTorrent |
| 64 K | WebSocket, HTTP bag, Tor |
| — (whole file) | Folder, and most store carriers |
B. Address-type (U) reference
U is what Neighbors<U> snoops and resolves. Null () media skip resolution
entirely (broadcast; the envelope dest filters).
U type |
Media |
|---|---|
() (null) |
Audio, LoRa P2P, QR, RNode broadcast, LiFi |
SocketAddr |
UDP |
Ipv4Addr |
Wi-Fi Direct |
Ipv6Addr |
Yggdrasil, cjdns, Thread |
[u8;6] (MAC) |
Ethernet, Wi-Fi, ESP-NOW, BLE GATT, BATMAN-adv |
[u8;7] |
NFC (UID) |
[u8;16] |
Reticulum (RNS destination) |
u8 |
Z-Wave, JANUS |
u16 |
BLE Mesh |
u32 |
Meshtastic, IrDA, DMR, goTenna, LoRaWAN, Iridium |
u64 |
Zigbee |
String / id |
AX.25, APRS, WebRTC, WebTorrent, PeerId, JID, .onion, b32, feed/room/session ids |
| connection | WebSocket, WebTransport, Web Serial, HTTP bag, TCP, Tor |
| — | Folder, Text armor (no addressing) |
C. Driver form comparison
dgram |
stream |
store |
|
|---|---|---|---|
| Delivery | fire-and-forget frames | ordered byte stream | shared container |
| Framing | one envelope per frame | needs a framer (KISS) | one envelope per item/file |
| Implement | DatagramTransport (recv/send) |
reader/writer + framing | poll + write |
| Neighbour | snoop source U |
live connection, forget on close |
none (content-ID dedup) |
| Broadcast | native (send(None, …)) |
per-connection fan-out | everyone reads the container |
| Reference | bridge::udp, bridge::meshtastic |
bridge::tcp, kiss_stream |
bridge::store, bridge::bag, bridge::ssb |
| Best for | radios, LAN, mesh | connections, tunnels, serial | delay-tolerant, offline, app carriers |
D. Protocol specification index
Authoritative references, grouped. (Where a protocol has no public wire spec, the canonical implementation is linked in its section instead.)
E. Bridge implementation checklist
Step-by-step guide for building a new bridge
Building a new bridge? Work down this list:
- Pick the form. Frames →
dgram; byte stream →stream; shared container →store. (See Driver form comparison.) - Pick
U. How does the medium name a peer? Use()if it is broadcast-only. - Set the MTU. The per-frame payload budget; the fountain fragmenter handles the rest. (See MTU reference.)
- Write
recv/send— the only platform-specific code.dgram: implementdriver::DatagramTransportand hand it todriver::run_datagram.stream: add framing (reusekiss_stream).store: poll + write items. - Handle state. Stateful media call
Neighbors::forgeton disconnect; stateless media rely onexpire. - Never re-implement routing, dedup, fragmentation, signing, or encryption — they live in the shared node and run identically on every medium.
- Respect medium law. On amateur bands, send signed-but-unencrypted envelopes.
- Document it here. Add an index row and a deep-dive section to this file, with
the correct status emoji and a real specification reference — the docs-sync guard
(
scripts/check_docs_sync.py) checks that every implemented bridge appears here and that every file/module this doc cites exists.
This reference is kept in sync with the code by scripts/check_docs_sync.py
(run in CI): every transport in web/transports/ and every runnable bridge in
src/bridge/ must be documented here, and every path or bridge:: module this
document cites must exist. A bridge and its description cannot drift apart without
failing the build.
WebTransport
WebTransport bridge - Browser API for HTTP/3 communication
Status: 🟢 implemented (browser side)
WebTransport is a browser API for sending and receiving data over HTTP/3. SPORE uses it to connect browsers to native nodes via a proxy that terminates TLS and forwards UDP.
Bridge mapping
- Form: Datagram (
dgram) - Underlay address (
U):Url(the WebTransport URL) - MTU: 1400 bytes (same as UDP)
Implementation
The browser-side implementation is in web/transports/webtransport.mjs. It:
- Opens a WebTransport session to the proxy URL
- Uses the datagram API for bidirectional communication
- Bridges datagrams to the SPORE hub interface
The native side requires a proxy that:
- Terminates TLS (Caddy/Nginx)
- Forwards WebTransport datagrams to UDP port 7439
- Runs on port 443 (browser requirement)
Browser --WebTransport (HTTPS)--> Proxy --plaintext UDP--> SPORE Daemon
Browser <--WebTransport datagrams-- Proxy <--UDP responses-- SPORE Daemon
See Proxy setup for sample configurations.