mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
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:
46
CHANGELOG.md
46
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))
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -783,7 +783,7 @@ checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
|
||||
|
||||
[[package]]
|
||||
name = "fips"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"chacha20poly1305",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

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

|
||||
|
||||
|
||||
Reference in New Issue
Block a user