From a16370e78d5dd3e4355b24609689260e15980ce4 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 22 Mar 2026 20:26:09 +0000 Subject: [PATCH] Update changelog, version, and design docs for v0.2.0 Bump version to 0.2.0 and finalize changelog with discovery rework, Tor transport, connect/disconnect commands, reproducible builds, and 12 bug fixes. Update design documentation for discovery protocol rework: - fips-wire-formats.md: remove visited bloom filter from LookupRequest, update size calculations - fips-mesh-operation.md: replace flooding description with bloom-guided tree routing, add retry/backoff/rate-limiting subsections - fips-configuration.md: add 5 new discovery config parameters, update control socket description for connect/disconnect commands --- CHANGELOG.md | 46 +++++++++--- Cargo.lock | 2 +- Cargo.toml | 2 +- docs/design/fips-configuration.md | 22 ++++-- docs/design/fips-mesh-operation.md | 115 +++++++++++++++++++++++------ docs/design/fips-wire-formats.md | 19 +++-- 6 files changed, 156 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86874df..d57bd86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,23 +5,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.2.0] - 2026-03-22 ### Added +#### Operator Tooling + +- `fipsctl connect` and `disconnect` commands for runtime peer + management via control socket, with hostname resolution from + `/etc/fips/hosts` + #### IPv6 Adapter -- Pre-seed identity cache from configured peer npubs at startup, so TUN - packets can be dispatched immediately without waiting for handshake - completion ([@v0l](https://github.com/v0l)) +- Pre-seed identity cache from configured peer npubs at startup, so TUN packets can be dispatched immediately without waiting for handshake completion ([@v0l](https://github.com/v0l)) #### Mesh Peer Transports -- Tor transport with SOCKS5 outbound, directory-mode onion service - inbound, and optional control port monitoring +- New Tor transport with SOCKS5 and directory-mode onion service for anonymous inbound and outbound peering - DNS hostname support in peer addresses for UDP and TCP transports -- Non-blocking transport connect for connection-oriented transports - (TCP, Tor) +- Non-blocking transport connect for connection-oriented transports (TCP, Tor) + +#### Packaging and Deployment + +- Reproducible build infrastructure: Rust toolchain pinning via + `rust-toolchain.toml`, `SOURCE_DATE_EPOCH` in CI and packaging + scripts, deterministic archive timestamps +- Top-level packaging Makefile for unified build across formats +- Kubernetes sidecar deployment example with Nostr relay demo +- Nostr release publishing in OpenWrt package workflow +- SHA-256 hash output in CI build and OpenWrt workflows + +#### Testing and CI + +- Maelstrom chaos scenario with dynamic topology mutation and + ephemeral node identities via connect/disconnect commands +- Consolidated Docker test harness infrastructure + +### Changed + +- Discovery protocol: replace flooding with bloom-filter-guided tree + routing. Includes originator retry (T=0/T=5s/T=10s), exponential + backoff after timeouts and bloom misses, and transit-side per-target + rate limiting. Removed 257-byte visited bloom filter from LookupRequest wire format. *This is a breaking change; nodes running versions prior to this release will not be compatible.* ### Fixed @@ -29,10 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 causing resolvers to give up without trying AAAA. Now returns NOERROR with empty answers for non-AAAA queries on resolvable names. (#9, reported by [@alopatindev](https://github.com/alopatindev)) -- Stale end-to-end session left in session table after peer removal blocked - session re-establishment on reconnect — `remove_active_peer` now cleans - up `self.sessions` and `self.pending_tun_packets`. - (#5, [@v0l](https://github.com/v0l)) +- Stale end-to-end session left in session table after peer removal blocked session re-establishment on reconnect — `remove_active_peer` now cleans up `self.sessions` and `self.pending_tun_packets`. (#5, [@v0l](https://github.com/v0l)) - `schedule_reconnect` reset exponential backoff to zero on each link-dead cycle instead of preserving accumulated retry count. (#5, [@v0l](https://github.com/v0l)) diff --git a/Cargo.lock b/Cargo.lock index 4288312..2d5275c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -783,7 +783,7 @@ checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" [[package]] name = "fips" -version = "0.1.0" +version = "0.2.0" dependencies = [ "bech32", "chacha20poly1305", diff --git a/Cargo.toml b/Cargo.toml index c91079f..7be712d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fips" -version = "0.1.0" +version = "0.2.0" edition = "2024" description = "A distributed, decentralized network routing protocol for mesh nodes connecting over arbitrary transports" license = "MIT" diff --git a/docs/design/fips-configuration.md b/docs/design/fips-configuration.md index e3d4251..3d8f614 100644 --- a/docs/design/fips-configuration.md +++ b/docs/design/fips-configuration.md @@ -56,9 +56,11 @@ peers: # Static peer list | `node.control.enabled` | bool | `true` | Enable the Unix domain control socket | | `node.control.socket_path` | string | *(auto)* | Socket file path. Default: `$XDG_RUNTIME_DIR/fips/control.sock`, then `/run/fips/control.sock` (if root), then `/tmp/fips-control.sock` | -The control socket provides read-only access to node state via the -`fipsctl` command-line tool. See the project -[README](../../README.md#inspect) for the command list. +The control socket provides access to node state and runtime management +via the `fipsctl` command-line tool. In addition to read-only status +queries, `fipsctl connect` and `fipsctl disconnect` enable runtime peer +management. See the project [README](../../README.md#inspect) for the +command list. All tunable protocol parameters live under `node.*`, organized as sysctl-style dotted paths. The top-level sections (`tun`, `dns`, `transports`, `peers`) @@ -144,13 +146,18 @@ Controls caching of tree coordinates and identity mappings. ### Discovery Protocol (`node.discovery.*`) -Controls flood-based node discovery (LookupRequest/LookupResponse). +Controls bloom-guided node discovery (LookupRequest/LookupResponse). | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `node.discovery.ttl` | u8 | `64` | Hop limit for LookupRequest flood | +| `node.discovery.ttl` | u8 | `64` | Hop limit for LookupRequest forwarding | | `node.discovery.timeout_secs` | u64 | `10` | Lookup completion timeout | | `node.discovery.recent_expiry_secs` | u64 | `10` | Dedup cache expiry for recent request IDs | +| `node.discovery.retry_interval_secs` | u64 | `5` | Retry interval within the timeout window; after this interval without a response, resend the lookup | +| `node.discovery.max_attempts` | u8 | `2` | Max attempts per lookup (1 = no retry, 2 = one retry) | +| `node.discovery.backoff_base_secs` | u64 | `30` | Base for exponential backoff after lookup failure; doubles per consecutive failure | +| `node.discovery.backoff_max_secs` | u64 | `300` | Cap on exponential backoff (5 minutes) | +| `node.discovery.forward_min_interval_secs` | u64 | `2` | Transit-side rate limiting: minimum interval between forwarded lookups for the same target | ### Spanning Tree (`node.tree.*`) @@ -616,6 +623,11 @@ node: ttl: 64 timeout_secs: 10 recent_expiry_secs: 10 + retry_interval_secs: 5 + max_attempts: 2 + backoff_base_secs: 30 + backoff_max_secs: 300 + forward_min_interval_secs: 2 tree: announce_min_interval_ms: 500 parent_hysteresis: 0.2 # cost improvement fraction for parent switch diff --git a/docs/design/fips-mesh-operation.md b/docs/design/fips-mesh-operation.md index 0be9b89..1a14725 100644 --- a/docs/design/fips-mesh-operation.md +++ b/docs/design/fips-mesh-operation.md @@ -330,7 +330,9 @@ fresh SessionSetup re-warms transit caches (still within their 300s TTL). ## Discovery Protocol Discovery resolves a destination's tree coordinates so that multi-hop routing -can proceed. +can proceed. Requests are forwarded using **bloom-guided tree routing** — +only to tree peers (parent + children) whose bloom filter contains the +target — producing single-path forwarding through the spanning tree. ### When Discovery Is Needed @@ -347,17 +349,70 @@ The source creates a LookupRequest containing: - **origin**: The requester's node_addr - **origin_coords**: The requester's current tree coordinates (so the response can route back) -- **TTL**: Bounds the flood radius +- **TTL**: Bounds the forwarding radius -The request floods through the mesh: each node decrements TTL, adds itself -to a visited filter (preventing loops on a single path), and forwards to all -peers not in the visited filter. Bloom filters may help direct the flood -toward likely candidates. +### Bloom-Guided Tree Routing -**Deduplication**: Nodes maintain a short-lived request_id dedup cache -(default 10s window) to drop convergent duplicates (the same request -arriving via different paths). This is a protocol requirement, not an -optimization. +Rather than flooding to all peers, the request is forwarded only to **tree +peers** (parent + children) whose bloom filter contains the target. Because +bloom filters propagate along tree edges with split-horizon exclusion, +typically only one tree peer matches — producing a single directed path +through the spanning tree toward the target's subtree. This reduces +discovery traffic by roughly 90% compared to flooding. + +If no tree peer's bloom filter matches the target, the request falls back +to **non-tree peers** whose bloom filter contains the target. This recovers +from dead ends caused by stale bloom filters, tree restructuring, or transit +node failures. If no peer at all has a bloom match, the request is dropped +at that node. + +**Loop prevention**: The spanning tree is inherently loop-free, so tree-only +forwarding cannot loop. The `request_id` dedup cache (default 10s window) +provides defense-in-depth, catching edge cases during tree restructuring +where a request might arrive via both tree and fallback paths. + +### Retry Logic + +Single-path forwarding is more fragile than flooding — if any transit node +on the path has a stale bloom filter or loses a link, the request fails. +To compensate, the originator retries: + +- **T=0**: Initial lookup sent +- **T=5s**: Retry if no response (configurable via `retry_interval_secs`) +- **T=10s**: Timeout, fail (configurable via `timeout_secs`) + +The default `max_attempts` is 2 (initial + one retry). Each retry generates +a fresh `request_id` and re-evaluates bloom filter matches, so it can take +a different path if the tree has restructured. + +### Originator Backoff + +After a lookup times out or no peer's bloom filter contains the target, the +originator enters **exponential backoff** before re-attempting discovery for +the same target: + +- **Base delay**: 30s (configurable via `backoff_base_secs`) +- **Multiplier**: 2x per consecutive failure +- **Cap**: 300s (configurable via `backoff_max_secs`) + +Backoff is **reset on topology changes** that might make previously +unreachable targets reachable: parent switch, new peer connection, first +RTT measurement from MMP, or peer reconnection. + +### Bloom Filter Pre-Check + +Before initiating a lookup, the originator checks whether *any* peer's +bloom filter contains the target. If no peer advertises reachability, the +lookup is skipped entirely and recorded as a failure for backoff purposes. +This avoids wasting network resources when the target is not in the mesh. + +### Transit-Side Rate Limiting + +Transit nodes enforce a per-target minimum interval (default 2s, configurable +via `forward_min_interval_secs`) for forwarded lookups. This is +defense-in-depth against misbehaving nodes that generate fresh `request_id`s +at high rate to bypass dedup. The rate limiter collapses rapid-fire lookups +for the same target regardless of `request_id`. ### LookupResponse @@ -373,14 +428,21 @@ direct peer), a LookupResponse is created containing: authenticates that the response is genuine and the target holds the claimed tree position -The response routes back to the requester using reverse-path routing as the -primary mechanism: each transit node looks up the request_id in its +The response routes back to the requester using **reverse-path routing** as +the primary mechanism: each transit node looks up the `request_id` in its `recent_requests` table to find the peer that forwarded the original request, and sends the response back through that peer. This ensures the response follows the same path as the request. Greedy tree routing toward the -origin_coords is used only as a fallback if the reverse-path entry has +`origin_coords` is used only as a fallback if the reverse-path entry has expired. +**Response-forwarded flag**: Each `recent_requests` entry tracks whether a +response has already been forwarded for that `request_id`. If a second +response arrives (e.g., from convergent request paths that reached the +target via different routes), the transit node drops it. This prevents +response routing loops where multiple responses for the same request +circulate through the network. + **Proof verification**: The source verifies the Schnorr proof upon receipt, confirming that the target actually signed the response. The proof covers `(request_id || target || target_coords)` — coordinates are included because @@ -390,12 +452,14 @@ annotation modified at each hop. ### Discovery Outcome -On receiving a LookupResponse, the source caches the target's coordinates. -Subsequent routing to that destination can proceed via the normal -`find_next_hop()` priority chain. +On receiving a verified LookupResponse, the source caches the target's +coordinates and clears any backoff state for that target. Subsequent routing +to that destination can proceed via the normal `find_next_hop()` priority +chain. -If discovery times out (no response), queued packets receive ICMPv6 -Destination Unreachable. +If discovery times out (no response after all retry attempts), queued +packets receive ICMPv6 Destination Unreachable and the target enters +backoff. ## SessionSetup Self-Bootstrapping @@ -467,7 +531,7 @@ coordinates for the destination. It cannot make a forwarding decision. 2. Reset CP warmup counter — subsequent data packets piggyback coordinates when possible, or trigger additional CoordsWarmup messages when piggybacking would exceed the transport MTU -3. Initiate discovery (LookupRequest flood) for the destination +3. Initiate discovery (bloom-guided LookupRequest) for the destination 4. When discovery completes, warmup counter resets again (covers timing gap) The crypto session remains active throughout — only routing state is @@ -553,8 +617,9 @@ sequence: identity cache with NodeAddr + PublicKey 2. **Session initiation attempt**: Fails because no coordinates are cached for the destination -3. **Discovery**: LookupRequest floods through the mesh; LookupResponse - returns the destination's coordinates +3. **Discovery**: LookupRequest routes through the spanning tree via + bloom-guided forwarding; LookupResponse returns the destination's + coordinates 4. **Session establishment**: SessionSetup carries coordinates, warming transit caches along the path 5. **Warmup**: First N data packets include CP flag, reinforcing transit @@ -643,7 +708,7 @@ routing decisions but retains its own end-to-end encryption and identity. | ------- | ------------ | ---- | ---------- | | TreeAnnounce | Variable (depth-dependent) | Topology changes | No (peer-to-peer) | | FilterAnnounce | ~1 KB | Topology changes | No (peer-to-peer) | -| LookupRequest | ~300 bytes | First contact, recovery | Yes (flood) | +| LookupRequest | ~300 bytes | First contact, recovery | Yes (bloom-guided tree) | | LookupResponse | ~400 bytes | Response to discovery | Yes (greedy routed) | | SessionDatagram + SessionSetup | ~232–402 bytes | Session establishment | Yes (routed) | | SessionDatagram + SessionAck | ~170 bytes | Session confirmation | Yes (routed) | @@ -696,10 +761,14 @@ recovery). | Flap dampening (hysteresis + hold-down) | **Implemented** | | Link liveness (dead timeout) | **Implemented** | | Discovery request deduplication | **Implemented** | +| Discovery bloom-guided tree routing | **Implemented** | +| Discovery retry logic | **Implemented** | +| Discovery originator backoff | **Implemented** | +| Discovery transit-side rate limiting | **Implemented** | +| Discovery response-forwarded dedup | **Implemented** | | Leaf-only operation | Under development | | Link cost in parent selection (ETX) | **Implemented** | | Link cost in candidate ranking | **Implemented** | -| Discovery path accumulation | Future direction | ## References diff --git a/docs/design/fips-wire-formats.md b/docs/design/fips-wire-formats.md index 41ab5f2..45bd213 100644 --- a/docs/design/fips-wire-formats.md +++ b/docs/design/fips-wire-formats.md @@ -255,7 +255,11 @@ With link overhead: 1,072 bytes. ### LookupRequest (0x30) -Coordinate discovery request, flooded through the mesh. +Coordinate discovery request, routed through the spanning tree via +bloom-filter-guided forwarding. Each transit node forwards only to tree +peers (parent + children) whose bloom filter contains the target. +Request_id dedup in recent_requests handles edge cases from tree +restructuring. ![LookupRequest](diagrams/lookup-request.svg) @@ -269,20 +273,19 @@ Coordinate discovery request, flooded through the mesh. | 42 | min_mtu | 2 bytes LE | Minimum transport MTU the origin requires (0 = no requirement) | | 44 | origin_coords_cnt | 2 bytes LE | Number of coordinate entries | | 46 | origin_coords | 16 x n bytes | Requester's ancestry (NodeAddr only) | -| 46 + 16n | visited_hash_cnt | 1 byte | Hash count for visited filter | -| 47 + 16n | visited_bits | 256 bytes | Compact bloom of visited nodes | -**Size**: `303 + (n x 16)` bytes, where n = origin depth + 1 +**Size**: `46 + (n x 16)` bytes, where n = origin depth + 1 | Origin Depth | Payload | | ------------ | ------- | -| 3 | 351 bytes | -| 5 | 383 bytes | -| 10 | 463 bytes | +| 3 | 110 bytes | +| 5 | 142 bytes | +| 10 | 222 bytes | ### LookupResponse (0x31) -Coordinate discovery response, greedy-routed back to requester. +Coordinate discovery response, reverse-path routed back to the +requester via the transit nodes that forwarded the request. ![LookupResponse](diagrams/lookup-response.svg)