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
This commit is contained in:
Johnathan Corgan
2026-03-22 20:26:09 +00:00
parent 537eaf0db7
commit a16370e78d
6 changed files with 156 additions and 50 deletions

View File

@@ -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))

2
Cargo.lock generated
View File

@@ -783,7 +783,7 @@ checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
[[package]]
name = "fips"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bech32",
"chacha20poly1305",

View File

@@ -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"

View File

@@ -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

View File

@@ -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 | ~232402 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

View File

@@ -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)