Files
fips/docs/design/fips-transport-layer.md
T
Johnathan Corgan 5abf9a9325 docs: four-section /docs/ restructure with new-user content, accuracy pass, and gateway feature-set rewrite
Restructures /docs/ by reader purpose (tutorials, how-to,
reference, design), adds the new-user-progression and
operator-recipe content the prior layout lacked, runs an
accuracy pass against current source across the pre-existing
design docs, and rewrites the gateway feature-set documentation
end-to-end around its actual operational profile (a niche
feature designed for systems already serving DHCP/DNS to a
LAN, with two independent halves — outbound LAN→mesh, inbound
mesh→LAN — sharing one nftables table, one binary, and one
control socket). Top-level README and getting-started rewritten
around two equally-weighted deployment modes (overlay on
existing IP networks; ground-up over non-IP transports).

## Additions

- 11 new tutorials in docs/tutorials/: an 8-step new-user
  progression from single-daemon test-mesh peering through
  to a ground-up two-device mesh, an IPv6-adapter side-trip
  walkthrough, an Advanced Tutorials index, and a hand-held
  OpenWrt walk-through for fips-gateway deployment that
  exercises both halves of the feature.
- 12 new how-tos in docs/how-to/: firewall activation,
  Nostr discovery (resolve / advertise / open across five
  scenarios), Tor onion (directory + control_port modes),
  UDP buffer tuning, unprivileged-user setup, persistent
  identity, host aliases, Bluetooth LE peering, MTU
  diagnostics, manual Linux-host gateway deployment (covers
  both halves), gateway troubleshooting (organised by half),
  and a section index.
- 9 new reference docs in docs/reference/: configuration,
  wire formats, control-socket protocol, four CLI references
  (fips, fipsctl, fipstop, fips-gateway), security posture
  matrix, and Nostr events catalog. Configuration and
  wire-formats are renamed-and-extended from prior design/
  versions; the other seven are net-new.
- 6 new design docs: fips-concepts, fips-architecture, and
  fips-prior-work split out of the deleted fips-intro.md;
  consolidated fips-mmp and fips-mtu aggregations; and a
  new generic port-advertisement-and-nat-traversal doc
  (Nostr-signaled port advertisement plus UDP NAT-traversal
  protocol, FIPS as an example implementation, suitable for
  eventual NIP submission).
- Top-level docs/getting-started.md walking through the
  binary-installer-only Install story.
- packaging/common/hosts pre-populated with the eight public
  test-mesh nodes so shortnames resolve out of the box on
  every fresh install.

## Changes

- 23 wire-format diagrams relocated to reference/diagrams/
  alongside the wire-formats move.
- 4 design diagrams corrected against source code
  (fips-protocol-stack, fips-identity-derivation,
  fips-coordinate-discovery, fips-routing-decision).
- 10 pre-existing design docs reconciled with current
  source. Numeric corrections: stale link-MMP report bounds
  (now [1s, 5s] with 200 ms cold-start floor); UDP default
  MTU (now 1280, IPv6 minimum); node_addr formula
  (SHA-256(pubkey)[..16]); Noise patterns (IK at link, XK
  at session); peer-ACL semantics (strict allowlist requires
  ALL in peers.deny); daemon DNS upstream ([::1]:5354);
  on-the-wire bloom-filter size (1,071 bytes); obsolete
  Cargo-feature references (PR #79 dropped them) removed.
- Transport framing tightened across the docs: TCP is for
  UDP-filtered networks (not NAT traversal); Tor is a
  deployment mode (not failover); WebSocket dropped (not a
  shipped FIPS transport); WiFi promoted to Implemented via
  Ethernet in infrastructure mode; classic-Bluetooth row
  removed (BLE is the only Bluetooth-mode transport).
- docs/design/fips-gateway.md rewritten end-to-end to lead
  with the niche-feature framing and the two-halves
  structure. Title moved from "FIPS Outbound LAN Gateway"
  to "FIPS Gateway"; architecture section describes the
  common machinery (the fips-gateway service, the nftables
  table, the control socket) before splitting into separate
  "Outbound Half" and "Inbound Half" sections of equal
  weight; security considerations split per-half; no Future
  Work section (speculative directions live in the project
  tracker, not in protocol design docs). Inbound port
  forwarding is a first-class half rather than a buried
  "Implemented Extensions" subsection.
- Gateway terminology unified across all gateway docs as a
  separate Linux service running alongside the fips daemon
  (its own systemd unit / OpenWrt init script). Container-
  pattern terms (sidecar) are reserved for the
  Docker/Kubernetes sidecar deployment examples — the
  testing/sidecar/ tree, examples/k8s-sidecar/,
  examples/sidecar-nostr-relay/,
  examples/wireguard-sidecar-macos/, and the related
  CHANGELOG / top-level README entries — where the term
  carries its standard container meaning.
- Net-new design body content: rekey section in
  fips-mesh-layer (Noise IK msg1/msg2 over the established
  link, K-bit cutover, drain window, smaller-NodeAddr-wins
  tie-breaker on dual-init); Mesh Size Estimation and
  Antipoison FPR Cap sections in fips-bloom-filters;
  Mesh-Interface Query Filter subsection in
  fips-ipv6-adapter; failure-suppression knobs and clock-
  skew tolerance in fips-nostr-discovery; loop-rejection
  and mid-chain ancestor swap added to spanning-tree
  propagation / stability rules; Priority Chain in
  fips-mesh-operation renumbered to match the
  routing-decision diagram.
- Top-level README: dropped the stale nostr-discovery
  cargo-feature parenthetical. docs/README.md and the four
  section READMEs (tutorials, how-to, reference, design)
  refreshed for the new structure; index rows reflect both
  halves of the gateway feature and the new fips-gateway
  CLI reference.
- Cargo.toml [package.metadata.deb] assets path updated for
  the fips-security.md move; .gitignore /reference/ rule
  anchored to repo root so docs/reference/ is trackable.
- packaging/openwrt-ipk/files/etc/fips/fips.yaml
  configuration-doc URL updated to the new
  docs/reference/configuration.md location.

## Deletions

- docs/design/fips-intro.md (split into the three new intro
  design docs).
- docs/design/document-relationships.svg (orphan, no longer
  referenced).
- docs/proposals/ tree removed; the only proposal it
  contained (the Nostr UDP hole-punch protocol) was
  rewritten as the new generic
  design/port-advertisement-and-nat-traversal.md.
2026-05-08 03:02:12 +00:00

36 KiB
Raw Blame History

FIPS Transport Layer

The transport layer is the bottom of the FIPS protocol stack. It delivers datagrams between transport-specific endpoints over arbitrary physical or logical media. Everything above — peer authentication, routing, encryption, session management — is built on the services the transport layer provides.

Role

A transport is a driver for a particular communication medium: a UDP socket, an Ethernet interface, a serial line, a Tor circuit, a radio modem. The transport layer's job is simple: accept a datagram and a transport address, deliver the datagram to that address, and push inbound datagrams up to the FIPS Mesh Protocol (FMP) above.

The transport layer deals exclusively in transport addresses — IP:port or hostname:port addresses, MAC addresses, .onion identifiers, radio device addresses. These are opaque to every layer above FMP. The mapping from transport address to FIPS identity happens at the link layer after the Noise IK link handshake completes. The word "peer" belongs to the link layer and above; the transport layer knows only about remote endpoints identified by transport addresses.

A single transport instance can serve multiple remote endpoints simultaneously — a UDP socket exchanges datagrams with many remote addresses, an Ethernet interface communicates with many MAC addresses on the same segment. Each endpoint may become a separate FMP link, but the transport layer itself maintains no per-endpoint state.

Services Provided to FMP

The transport layer provides four services to the FIPS Mesh Protocol above:

Datagram Delivery

Send and receive datagrams to/from transport addresses. The transport handles all medium-specific details: socket management, framing for stream transports, radio configuration. FMP sees only "send bytes to address" and "bytes arrived from address."

Inbound datagrams are pushed to FMP through a channel. The transport spawns a receive task that pushes arriving datagrams (along with the source transport address and transport identifier) onto a bounded channel. FMP reads from this channel and dispatches based on the source address and packet content.

MTU Reporting

Report the maximum datagram size for a given link. FMP needs this to determine how much payload can fit in a single packet after link-layer encryption overhead.

MTU is fundamentally a per-link property. A transport with a fixed MTU (Ethernet effective 1499, UDP default 1280) returns the same value for every link — this is the degenerate case. Transports that negotiate MTU per-connection (e.g., BLE ATT_MTU) report the negotiated value for each link individually.

The transport trait exposes two MTU methods:

  • fn mtu(&self) -> u16 — Transport-wide default MTU
  • fn link_mtu(&self, addr: &TransportAddr) -> u16 — Per-link MTU for a specific remote address. The default implementation falls back to mtu(), so transports with uniform MTU (like UDP) need not override it.

FMP uses link_mtu() when computing path MTU for SessionDatagram forwarding and LookupResponse transit annotation.

Connection Lifecycle

For connection-oriented transports, manage the underlying connection: TCP handshake, Tor circuit establishment, BLE pairing. FMP cannot begin the Noise IK link handshake until the transport-layer connection is established.

Connection-oriented transports expose a non-blocking connect interface. connect(addr) initiates the connection in a background task and returns immediately. connection_state(addr) reports the current status:

ConnectionState {
    None        No connection attempt in progress
    Connecting  Background task running
    Connected   Ready for send()
    Failed(msg) Error message from failed attempt
}

Connectionless transports (UDP, raw Ethernet) return Connected immediately — no async work needed.

At the node level, PendingConnect entries track links waiting for transport connection. poll_pending_connects() runs each tick, checks connection_state(), and calls start_handshake() on success or schedule_retry() on failure. This decouples transport-layer connection (which may take seconds for Tor circuits) from the FMP event loop.

Discovery (Optional)

Notify FMP when FIPS-capable endpoints are discovered on the local medium. This is an optional capability — transports that don't support it simply don't provide discovery events.

See Discovery below for details.

Transport Properties

Transports vary widely in their characteristics. FIPS operates over all of them because the transport interface abstracts these differences behind a uniform datagram service.

Transport Categories

Overlay transports tunnel FIPS over an existing network layer, typically for internet connectivity:

Transport Addressing MTU Reliability Notes
UDP/IP host:port 12801472 Unreliable Primary internet transport
TCP/IP host:port Stream Reliable Requires length-prefix framing
Tor .onion Stream Reliable High latency, strong anonymity

Shared medium transports operate over broadcast- or multicast-capable media:

Transport Addressing MTU Reliability Notes
Ethernet MAC 1500 Unreliable Raw AF_PACKET frames
WiFi MAC 1500 Unreliable Infrastructure mode = Ethernet
BLE BD_ADDR 23517 Reliable Negotiated ATT_MTU
Radio Device addr 51222 Unreliable Low bandwidth, long range

Point-to-point transports connect exactly two endpoints:

Transport Addressing MTU Reliability Notes
Serial None (P2P) 2561500 Reliable SLIP/COBS framing
Dialup None (P2P) 1500 Reliable PPP framing

Properties That Matter to FMP

MTU: Determines how much data FMP can pack into a single datagram after accounting for link encryption overhead. Heterogeneous MTUs across the mesh are normal — the IPv6 minimum (1280 bytes) is the safe baseline for FIPS packet sizing.

Reliability: Whether the transport guarantees delivery. FIPS prefers unreliable transports because running TCP application traffic over a reliable transport creates TCP-over-TCP, where retransmission and congestion control at both layers interact adversely. FIPS tolerates packet loss, reordering, and duplication at the routing layer.

Connection model: Connectionless transports (UDP, raw Ethernet) allow immediate datagram exchange. Connection-oriented transports (TCP, Tor, BLE) require connection setup before FMP can begin the Noise IK link handshake, adding startup latency.

Stream vs. datagram: Datagram transports have natural packet boundaries. Stream transports (TCP, Tor) require framing to delineate FIPS packets within the byte stream. The FMP common prefix includes a payload length field that provides this framing directly, replacing the need for a separate length-prefix layer.

Addressing opacity: Transport addresses are opaque byte vectors. FMP doesn't interpret them — it just passes them back to the transport when sending. This means adding a new transport type with a novel address format requires no changes to FMP or FSP.

Connection Model

Connectionless Transports

Datagrams can be sent to any reachable address without prior setup. Links are lightweight — a transport address is sufficient to begin communication.

Transport Notes
UDP/IP Stateless datagrams; NAT state is implicit
Ethernet Send to MAC address directly
Radio Raw packets to device address

Connection-Oriented Transports

Explicit connection setup is required before FIPS traffic can flow. The link must complete transport-layer connection before FMP authentication can proceed.

Transport Connection Setup
TCP/IP TCP three-way handshake
Tor Circuit establishment (typically 1060s, default timeout 120s)
BLE L2CAP CoC or GATT connection
Serial Physical connection (static)

Implications

Link lifecycle: Connectionless transports use a trivial link model. Connection-oriented transports need a real state machine: Connecting → Connected → Disconnected. Failure can occur during connection setup, adding error handling paths that connectionless transports don't have.

Startup latency: Connection-oriented transports add delay before a peer becomes usable. This ranges from milliseconds (TCP) to tens of seconds (Tor circuit). Peer timeout configuration must account for transport-specific setup times.

Framing: Stream transports must delimit FIPS packets within the byte stream. The FMP common prefix includes a payload length field that provides integrated framing. Datagram transports preserve packet boundaries naturally.

UDP/IP: The Primary Internet Transport

For internet-connected nodes, UDP/IP is the recommended transport:

  • No TCP-over-TCP: UDP's unreliable delivery avoids the adverse interaction between application-layer TCP retransmission and transport-layer TCP retransmission
  • NAT traversal: UDP hole punching enables peer connections through NAT without relay infrastructure
  • Low overhead: 8-byte UDP header, no connection state
  • Matches FIPS model: FIPS is datagram-oriented; UDP preserves this naturally without framing

Raw IP with a custom protocol number would be simpler but is blocked by most NAT devices and firewalls, limiting deployment to networks without NAT.

Socket Buffer Sizing

The default Linux UDP receive buffer (net.core.rmem_default, typically 212 KB) is insufficient for high-throughput forwarding. At ~85 MB/s, a 212 KB buffer fills in ~2.5 ms; any stall in the async receive loop (decryption, routing, forwarding overhead) causes the kernel to silently drop incoming datagrams.

FIPS uses socket2::Socket wrapped in tokio::io::unix::AsyncFd for the UDP receive path. This replaces tokio::UdpSocket and enables direct libc::recvmsg() calls with ancillary data parsing — specifically the SO_RXQ_OVFL socket option, which delivers a cumulative kernel receive buffer drop counter on every received packet. The drop counter feeds into the ECN congestion detection system (see fips-mmp.md).

Socket buffers (recv_buf_size, send_buf_size) are configured at bind time via socket2. Linux internally doubles the requested value (to account for kernel bookkeeping overhead) and silently clamps to net.core.rmem_max / net.core.wmem_max if the request exceeds the host kernel limits. The full UDP transport configuration is in ../reference/configuration.md. The host-side sysctl requirements and how to set them persistently live in ../how-to/tune-udp-buffers.md.

Ethernet: The Local Network Transport

For nodes on the same LAN segment, raw Ethernet provides a direct transport without IP/UDP overhead — 28 bytes more FIPS payload per frame compared to UDP (1500 vs 1472 MTU).

  • No IP dependency: Operates below the IP layer. Nodes on the same Ethernet segment can communicate without IP addresses or routing infrastructure
  • Broadcast discovery: Nodes discover each other via periodic beacon broadcasts on the shared medium, with no static peer configuration required
  • Higher MTU: Standard Ethernet frames carry 1500 bytes of payload, yielding an effective FIPS MTU of 1499 after the frame type prefix
  • Matches FIPS model: Like UDP, Ethernet is connectionless and unreliable — datagrams flow immediately to any MAC address on the segment

Implementation

The Ethernet transport uses Linux AF_PACKET sockets in SOCK_DGRAM mode with EtherType 0x2121. SOCK_DGRAM mode lets the kernel handle Ethernet header construction and parsing — the transport deals only with payloads and MAC addresses.

Data frames use a 3-byte header: a 1-byte frame type (0x00) followed by a 2-byte little-endian payload length. The length field allows the receiver to trim Ethernet minimum-frame padding that would otherwise corrupt AEAD verification. Beacon frames (0x01) use only the 1-byte type prefix (fixed 34-byte payload). Beacons and data share the same EtherType and socket.

Property Value
EtherType 0x2121
Socket type AF_PACKET SOCK_DGRAM
Data frame header [type:1][length:2 LE][payload]
Beacon frame header [type:1][payload] (fixed 34 bytes)
Effective MTU Interface MTU - 3 (typically 1497)
Addressing 6-byte MAC address
Platform Linux only (CAP_NET_RAW required)

Beacon Discovery

Ethernet nodes discover peers via broadcast beacons sent to ff:ff:ff:ff:ff:ff. Each beacon is a 34-byte frame containing the sender's x-only public key. Receiving nodes extract the MAC source address from the frame and the public key from the payload, then report the discovered peer to FMP.

Four configuration flags control discovery behavior — discovery (listen for beacons), announce (broadcast beacons), auto_connect (initiate handshakes to discovered peers), and accept_connections (accept inbound handshakes). The flag table and per-flag defaults live in ../reference/configuration.md under transports.ethernet.*.

A typical discoverable node sets announce, auto_connect, and accept_connections all true. A passive listener uses just discovery: true to observe the network without announcing itself.

WiFi Compatibility

WiFi interfaces in infrastructure (managed) mode work transparently for unicast — the mac80211 subsystem handles frame translation between 802.11 and 802.3. Broadcast beacon discovery is unreliable in managed mode because access points commonly isolate clients from each other's broadcast traffic.

Startup logging:

Ethernet transport started name=eth0 interface=eth0 mac=aa:bb:cc:dd:ee:ff mtu=1499 if_mtu=1500

TCP/IP: Transport for UDP-Filtered Networks

For peers whose networks filter outbound UDP, the TCP transport provides an alternative datagram path between public endpoints. TCP is not a NAT-traversal mechanism — there is no tcp:nat analogue to the UDP hole-punch flow.

FIPS protocols (FMP, FSP, MMP) are all unreliable datagrams. Running them over TCP introduces head-of-line blocking, which adds latency jitter. MMP correctly measures this jitter, and cost-based parent selection naturally penalizes TCP links (higher SRTT leads to higher link cost). ETX will be 1.0 over TCP since TCP handles retransmission.

Architecture

Unlike UDP (one socket serves all peers), TCP requires one TcpStream per peer. The transport maintains two pools: a ConnectingPool for background connection attempts in progress, and an established connection pool (HashMap<TransportAddr, TcpConnection>) for active connections, plus an optional TcpListener for inbound connections.

Property Value
Addressing host:port — IP address or DNS hostname
Default MTU 1400 bytes
Per-link MTU Derived from TCP_MAXSEG socket option
Framing FMP header-based (zero overhead)
Connection model Non-blocking connect, connect-on-send fallback, optional listener
Platform Cross-platform (no #[cfg] gates)

FMP Header-Based Framing

TCP is a byte stream; FIPS packets need delineation. Rather than adding a separate length-prefix layer, the TCP transport uses the existing 4-byte FMP common prefix [ver+phase:1][flags:1][payload_len:2 LE] to determine packet boundaries:

  • Phase 0x0 (established): remaining = 12 + payload_len + 16 (header + AEAD tag)
  • Phase 0x1 (msg1): remaining = payload_len (fixed at 110, total 114 bytes)
  • Phase 0x2 (msg2): remaining = payload_len (fixed at 65, total 69 bytes)
  • Unknown phase: close connection (protocol error)

This provides zero framing overhead and built-in phase validation. The stream reader is implemented in a separate module (stream.rs) for reuse by the Tor transport.

Connection Establishment

TCP connections use a non-blocking connect model. When FMP needs to reach a configured peer address, the node calls connect(addr) on the transport, which spawns a background tokio task to perform the TCP handshake and socket configuration (TCP_NODELAY, keepalive, buffer sizes, TCP_MAXSEG query). The call returns immediately without blocking the event loop.

The node tracks each pending connection in a PendingConnect entry. On every tick, poll_pending_connects() calls connection_state(addr) to check progress. When the transport reports Connected, the completed connection is promoted to the established pool (stream split into read/write halves, per-connection receive task spawned), and the node initiates the Noise IK link handshake. If the transport reports Failed, the node schedules a retry with exponential backoff.

As a fallback, send(addr, data) still performs synchronous connect-on-send if no connection exists — this handles the case where a send arrives before the node-level connect path runs. The non-blocking path is the primary mechanism for configured peers.

Session Independence

TCP connection loss does not tear down the FIPS peer. Noise keys, MMP state, and FSP sessions are bound to the peer's npub, not the TCP connection. The transport reconnects transparently via the non-blocking connect path or connect-on-send fallback. MMP liveness timeout is the sole authority for peer death.

Connection Deduplication

Simultaneous outbound connections from both sides are resolved by the existing cross-connection tie-breaker in promote_connection. The losing TCP connection is closed via Transport::close_connection(addr), which removes it from the pool and aborts its receive task.

Configuration

The TCP transport configuration block (transports.tcp.* — bind address, MTU, connect timeout, TCP_NODELAY, keepalive, socket buffer sizes, max inbound connections) is documented in ../reference/configuration.md. If bind_addr is configured, the transport accepts inbound connections; without it, the transport operates in outbound-only mode (no listener socket is created).

Tor: The Anonymity Transport

The Tor transport routes FIPS traffic through the Tor network, hiding a node's IP address from its peers. A node behind Tor connects outbound through a local Tor SOCKS5 proxy; the remote peer sees the Tor exit node's IP, not the initiator's. After the Noise IK handshake, the remote peer knows the initiator's FIPS identity (npub) but not its network location.

Like TCP, Tor is connection-oriented and reliable. The same TCP-over-TCP considerations apply — MMP correctly measures the elevated latency and cost-based parent selection naturally deprioritizes Tor links.

Architecture

The Tor transport is a separate TorTransport implementation, not a TCP variant, because it manages SOCKS5 proxy negotiation, has different address semantics (.onion vs IP:port), and has significantly different latency characteristics. It reuses the FMP header-based stream reader (tcp/stream.rs) for packet framing on the underlying TCP connection.

The transport maintains two pools (same pattern as TCP): a ConnectingPool for background SOCKS5 connection attempts, and an established pool of TorConnection entries. Each TorConnection holds a write half, a per-connection receive task, the negotiated MTU, and a connection timestamp.

Property Value
Addressing .onion:port or IP:port
Default MTU 1400 bytes
Framing FMP header-based (shared with TCP)
Connection model Non-blocking connect, outbound SOCKS5 + inbound via onion service
Platform Cross-platform (requires external Tor daemon)

Address Types

The Tor transport accepts three address formats, parsed into a TorAddr enum:

  • Onion: .onion:port — connects to a Tor hidden service. Both sides anonymous. (e.g., abcdef...xyz.onion:8443)
  • Clearnet IP: IP:port — connects through a Tor exit node to a remote TCP listener. Hides the initiator's IP; the remote peer sees the exit node's IP.
  • Clearnet Hostname: hostname:port — hostname is passed through SOCKS5 for Tor-side DNS resolution, avoiding local DNS leaks. Compatible with SafeSocks 1. (e.g., fips.example.com:8443)

All address types are routed through the same SOCKS5 proxy.

Connection Establishment

Connection setup follows the same non-blocking pattern as TCP. When FMP needs to reach a peer, the node calls connect(addr) on the transport. The transport spawns a background tokio task that:

  1. Opens a SOCKS5 connection through the local Tor proxy
  2. Configures the socket: TCP_NODELAY, keepalive (30s)
  3. Returns the connected stream

The call returns immediately. connection_state(addr) reports progress. Tor circuit establishment typically takes 1060 seconds (vs milliseconds for TCP), making non-blocking connect essential — a blocking connect would stall the entire FMP event loop.

The connect timeout defaults to 120 seconds (vs 5 seconds for TCP), accounting for Tor circuit setup time. As a fallback, send(addr, data) performs synchronous connect-on-send if no connection exists.

Inbound via Onion Service (Directory Mode)

In directory mode (recommended for production), Tor manages the onion service via HiddenServiceDir in torrc. FIPS reads the .onion address from the hostname file at startup and binds a local TCP listener that the Tor daemon forwards inbound connections to.

This mode enables Tor's Sandbox 1 (seccomp-bpf) — the strongest single hardening option — because no control port interaction is required for onion service management. Tor handles key generation and persistence directly through the HiddenServiceDir.

The inbound accept loop mirrors the TCP transport's pattern: accept connection, configure socket (TCP_NODELAY, keepalive), spawn a per-connection receive loop using the shared FMP stream reader. Inbound connections arrive from 127.0.0.1 (Tor daemon's local forwarding); peer identity is resolved during the Noise IK handshake, not from the transport address.

Configuration requires coordinating torrc and fips.yaml. The operator setup — torrc directives, fips.yaml tor section, HiddenServiceDir permissions, and Sandbox 1 notes — is in ../how-to/deploy-tor-onion.md. In brief: the HiddenServicePort external port is what peers connect to, and tor.directory_service.bind_addr must match the HiddenServicePort target address.

Session Independence

Same as TCP: Tor connection loss does not tear down the FIPS peer. Noise keys, MMP state, and FSP sessions survive reconnection.

Bridge Node Pattern

A node running both Tor and UDP transports acts as a bridge between anonymous and clearnet portions of the mesh:

[Anonymous node] --tor--> [Bridge node] --udp--> [Clearnet node]

No special code is needed — FIPS multi-transport routing handles it. Anonymous nodes connect to the bridge via Tor; the bridge forwards traffic to clearnet peers over UDP. Clearnet peers never see the anonymous node's IP.

Latency Characteristics

Tor adds 200ms2s RTT per circuit. MMP measures this elevated latency, and cost-based parent selection penalizes Tor links (high SRTT → high link cost). ETX is 1.0 since TCP handles retransmission.

Tor throughput is typically 15 Mbps — adequate for control plane and moderate data transfer, not for bulk transfer.

Monitoring

In control_port mode and optionally in directory mode (when control_addr is configured), the transport spawns a background monitoring task that polls the Tor daemon every 10 seconds via the control port. The cached monitoring data is exposed through the show_transports control socket query and displayed in fipstop.

Monitoring data includes:

  • Bootstrap progress (0100%) with INFO logging at milestones (25/50/75/100%) and WARN if stalled >60s
  • Circuit status (whether Tor has a working circuit)
  • Network liveness (up/down) with WARN on transitions
  • Dormant mode detection with WARN on entry
  • Tor daemon version and traffic counters (bytes read/written)

The control port connection uses cookie authentication by default (reading from /var/run/tor/control.authcookie). Unix socket connections (/run/tor/control) are preferred over TCP for security.

Configuration

The Tor transport block (transports.tor.*) is documented in ../reference/configuration.md. Three modes are available:

  • socks5 (default): Outbound-only through a SOCKS5 proxy. No control port, no inbound connections.
  • control_port: Outbound via SOCKS5 plus control port connection for Tor daemon monitoring. No inbound connections.
  • directory (recommended for inbound): Outbound via SOCKS5 plus inbound via Tor-managed HiddenServiceDir onion service. Optionally connects to the control port for monitoring when control_addr is set. Enables Tor's Sandbox 1 for maximum security.

The Tor transport requires an external Tor daemon. Named instances are supported for multiple proxy endpoints.

Implementation Roadmap

  • Outbound SOCKS5 connections to .onion, clearnet IP, and clearnet hostname addresses (implemented)
  • Inbound connections via Tor onion service using HiddenServiceDir directory mode (implemented)
  • Operator visibility: cached monitoring snapshot, control socket exposure, fipstop display, bootstrap/liveness logging (implemented)
  • Embedded arti (Rust Tor implementation) for self-contained operation without an external Tor daemon (future)

Statistics

The Tor transport exposes per-instance counters covering successful send/receive, send/receive errors, connection establishment, SOCKS5-level errors, MTU rejections, accepted/rejected inbound connections, and Tor control-port errors. The full counter table lives in ../reference/transports.md.

Discovery

Discovery determines that a FIPS-capable endpoint is reachable at a given transport address. It is distinct from raw transport-level endpoint detection — a new TCP connection or UDP packet from an unknown source is not discovery; a FIPS-specific announcement or response is.

Discovery is an optional transport capability. Transports that don't support it (configured UDP endpoints, TCP, Tor) simply don't provide discovery events. FMP handles both cases uniformly: with discovery, it waits for events then initiates link setup; without discovery, it initiates link setup directly to configured addresses.

Local/Medium Discovery

For transports where endpoints share a physical or link-layer medium — LAN broadcast, radio, BLE — discovery uses beacon and query mechanisms:

  • Beacon: A node periodically broadcasts its FIPS presence on the shared medium. Content is a FIPS-defined discovery frame carrying enough information to initiate a link. Non-FIPS endpoints ignore the frame.
  • Query: A node broadcasts a one-shot solicitation. FIPS-capable nodes respond. Responses arrive on the same channel as beacon events.

Both produce the same result: "FIPS endpoint available at transport address X." FMP does not need to distinguish beacons from query responses.

Transport Discovery Notes
UDP (LAN) Broadcast/multicast On local network segment
Ethernet Broadcast Custom EtherType, ff:ff:ff:ff:ff:ff
Radio Beacon Shared RF channel, natural fit
BLE Advertising GATT service UUID

Nostr Relay Discovery

For internet-reachable transports, a node publishes a signed Nostr event containing its FIPS discovery information — public key and reachable transport endpoints (UDP host:port, TCP host:port, .onion address). Other FIPS nodes subscribing on the same relays learn about available peers.

Nostr relay discovery is not a transport — it is a discovery service that feeds addresses to other transports. A node discovers via Nostr that a peer is reachable at UDP 1.2.3.4:9735, then establishes the link over the UDP transport.

For NAT'd UDP endpoints, a node may advertise addr: "nat" instead of a concrete address, signaling that peers should initiate STUN-assisted UDP hole punching. Offer/answer exchange uses Nostr gift-wrap (NIP-59) events on the configured DM relays; the resulting punched socket is adopted into the standard UDP transport via the bootstrap handoff path.

Key properties:

  • Identity is built in — Nostr events are signed, so discovery information is authenticated
  • Relay selection acts as scoping — which relays a node publishes to and subscribes on determines its discovery neighborhood
  • Can only advertise IP-reachable endpoints (not radio, BLE, serial)
  • Higher latency than local discovery (relay propagation delays)

Current State

Implemented: UDP, TCP, Tor, and Ethernet peers can be configured statically via YAML. Ethernet peers can also be discovered via beacon broadcast — the discover() trait method returns newly seen endpoints, and per-transport auto_connect() / accept_connections() policies control whether discovered peers are connected automatically or require explicit configuration. TCP and Tor have no built-in discovery mechanism. Nostr relay discovery and STUN-assisted UDP hole punching are implemented and toggled via configuration; see ../reference/configuration.md for the node.discovery.nostr.* configuration tree.

Transport Interface

The transport interface defines what every transport driver must provide.

Trait Surface

transport_id()        → TransportId         Unique identifier for this transport instance
transport_type()      → &TransportType      Static metadata (name, connection-oriented, reliable)
name()                → Option<&str>        Instance name (for multi-instance transports)
state()               → TransportState      Current lifecycle state
mtu()                 → u16                 Transport-wide default MTU
link_mtu(addr)        → u16                 Per-link MTU (defaults to mtu())
start()               → lifecycle           Bring transport up (bind socket, open device)
stop()                → lifecycle           Bring transport down
send(addr, data)      → delivery            Send datagram to transport address
connect(addr)         → ()                  Initiate non-blocking connection (connection-oriented only)
connection_state(addr)→ ConnectionState     Poll connection status (None/Connecting/Connected/Failed)
close_connection(addr)→ ()                  Close a specific connection (no-op for connectionless)
congestion()          → TransportCongestion  Local congestion indicators (optional)
discover()            → Vec<DiscoveredPeer> Report discovered FIPS endpoints (optional)
auto_connect()        → bool                Auto-connect discovered peers (default: false)
accept_connections()  → bool                Accept inbound handshakes (default: true)

Receive Path

Rather than a synchronous receive method, transports use a channel-push model. Each transport takes a sender handle at construction and spawns an internal receive loop that pushes inbound datagrams onto the channel. The node's main event loop reads from the corresponding receiver, which aggregates datagrams from all active transports into a single stream.

Each inbound datagram carries:

  • transport_id — which transport it arrived on
  • remote_addr — the transport address of the sender
  • data — the raw datagram bytes
  • timestamp — arrival time

Transport Metadata

Transport types carry static metadata that FMP can query:

TransportType {
    name              "udp", "ethernet", "tor", etc.
    connection_oriented   bool
    reliable              bool
}

Predefined types exist for UDP, TCP, Ethernet, WiFi, Tor, and Serial.

Congestion Reporting

Transports optionally report local congestion indicators via a TransportCongestion struct, providing a transport-agnostic interface for the node layer's ECN congestion detection:

TransportCongestion {
    recv_drops: Option<u64>    Cumulative kernel-dropped packets (monotonic)
}

The node samples each transport's congestion state on a 1-second tick via sample_transport_congestion(). TransportDropState tracks per-transport drop deltas: when new drops appear (rising edge), the dropping flag is set, and detect_congestion() in the forwarding path triggers CE marking on all forwarded datagrams.

Transport Congestion Source Mechanism
UDP SO_RXQ_OVFL kernel drop counter recvmsg() ancillary data on every packet
TCP Not implemented Returns None (TCP handles congestion internally)
Tor Not implemented Returns None (TCP handles congestion internally)
Ethernet Not implemented Returns None

Transport Addresses

Transport addresses (TransportAddr) are opaque byte vectors. The transport layer interprets them — e.g. UDP and TCP resolve host:port strings (IP fast path, DNS fallback with a 60s cache on UDP). All layers above treat them as opaque handles passed back to the transport for sending.

Transport State Machine

Configured → Starting → Up → Down
                         ↓
                       Failed

Transports begin in Configured state with all parameters set. start() transitions through Starting to Up (operational). stop() moves to Down. Transport failures move to Failed.

Implementation Status

Transport Status Notes
UDP/IP Implemented Primary transport, AsyncFd/recvmsg, SO_RXQ_OVFL kernel drop detection
TCP/IP Implemented FMP header-based framing, non-blocking connect, per-connection MSS MTU
Ethernet Implemented AF_PACKET SOCK_DGRAM, EtherType 0x2121, beacon discovery, Linux only
WiFi Implemented (via Ethernet transport, infrastructure mode) mac80211 translates 802.11↔802.3; broadcast beacons unreliable through APs
Tor Implemented Outbound SOCKS5, inbound via onion service, .onion and clearnet addressing
BLE Implemented (Linux/glibc only; experimental) L2CAP CoC, ATT_MTU negotiation, per-link MTU; musl/macOS/Windows skip
Radio Future direction Constrained MTU (51222 bytes)
Serial Future direction SLIP/COBS framing, point-to-point

Design Considerations

TCP-over-TCP Avoidance

Running TCP application traffic over a reliable transport (TCP, Tor) creates a layering violation where retransmission and congestion control operate at both levels. When the inner TCP detects loss (which may just be transport-layer retransmission delay), it retransmits, creating more traffic for the outer TCP, which may itself be retransmitting. This amplification loop degrades performance severely under any packet loss.

FIPS prefers unreliable transports for this reason. When a reliable transport must be used (e.g., Tor), applications should be aware of the performance implications.

Multi-Transport Operation

A node can run multiple transports simultaneously. Peers from all transports feed into a single spanning tree and routing table. If one transport fails, traffic automatically routes through alternatives. A node with both UDP and Ethernet transports bridges between internet-connected and local-only networks transparently.

Multiple links to the same peer over different transports are possible. FMP manages these independently — each link has its own Noise session, its own MTU, and its own liveness tracking.

Transport Quality and Path Selection

Transport characteristics (latency, bandwidth, reliability) affect path quality. The spanning tree parent selection factors in link quality through cost-based effective depth (effective_depth = depth + link_cost), where link_cost is derived from locally measured MMP metrics (ETX and SRTT). This allows the tree to prefer lower-latency, lower-loss links when the quality difference is significant. Link cost is not yet used in find_next_hop() candidate ranking for data forwarding.

References