mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 01:27:32 +00:00
Merge branch 'master' into next
Forward-merge the v0.4.0 pre-release content (dependency refresh, CI deb-install + AUR-build legs, docs refresh, doc-comment fixes, and the Phase 4 source-content squash: CHANGELOG, release notes, README, fips.yaml, Cargo.toml metadata, reference docs) up the one-way flow. Conflict fixups (keep next's identity, fold in master's improvements): - Version: keep next's 0.5.0-dev (Cargo.toml/lock). - README status: keep next's v0.5.0-dev / wire-format-breaking framing and the Breaking-section pointer; fold in the Nym transport and the "global, public test mesh of thousands of nodes" description. - docs/reference/cli-fips.md: keep next's 0.5.0-dev version example. - CHANGELOG: keep next's XX-handshake admission entry (no early cap gate on XX) and drop master's IK early-cap-at-handle_msg1 entry, which describes IK-only behavior that does not apply on next; take master's OR-union mesh-size rewrite (next carries the OR-union code); restore the Tor connect_refused and MMP receiver-report entries that the Fixed-section consolidation would otherwise have dropped. - lifecycle.rs: keep the mDNS/LAN handshake doc-comment as Noise XX (next unifies on XX), not master's XX-to-IK correction. Quartet green on the merged tree: fmt, build, clippy -D warnings, and cargo test --lib (1434 passed).
This commit is contained in:
@@ -219,9 +219,11 @@ discovery protocol, and error-recovery integration view live in
|
||||
## Transport Abstraction
|
||||
|
||||
FIPS treats the communication medium as a pluggable component. UDP,
|
||||
TCP, raw Ethernet, Tor, and BLE all implement the same small datagram
|
||||
interface (send, receive, report MTU) and feed peers into a single FMP
|
||||
routing layer; radio and serial transports are in the planned set.
|
||||
TCP, raw Ethernet, Tor, BLE, and Nym all implement the same small
|
||||
datagram interface (send, receive, report MTU) and feed peers into a
|
||||
single FMP routing layer; radio and serial transports are in the
|
||||
planned set. Nym (an outbound-only mixnet transport) and Tor are
|
||||
privacy-oriented deployment modes rather than failover paths.
|
||||
Multi-transport nodes bridge between networks transparently. The
|
||||
transport-layer specification — including per-transport categories,
|
||||
the trait surface, the connection model, and implementation status —
|
||||
|
||||
@@ -180,7 +180,7 @@ network with no overlap (excluding the node itself at the split point).
|
||||
|
||||
All peers — including non-tree mesh shortcuts — still **receive**
|
||||
FilterAnnounce messages and **store** received filters locally. These
|
||||
stored filters are consulted during routing (step 3 of `find_next_hop()`)
|
||||
stored filters are consulted during routing (step 4 of `find_next_hop()`)
|
||||
for single-hop shortcut discovery. However, mesh peer filters contain
|
||||
only the mesh peer's own tree-propagated information, not transitive
|
||||
entries from the broader network.
|
||||
@@ -336,14 +336,24 @@ positions that folding produces.
|
||||
|
||||
## Mesh Size Estimation
|
||||
|
||||
Each filter's saturation can be inverted into an estimated entry count
|
||||
A filter's saturation can be inverted into an estimated entry count
|
||||
via the standard formula `n ≈ -(m/k) · ln(1 − X/m)`, where `m` is the
|
||||
filter size in bits, `k` is the hash count, and `X` is the population
|
||||
count. Combining the parent's inbound filter with the children's
|
||||
inbound filters gives an estimate of the whole network: parent + each
|
||||
child's subtree are disjoint by construction, and adding 1 for the
|
||||
node itself yields the total. The result is cached on the node and
|
||||
exposed through the control socket and `fipstop` dashboard.
|
||||
count. Rather than estimate per-filter and sum, the node first builds
|
||||
an **OR-union of every connected peer's inbound filter** — all routing
|
||||
peers, including cross-links, not just the tree parent and children —
|
||||
inserts its own address into the union, and inverts the cardinality
|
||||
**once on the resulting union**. Because filter propagation is
|
||||
split-horizon (each outgoing filter excludes the peer it routes back
|
||||
to), every routing peer advertises a near-complete "whole mesh minus
|
||||
my subtree" view, so the union covers the network. OR-ing is
|
||||
idempotent, so overlapping bits deduplicate instead of over-counting,
|
||||
and folding in all peers rather than only the tree neighborhood damps
|
||||
the count flap on a parent switch (the cross-links still carry the
|
||||
upward coverage) and removes any dependence on tree-declaration cache
|
||||
freshness. The result is cached on the node and exposed through the
|
||||
control socket and `fipstop` dashboard. (See `compute_mesh_size()` in
|
||||
`src/node/mod.rs`.)
|
||||
|
||||
The estimator refuses to produce a value when any contributing filter
|
||||
is above the antipoison FPR cap (`node.bloom.max_inbound_fpr`,
|
||||
@@ -379,7 +389,7 @@ as described above.
|
||||
| Fold/duplicate size conversion | **Implemented** |
|
||||
| FilterAnnounce gossip (all peers) | **Implemented** |
|
||||
| Filter cardinality logging | **Implemented** |
|
||||
| Mesh size estimation (parent + children + 1) | **Implemented** |
|
||||
| Mesh size estimation (OR-union of peer filters) | **Implemented** |
|
||||
| Inbound FPR cap (antipoison) | **Implemented** |
|
||||
| Size class negotiation | Future direction |
|
||||
| Folding support | Future direction |
|
||||
|
||||
@@ -218,7 +218,7 @@ involving the DNS proxy or the pool.
|
||||
|
||||
### Virtual IP Pool
|
||||
|
||||
The pool allocates IPv6 addresses from a configured CIDR (default
|
||||
The pool allocates IPv6 addresses from a required CIDR (commonly
|
||||
`fd01::/112`). Each address maps to one mesh destination, keyed by
|
||||
`NodeAddr` rather than by hostname — different `.fips` aliases for
|
||||
the same node share a virtual IP. Address 0 (the network-equivalent)
|
||||
|
||||
@@ -251,7 +251,7 @@ would later drop.
|
||||
The adapter integrates with the MTU subsystem rather than owning it.
|
||||
The "why we clamp and what `max_mss` means" lives here in the MTU
|
||||
design; the "how the clamp is implemented at the TUN" lives in the
|
||||
[IPv6 adapter](fips-ipv6-adapter.md#tcp-mss-clamping) doc.
|
||||
[IPv6 adapter](fips-ipv6-adapter.md#tun-side-tcp-mss-clamping) doc.
|
||||
|
||||
## ICMP Packet Too Big
|
||||
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
# FIPS Nostr-Mediated Discovery and NAT Traversal
|
||||
# FIPS Discovery: Nostr-Mediated and LAN/mDNS
|
||||
|
||||
FIPS nodes have two discovery mechanisms beyond the static `peers[]`
|
||||
list. The bulk of this document describes **Nostr-mediated discovery**,
|
||||
which works across the internet using public Nostr relays as a
|
||||
signaling channel and can punch through UDP NAT. A second, much
|
||||
simpler mechanism — **LAN/mDNS discovery** — finds peers on the same
|
||||
local link with no relay, STUN, or NAT traversal at all; it is
|
||||
described in its own section near the end. The two are independent: a
|
||||
node can enable either, both, or neither.
|
||||
|
||||
Nostr-mediated discovery lets FIPS nodes find each other, and if
|
||||
necessary, punch through UDP NAT, using public Nostr relays as the
|
||||
@@ -377,6 +386,182 @@ semaphore and replay-cache layers downstream.
|
||||
advert says "I am npub X at 1.2.3.4:5678" but whose FMP handshake
|
||||
presents a different static key is rejected at the mesh layer.
|
||||
|
||||
## LAN/mDNS discovery
|
||||
|
||||
LAN discovery is a separate, link-local discovery mechanism that finds
|
||||
peers on the same broadcast domain using mDNS / DNS-SD
|
||||
([RFC 6762](https://www.rfc-editor.org/rfc/rfc6762) /
|
||||
[RFC 6763](https://www.rfc-editor.org/rfc/rfc6763)). Unlike
|
||||
Nostr-mediated discovery, it contacts no relay, runs no STUN
|
||||
observation, and performs no NAT traversal: an endpoint learned from a
|
||||
LAN advert is by construction routable from the consumer's own link.
|
||||
The result is sub-second peer pairing on the same LAN.
|
||||
|
||||
It is unrelated to the "LAN candidate" terminology used in the
|
||||
NAT-traversal sections above (which refers to a host's own
|
||||
locally-bound address offered as a hole-punch candidate). LAN/mDNS
|
||||
discovery is a distinct subsystem under `src/discovery/lan/`.
|
||||
|
||||
### Role
|
||||
|
||||
LAN discovery adds two capabilities, both confined to the local link:
|
||||
|
||||
- **Advertising.** The node publishes a `_fips._udp.local.` DNS-SD
|
||||
service advert carrying its `npub`, its protocol version, and (if
|
||||
configured) a discovery scope. The advert is multicast on the local
|
||||
link only; it does not leave the broadcast domain unless the
|
||||
operator's network bridges mDNS.
|
||||
- **Browsing.** The node concurrently browses for the same service
|
||||
type, learns the endpoints of other FIPS nodes on the link, and
|
||||
initiates a normal FMP link to each newly-seen peer.
|
||||
|
||||
The mDNS service type is `_fips._udp.local.`
|
||||
(`src/discovery/lan/mod.rs:45`). Per RFC 6763 the `_udp` label denotes
|
||||
the IP transport used for the advert, not the FIPS upper protocol —
|
||||
both UDP and TCP FIPS endpoints announce under the same service type
|
||||
because the link-layer handshake travels over UDP either way. (In
|
||||
practice LAN discovery dials only over a UDP transport; see the
|
||||
handshake subsection.)
|
||||
|
||||
### When to use it
|
||||
|
||||
- **You run several FIPS nodes on one LAN** (a lab bench, an office
|
||||
segment, a home network) and want them to find each other without
|
||||
hand-maintaining `peers[]` blocks or standing up Nostr discovery.
|
||||
- **You want the lowest-latency pairing path.** Same-link pairing
|
||||
completes in well under a second with no relay round-trip.
|
||||
|
||||
Skip it when nodes are not on a shared broadcast domain (mDNS does not
|
||||
cross routed boundaries), or when you do not want the node to multicast
|
||||
its identity on the local link. LAN discovery is **opt-in and disabled
|
||||
by default**, so doing nothing leaves it off.
|
||||
|
||||
### How it works
|
||||
|
||||
The LAN discovery runtime (`src/discovery/lan/mod.rs`) is started
|
||||
during node initialization when `node.discovery.lan.enabled` is true.
|
||||
It is independent of Nostr discovery and runs even when Nostr is
|
||||
disabled (`src/node/lifecycle.rs:1159-1162`). Startup requires an
|
||||
operational UDP transport: the node advertises the port of its
|
||||
lowest-`TransportId` operational, non-bootstrap UDP transport, chosen
|
||||
deterministically so the advertised port is stable across restarts
|
||||
(`src/node/lifecycle.rs:1169-1180`). If no such port exists, the
|
||||
runtime returns `NoAdvertisedPort` and LAN discovery does not start
|
||||
(`src/discovery/lan/mod.rs:156-158`).
|
||||
|
||||
The runtime does two things concurrently:
|
||||
|
||||
1. **Responder.** It registers a DNS-SD service with instance name
|
||||
`fips-<first-16-chars-of-npub>` and a TXT record carrying the keys
|
||||
below. `mdns-sd`'s address auto-detection appends every non-loopback
|
||||
interface address, with `127.0.0.1` seeded so same-host peers and
|
||||
integration tests can still resolve the advert
|
||||
(`src/discovery/lan/mod.rs:182-203`).
|
||||
2. **Browser.** A background pump receives `ServiceResolved` events for
|
||||
the same service type. For each resolved advert it extracts the
|
||||
`npub` and `scope` TXT values, drops adverts that echo the node's own
|
||||
npub, drops cross-scope adverts (see scope filtering), drops records
|
||||
without an `npub`, and surfaces one `LanDiscoveredPeer` per routable
|
||||
interface address (`src/discovery/lan/mod.rs:212-299`). IPv6
|
||||
unicast link-local addresses without an interface scope id are
|
||||
skipped, since they cannot be dialed unambiguously
|
||||
(`src/discovery/lan/mod.rs:348-365`).
|
||||
|
||||
The TXT record carries three keys (`src/discovery/lan/mod.rs:47-55`):
|
||||
|
||||
| TXT key | Contents |
|
||||
| --- | --- |
|
||||
| `npub` | bech32-encoded npub of the advertising node |
|
||||
| `scope` | the node's discovery scope, if one is configured (omitted otherwise) |
|
||||
| `v` | FIPS protocol version (the same `PROTOCOL_VERSION` used by the Nostr advert) |
|
||||
|
||||
Once per node tick, the node drains browser events and acts on them in
|
||||
`poll_lan_discovery()` (`src/node/lifecycle.rs:907`, called from
|
||||
`src/node/handlers/rx_loop.rs:266`). For each discovered peer it finds
|
||||
a UDP transport whose family matches the peer address, parses the
|
||||
`npub` into a `PeerIdentity`, skips peers it is already connected to or
|
||||
currently connecting to, and otherwise initiates a connection.
|
||||
|
||||
### Handshake: Noise IK
|
||||
|
||||
LAN-discovered peers are dialed through the standard FMP outbound link
|
||||
path. `poll_lan_discovery()` calls `initiate_connection()`
|
||||
(`src/node/lifecycle.rs:380`), which, for connectionless transports
|
||||
such as UDP, allocates a link and **starts the Noise IK handshake**
|
||||
(documented at `src/node/lifecycle.rs:373-374`). This is the same
|
||||
link-layer handshake used by every other FMP connection — IK at the
|
||||
link layer per the FIPS architecture — not a different pattern for LAN
|
||||
peers.
|
||||
|
||||
The mDNS advert is **unauthenticated**: anyone on the link can
|
||||
multicast a TXT claiming any `npub`. Identity is proven end-to-end by
|
||||
the Noise IK handshake against the observed endpoint. A spoofed advert
|
||||
carrying another node's npub fails the handshake — the impostor does
|
||||
not hold the matching static key — and the half-open link is dropped.
|
||||
The mDNS advert is therefore a routing hint, never an identity
|
||||
assertion, exactly as a Nostr advert is treated (a successful contact
|
||||
is not trusted until FMP's Noise IK handshake completes).
|
||||
|
||||
> Note: a stale source doc-comment at `src/node/lifecycle.rs:904-906`
|
||||
> describes this path as a "Noise XX" handshake. That comment is
|
||||
> inaccurate — the path uses Noise IK as described above. The comment
|
||||
> is flagged for a separate source fix and does not reflect actual
|
||||
> behavior.
|
||||
|
||||
### Scope filtering
|
||||
|
||||
When a discovery scope is configured, the advert carries it in the
|
||||
`scope` TXT entry and the browser surfaces only peers whose advert
|
||||
carries a matching scope. Nodes on the same physical LAN but configured
|
||||
for different mesh networks therefore do not cross-feed each other.
|
||||
|
||||
The scope is resolved by `lan_discovery_scope()`
|
||||
(`src/node/lifecycle.rs:880-902`): the explicit
|
||||
`node.discovery.lan.scope`, if non-empty, is used directly. Otherwise
|
||||
the node falls back to deriving a scope from the Nostr discovery `app`
|
||||
tag (stripping the `fips-overlay-v1:` prefix when present). This lets
|
||||
an application keep its public, relay-visible Nostr `app` tag generic
|
||||
while still isolating LAN discovery per private network, or share one
|
||||
value across both. A node with no scope on either side surfaces all
|
||||
adverts it sees on the link.
|
||||
|
||||
### Configuration
|
||||
|
||||
LAN discovery is configured under `node.discovery.lan.*`
|
||||
(`src/config/node.rs:222-227`, `src/discovery/lan/mod.rs:88-129`):
|
||||
|
||||
| Key | Type | Default | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `node.discovery.lan.enabled` | bool | `false` | Master switch. LAN discovery is opt-in; default-off avoids an unexpected per-link identity multicast on upgrade. |
|
||||
| `node.discovery.lan.service_type` | string | `_fips._udp.local.` | DNS-SD service type. Overridable mainly so integration tests can isolate multiple services on one loopback interface. |
|
||||
| `node.discovery.lan.scope` | string (optional) | unset | Application/network scope carried in the LAN-only `scope` TXT record. Kept deliberately separate from the public Nostr `app` tag. When unset, the scope falls back to the derived Nostr `app` value. |
|
||||
|
||||
The identity surface published over mDNS (`npub`, version, optional
|
||||
scope) is a strict subset of what `nostr.advertise` already publishes
|
||||
publicly, so enabling LAN discovery adds no marginal privacy cost
|
||||
beyond making the node's presence observable on its own local link.
|
||||
|
||||
### Relationship to Nostr discovery
|
||||
|
||||
The two mechanisms are complementary and independent:
|
||||
|
||||
| | Nostr-mediated | LAN/mDNS |
|
||||
| --- | --- | --- |
|
||||
| Reach | Internet-wide, via relays | Same broadcast domain only |
|
||||
| Signaling channel | Public Nostr relays | mDNS multicast on the local link |
|
||||
| NAT traversal | STUN + UDP hole-punch for `udp:nat` peers | None — endpoint is link-routable by construction |
|
||||
| Identity carrier | signed kind 37195 advert (authenticated at publish) | unauthenticated mDNS TXT (routing hint only) |
|
||||
| Identity proof | FMP Noise IK on the connection | FMP Noise IK on the connection |
|
||||
| Default | disabled (`nostr.enabled: false`) | disabled (`lan.enabled: false`) |
|
||||
| Scope key | `app` tag (public) | `scope` TXT (link-local), falls back to `app` |
|
||||
|
||||
Both ultimately converge on the same trust boundary: discovery only
|
||||
supplies candidate endpoints, and no peer is trusted until FMP's Noise
|
||||
IK handshake confirms the claimed identity. A node may run both at
|
||||
once — for example, advertising globally over Nostr while also pairing
|
||||
instantly with same-LAN peers — with no interaction between the two
|
||||
beyond the shared scope fallback.
|
||||
|
||||
## See also
|
||||
|
||||
- [../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md)
|
||||
|
||||
@@ -120,6 +120,7 @@ for internet connectivity:
|
||||
| UDP/IP | host:port | 1280–1472 | Unreliable | Primary internet transport |
|
||||
| TCP/IP | host:port | Stream | Reliable | Requires length-prefix framing |
|
||||
| Tor | .onion | Stream | Reliable | High latency, strong anonymity |
|
||||
| Nym | host:port | Stream | Reliable | Mixnet, outbound-only, strong anonymity |
|
||||
|
||||
**Shared medium transports** operate over broadcast- or multicast-capable
|
||||
media:
|
||||
@@ -190,6 +191,7 @@ proceed.
|
||||
| --------- | ---------------- |
|
||||
| TCP/IP | TCP three-way handshake |
|
||||
| Tor | Circuit establishment (typically 10–60s, default timeout 120s) |
|
||||
| Nym | SOCKS5 connect through mixnet (minutes possible, default timeout 300s) |
|
||||
| BLE | L2CAP CoC or GATT connection |
|
||||
| Serial | Physical connection (static) |
|
||||
|
||||
@@ -597,6 +599,120 @@ SOCKS5-level errors, MTU rejections, accepted/rejected inbound
|
||||
connections, and Tor control-port errors. The full counter table
|
||||
lives in [../reference/transports.md](../reference/transports.md).
|
||||
|
||||
## Nym: The Mixnet Transport
|
||||
|
||||
The Nym transport routes FIPS traffic through the Nym mixnet, providing
|
||||
network-level anonymity via Sphinx packet routing and timing
|
||||
obfuscation. It uses the "mixnet-as-proxy" pattern: a node connects
|
||||
outbound through a local `nym-socks5-client` SOCKS5 proxy, which carries
|
||||
the traffic into the mixnet. The `nym-socks5-client` runs as a separate
|
||||
process alongside the fips daemon and must be started independently.
|
||||
|
||||
Like Tor, Nym is a privacy-oriented deployment mode chosen for the
|
||||
anonymity properties of the mixnet, not a failover for other transports.
|
||||
Like TCP and Tor, it is connection-oriented and reliable; the same
|
||||
TCP-over-TCP considerations apply, and cost-based parent selection
|
||||
naturally deprioritizes the high-latency Nym links.
|
||||
|
||||
### Architecture
|
||||
|
||||
The Nym transport is a separate `NymTransport` implementation. It reuses
|
||||
the FMP header-based stream reader (`tcp/stream.rs`) for packet framing
|
||||
on the underlying byte stream, and follows the same connection-pool
|
||||
pattern as the TCP and Tor transports.
|
||||
|
||||
It maintains two pools: a `ConnectingPool` for background SOCKS5
|
||||
connection attempts, and an established pool of `NymConnection` entries.
|
||||
Each `NymConnection` holds a write half, a per-connection receive task,
|
||||
the configured MTU, and a connection timestamp.
|
||||
|
||||
| Property | Value |
|
||||
| -------- | ----- |
|
||||
| Addressing | IP:port or hostname:port |
|
||||
| Default MTU | 1400 bytes |
|
||||
| Framing | FMP header-based (shared with TCP) |
|
||||
| Connection model | Outbound-only, non-blocking connect through SOCKS5 |
|
||||
| Platform | Cross-platform (requires external nym-socks5-client) |
|
||||
|
||||
### Outbound-Only
|
||||
|
||||
The Nym transport is strictly outbound. It supports no inbound service:
|
||||
`accept_connections()` returns `false` and `discover()` returns no
|
||||
peers. A node using the Nym transport can initiate links to remote peers
|
||||
through the mixnet, but cannot accept inbound connections over Nym. (A
|
||||
node can still accept inbound links over other transports it runs.)
|
||||
|
||||
### Address Types
|
||||
|
||||
The Nym transport accepts two address formats, parsed into an internal
|
||||
target address:
|
||||
|
||||
- **IP:port** — a numeric IP and port, sent to the SOCKS5 proxy as a
|
||||
numeric target.
|
||||
- **Hostname:port** — the hostname is passed through SOCKS5 so it is
|
||||
resolved on the exit side rather than locally.
|
||||
|
||||
Both forms are routed through the same SOCKS5 proxy.
|
||||
|
||||
### Connection Establishment
|
||||
|
||||
Connection setup follows the same non-blocking pattern as the TCP and
|
||||
Tor transports. When FMP needs to reach a peer, the node initiates a
|
||||
background connect (`connect_async`). The transport spawns a background
|
||||
tokio task that opens a SOCKS5 connection through the local
|
||||
`nym-socks5-client`, configures the socket (including TCP keepalive),
|
||||
splits the stream, and spawns a per-connection receive loop using the
|
||||
shared FMP stream reader. The call returns immediately while the connect
|
||||
proceeds in the background.
|
||||
|
||||
SOCKS5 connection setup through the mixnet can take much longer than a
|
||||
direct TCP connection because each connection traverses multiple mix
|
||||
nodes with timing obfuscation. Accordingly the connect timeout defaults
|
||||
to 300 seconds (`connect_timeout_ms`). Non-blocking connect is essential
|
||||
here — a blocking connect would stall the FMP event loop for the
|
||||
duration of mixnet setup. As a fallback, `send_async(addr, data)`
|
||||
performs a connect-on-send if no connection to the address yet exists.
|
||||
|
||||
Each outbound packet is checked against the configured MTU before being
|
||||
written; an oversized packet is rejected with an MTU-exceeded error
|
||||
rather than being sent.
|
||||
|
||||
### Startup Readiness
|
||||
|
||||
At startup the transport validates the configured `socks5_addr` and then
|
||||
probes the SOCKS5 port to wait for `nym-socks5-client` to become ready,
|
||||
using exponential backoff (starting at 1 second, capped at 10 seconds
|
||||
between attempts) up to `startup_timeout_secs` (default 120 seconds). If
|
||||
the proxy does not become reachable within that window, the transport
|
||||
logs a warning and starts anyway; outbound connections then fail until
|
||||
the `nym-socks5-client` becomes available.
|
||||
|
||||
### Session Independence
|
||||
|
||||
Same as TCP and Tor: loss of a Nym connection does **not** tear down the
|
||||
FIPS peer. Noise keys, MMP state, and FSP sessions survive reconnection.
|
||||
|
||||
### Configuration
|
||||
|
||||
The Nym transport block (`transports.nym.*`) has the following fields:
|
||||
|
||||
| Field | Default | Description |
|
||||
| ----- | ------- | ----------- |
|
||||
| `socks5_addr` | `127.0.0.1:1080` | Address (host:port) of the local nym-socks5-client SOCKS5 proxy |
|
||||
| `connect_timeout_ms` | `300000` | Outbound SOCKS5 connect timeout in milliseconds (300s) |
|
||||
| `mtu` | `1400` | Maximum FIPS packet size for Nym connections, in bytes |
|
||||
| `startup_timeout_secs` | `120` | Seconds to wait for nym-socks5-client to become ready at startup |
|
||||
|
||||
The Nym transport requires an external `nym-socks5-client`. Named
|
||||
instances are supported for multiple proxy endpoints. Unknown
|
||||
configuration keys are rejected.
|
||||
|
||||
### Statistics
|
||||
|
||||
The Nym transport exposes per-instance counters covering successful
|
||||
send/receive, send/receive errors, connection establishment, SOCKS5-level
|
||||
errors, connect timeouts, and MTU rejections.
|
||||
|
||||
## Discovery
|
||||
|
||||
Discovery determines that a FIPS-capable endpoint is reachable at a given
|
||||
@@ -723,7 +839,8 @@ TransportType {
|
||||
}
|
||||
```
|
||||
|
||||
Predefined types exist for UDP, TCP, Ethernet, WiFi, Tor, and Serial.
|
||||
Predefined types exist for UDP, TCP, Ethernet, WiFi, Tor, Nym, BLE, and
|
||||
Serial.
|
||||
|
||||
### Congestion Reporting
|
||||
|
||||
@@ -748,6 +865,7 @@ on all forwarded datagrams.
|
||||
| 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) |
|
||||
| Nym | Not implemented | Returns `None` (TCP handles congestion internally) |
|
||||
| Ethernet | Not implemented | Returns `None` |
|
||||
|
||||
### Transport Addresses
|
||||
@@ -778,6 +896,7 @@ transitions through `Starting` to `Up` (operational). `stop()` moves to
|
||||
| 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 |
|
||||
| Nym | **Implemented** | Outbound-only SOCKS5 through nym-socks5-client, mixnet anonymity, IP/hostname addressing |
|
||||
| BLE | **Implemented** (Linux/glibc only; experimental) | L2CAP CoC, ATT_MTU negotiation, per-link MTU; musl/macOS/Windows skip |
|
||||
| Radio | Future direction | Constrained MTU (51–222 bytes) |
|
||||
| Serial | Future direction | SLIP/COBS framing, point-to-point |
|
||||
|
||||
@@ -526,7 +526,7 @@ own.
|
||||
|
||||
| Failure | Symptom | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| Symmetric NAT (one side) | Punch timeout | Retry with port-prediction heuristics; otherwise fall back to a relay or different transport |
|
||||
| Symmetric NAT (one side) | Punch timeout | Retry with port-prediction heuristics; otherwise fall back to an application-level relay |
|
||||
| Symmetric NAT (both sides) | Punch timeout | Application-level relay required |
|
||||
| Relay latency > 60 s | Stale reflexive address | Use low-latency relays; consider self-hosted relay |
|
||||
| Relay does not support ephemeral kinds | Signaling events persist | Use NIP-40 expiration + NIP-09 deletion as fallback |
|
||||
|
||||
@@ -284,6 +284,7 @@ transports:
|
||||
tor:
|
||||
mode: directory
|
||||
socks5_addr: "127.0.0.1:9050"
|
||||
advertised_port: 8443
|
||||
directory_service:
|
||||
hostname_file: "/var/lib/tor/fips/hostname"
|
||||
bind_addr: "127.0.0.1:8444"
|
||||
|
||||
@@ -169,7 +169,7 @@ sudo usermod -aG fips $USER
|
||||
Then:
|
||||
|
||||
```sh
|
||||
fipsctl show node
|
||||
fipsctl show status
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
@@ -151,7 +151,7 @@ rather than `flush ruleset`, which destroys every table on the host.
|
||||
Symptom: `nc -U /run/fips/gateway.sock` fails with "Permission
|
||||
denied" or "No such file or directory".
|
||||
|
||||
The socket is owned by root with mode `0660` (group `fips`). Either
|
||||
The socket is owned by root with mode `0770` (group `fips`). Either
|
||||
run `nc` as root (`sudo nc -U ...`) or add your user to the `fips`
|
||||
group and re-login. If the file does not exist at all, the gateway
|
||||
either failed to start (check `journalctl -u fips-gateway`) or
|
||||
|
||||
@@ -69,6 +69,7 @@ Time-series metrics from the in-process history rings.
|
||||
| Subcommand | Control-socket command | Description |
|
||||
| ---------- | ---------------------- | ----------- |
|
||||
| `stats list` | `show_stats_list` | Enumerate available metrics, their units, and the per-ring retention windows. |
|
||||
| `stats metrics` | `show_metrics` | Dump current counter values for every protocol metric family (`forwarding`, `discovery`, `tree`, `bloom`, `congestion`, `errors`). |
|
||||
| `stats peers` | `show_stats_peers` | List peers tracked in stats history (active or recently active). |
|
||||
| `stats history <metric> [options]` | `show_stats_history` | Fetch a time-series window for one metric. |
|
||||
|
||||
@@ -104,7 +105,7 @@ Tell the daemon to dial a peer over a specific transport.
|
||||
| -------- | ----------- |
|
||||
| `peer` | npub (bech32) or hostname from `/etc/fips/hosts`. |
|
||||
| `address` | Transport endpoint, e.g. `192.168.1.10:2121`, `[2001:db8::1]:2121`, or a Tor onion. FIPS-mesh ULAs (`fd00::/8`) are rejected for the IP-based transports (udp, tcp, ethernet). |
|
||||
| `transport` | One of `udp`, `tcp`, `tor`, `ethernet`. |
|
||||
| `transport` | One of `udp`, `tcp`, `tor`, `nym`, `ethernet`. The named transport must be configured and running. |
|
||||
|
||||
### `disconnect <peer>`
|
||||
|
||||
|
||||
@@ -15,8 +15,11 @@ socket, polls a small set of `show_*` queries on a timer, and renders
|
||||
the state in a tabbed full-screen UI. A separate poll runs against the
|
||||
gateway control socket when the Gateway tab is active.
|
||||
|
||||
`fipstop` is read-only — it cannot mutate daemon state. Use
|
||||
[`fipsctl`](cli-fipsctl.md) for `connect` / `disconnect` and friends.
|
||||
`fipstop` is almost entirely read-only: the only state-mutating action
|
||||
it offers is disconnecting a peer (`Del` on a selected Peers row, with
|
||||
a confirmation prompt — see [Keybindings](#keybindings)). For
|
||||
`connect` and other mutating commands, use
|
||||
[`fipsctl`](cli-fipsctl.md).
|
||||
|
||||
## Options
|
||||
|
||||
@@ -86,6 +89,11 @@ empty list and the panel hides.
|
||||
|
||||
## Keybindings
|
||||
|
||||
Press `?` at any time for an in-app help overlay. The overlay and the
|
||||
status-bar hint footer both read from a single keybinding registry
|
||||
keyed by `(tab, mode)`, so the always-visible hints describe exactly
|
||||
the keys the current context accepts.
|
||||
|
||||
### Global
|
||||
|
||||
| Key | Action |
|
||||
@@ -94,7 +102,8 @@ empty list and the panel hides.
|
||||
| `Tab` | Next tab. |
|
||||
| `Shift-Tab` | Previous tab. |
|
||||
| `g` | Jump to the Graphs tab. |
|
||||
| `Esc` | Close detail view (if open). |
|
||||
| `?` | Toggle the help overlay. |
|
||||
| `Esc` | Close an open detail view; otherwise deselect the active table row. |
|
||||
|
||||
### Table tabs (Peers, Sessions, Transports, Gateway)
|
||||
|
||||
@@ -102,6 +111,13 @@ empty list and the panel hides.
|
||||
| --- | ------ |
|
||||
| `Up`, `Down` | Move row selection. |
|
||||
| `Enter` | Open detail view for the selected row. |
|
||||
| `Esc` | Deselect the row (return to the tab's overview state). |
|
||||
|
||||
### Peers tab (extra)
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------ |
|
||||
| `Del` | Disconnect the selected peer. Opens a `Y`/`N` confirmation modal first; this is the only state-mutating action in `fipstop`. |
|
||||
|
||||
### Transports tab (extra)
|
||||
|
||||
@@ -112,16 +128,43 @@ empty list and the panel hides.
|
||||
| `e` | Expand all transports. |
|
||||
| `c` | Collapse all transports. |
|
||||
|
||||
### Multi-pane scrolling tabs (Tree, Filters, Routing)
|
||||
|
||||
Each lays out stacked panes that scroll independently.
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------ |
|
||||
| `f` | Move focus to the next pane. |
|
||||
| `Up`, `Down` | Scroll the focused pane by one row. |
|
||||
| `PageUp`, `PageDown` | Scroll the focused pane by ten rows. |
|
||||
| `Home`, `End` | Jump to the top / bottom of the focused pane. |
|
||||
|
||||
### Performance tab (extra)
|
||||
|
||||
The Performance tab lays out two panes (Link MMP, Session MMP).
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------ |
|
||||
| `f` | Move focus between the Link and Session MMP panes. |
|
||||
| `Up`, `Down` | Scroll the focused pane. |
|
||||
| `PageUp`, `PageDown` | Scroll the focused pane by ten rows. |
|
||||
| `Home`, `End` | Jump to the top / bottom of the focused pane. |
|
||||
| `s` | Cycle the sort column of the focused pane. |
|
||||
| `Shift-S` | Toggle the sort direction of the focused pane. |
|
||||
|
||||
### Graphs tab (extra)
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------ |
|
||||
| `Up`, `Down` | Scroll within the stacked plots. |
|
||||
| `Up`, `Down` | Scroll the stacked plots; in `MetricByPeer` mode, move the by-peer selection (and follow it when the by-peer detail is open). |
|
||||
| `Right`, `Space` | Next time window. Cycles `1m / 1s` → `10m / 1s` → `1h / 1s` → `24h / 1m`. |
|
||||
| `Left` | Previous time window. |
|
||||
| `Enter` | In `MetricByPeer` mode, expand the selected peer summary into a full-pane plot. |
|
||||
| `m` | Cycle view mode: `Node` (stacked node metrics) → `MetricByPeer` (one per-peer metric across all peers) → `PeerByMetric` (all per-peer metrics for one peer). |
|
||||
| `n` | Next selector (next per-peer metric in MetricByPeer; next peer in PeerByMetric). |
|
||||
| `Shift-N` | Previous selector. |
|
||||
| `s` | Cycle the sort column of the by-peer summary list. |
|
||||
| `Shift-S` | Toggle the sort direction of the by-peer summary list. |
|
||||
|
||||
## Exit Codes
|
||||
|
||||
|
||||
@@ -236,6 +236,29 @@ addresses for the punch socket port.
|
||||
During punching, compatible private-subnet candidates and reflexive candidates
|
||||
are attempted in parallel; the first successful path wins.
|
||||
|
||||
#### LAN Discovery (`node.discovery.lan.*`)
|
||||
|
||||
Peer discovery on the local link via mDNS / DNS-SD (RFC 6762 / RFC
|
||||
6763). When enabled, the node publishes a `_fips._udp.local.` service
|
||||
advert carrying its `npub` (and optional scope) and concurrently
|
||||
browses for the same service type to learn same-broadcast-domain peers.
|
||||
The result is sub-second peer pairing with no Nostr-relay roundtrip,
|
||||
STUN observation, or NAT traversal: the observed endpoint is by
|
||||
construction routable from the consumer's LAN.
|
||||
|
||||
mDNS adverts are unauthenticated, so a LAN advert is treated only as a
|
||||
routing hint. Identity is still proven end-to-end by the Noise XX
|
||||
handshake the node initiates against the observed endpoint; a spoofed
|
||||
advert carrying another peer's npub fails the handshake and is dropped.
|
||||
LAN discovery requires an active UDP transport (peers dial the
|
||||
advertised UDP port to begin the handshake).
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `node.discovery.lan.enabled` | bool | `false` | Master switch. Opt-in: enable for sub-second same-LAN pairing. Default-off avoids reintroducing a per-LAN identity broadcast on nodes that have deliberately disabled other discovery channels |
|
||||
| `node.discovery.lan.service_type` | string | `"_fips._udp.local."` | DNS-SD service type. Primarily an override for integration tests running multiple isolated services on one loopback interface; leave at the default in production |
|
||||
| `node.discovery.lan.scope` | string | *(none)* | Optional application/network scope carried in a `scope=<name>` TXT entry. Browsers with a scope set only surface peers advertising the same scope, so nodes on the same physical LAN configured for different mesh networks do not cross-feed. Intentionally separate from `node.discovery.nostr.app` so relay-visible adverts can stay generic while LAN discovery is isolated per private network |
|
||||
|
||||
### Spanning Tree (`node.tree.*`)
|
||||
|
||||
Controls tree construction and parent selection.
|
||||
@@ -577,6 +600,25 @@ HiddenServiceDir /var/lib/tor/fips
|
||||
HiddenServicePort 8443 127.0.0.1:8444
|
||||
```
|
||||
|
||||
### Nym (`transports.nym.*`)
|
||||
|
||||
Nym transport routes FIPS traffic through the Nym mixnet for
|
||||
metadata-resistant anonymity. Outbound-only: connections are made
|
||||
through a `nym-socks5-client` SOCKS5 proxy that must be running
|
||||
separately (e.g. as a service running alongside the fips daemon or as a
|
||||
container). There is no inbound listener — a Nym-only node initiates
|
||||
outbound links but is not reachable for unsolicited inbound handshakes.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `transports.nym.socks5_addr` | string | `"127.0.0.1:1080"` | `nym-socks5-client` SOCKS5 proxy address (host:port) |
|
||||
| `transports.nym.connect_timeout_ms` | u64 | `300000` | Outbound connect timeout in milliseconds. Mixnet SOCKS5 connections traverse 3 mix nodes with timing obfuscation and can take several minutes, so this is generous (300s). |
|
||||
| `transports.nym.mtu` | u16 | `1400` | Default MTU |
|
||||
| `transports.nym.startup_timeout_secs` | u64 | `120` | Seconds to wait for `nym-socks5-client` to become ready at startup before giving up |
|
||||
|
||||
**Named instances.** Like other transports, multiple Nym instances can
|
||||
be configured with named sub-keys for different SOCKS5 proxy endpoints.
|
||||
|
||||
### BLE (`transports.ble.*`)
|
||||
|
||||
Bluetooth Low Energy transport using L2CAP Connection-Oriented Channels.
|
||||
@@ -891,6 +933,9 @@ node:
|
||||
backoff_base_secs: 0
|
||||
backoff_max_secs: 0
|
||||
forward_min_interval_secs: 2
|
||||
# lan: # uncomment to enable mDNS LAN discovery
|
||||
# enabled: true # opt-in, default false
|
||||
# scope: "my-mesh" # optional per-network scope filter
|
||||
tree:
|
||||
announce_min_interval_ms: 500
|
||||
parent_hysteresis: 0.2 # cost improvement fraction for parent switch
|
||||
@@ -985,6 +1030,11 @@ transports:
|
||||
# # bind_addr: "127.0.0.1:8443"
|
||||
# # max_inbound_connections: 64
|
||||
# # advertised_port: 443 # public-facing onion port for Nostr adverts
|
||||
# nym: # uncomment to enable Nym mixnet transport (outbound-only)
|
||||
# socks5_addr: "127.0.0.1:1080" # nym-socks5-client SOCKS5 proxy address
|
||||
# connect_timeout_ms: 300000 # connect timeout (300s for mixnet)
|
||||
# mtu: 1400 # default MTU
|
||||
# startup_timeout_secs: 120 # wait for nym-socks5-client to be ready
|
||||
# ble: # uncomment to enable BLE transport (Linux only, requires BlueZ)
|
||||
# adapter: "hci0" # HCI adapter name
|
||||
# psm: 0x0085 # L2CAP PSM (133)
|
||||
|
||||
@@ -100,21 +100,22 @@ table below lists every command currently registered.
|
||||
|
||||
| Command | Params | `data` shape (top-level keys) |
|
||||
| ------- | ------ | ----------------------------- |
|
||||
| `show_status` | — | `version`, `npub`, `node_addr`, `ipv6_addr`, `state`, `is_leaf_only`, `peer_count`, `session_count`, `link_count`, `transport_count`, `connection_count`, `tun_state`, `tun_name`, `effective_ipv6_mtu`, `control_socket`, `pid`, `exe_path`, `uptime_secs`, `estimated_mesh_size`, `forwarding`, `sparklines`. |
|
||||
| `show_status` | — | `version`, `npub`, `node_addr`, `ipv6_addr`, `state`, `is_leaf_only`, `is_root` (bool — this node is the spanning-tree root), `root` (hex node-addr of the current tree root), `persistent` (bool — identity is persisted, i.e. `persistent` set or an `nsec` configured), `peer_count`, `session_count`, `link_count`, `transport_count`, `connection_count`, `transport_peer_counts` (object mapping transport-type name to its connected-peer count; configured transports appear with `0`), `tun_state`, `tun_name`, `effective_ipv6_mtu`, `control_socket`, `pid`, `exe_path`, `uptime_secs`, `estimated_mesh_size`, `forwarding`, `sparklines`. |
|
||||
| `show_acl` | — | `allow_file`, `deny_file`, `enforcement_active`, `effective_mode`, `default_decision`, `allow_all`, `deny_all`, `allow_file_entries`, `deny_file_entries`, `allow_entries`, `deny_entries`. |
|
||||
| `show_peers` | — | `peers[]` — per-peer object: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `connectivity`, `link_id`, `direction`, `transport_addr`, `transport_type`, `is_parent`, `is_child`, `tree_depth`, `stats`, `noise`, `current_k_bit`, `mmp`, plus optional `nostr_traversal`, `rekey_in_progress`, `rekey_draining`. |
|
||||
| `show_peers` | — | `peers[]` — per-peer object: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `connectivity`, `link_id`, `direction`, `transport_addr`, `transport_type`, `is_parent`, `is_child`, `tree_depth`, `effective_depth` (`tree_depth + link_cost` — the metric `evaluate_parent` ranks on; `null` when the peer has no coords, or is unmeasured while another peer has an SRTT sample, per the cold-start gate), `stats`, `noise`, `current_k_bit`, `mmp`, plus optional `nostr_traversal`, `rekey_in_progress`, `rekey_draining`. |
|
||||
| `show_links` | — | `links[]` — `link_id`, `transport_id`, `remote_addr`, `direction`, `state`, `created_at_ms`, `stats`. |
|
||||
| `show_tree` | — | `my_node_addr`, `root`, `is_root`, `depth`, `my_coords[]`, `parent`, `parent_display_name`, `declaration_sequence`, `declaration_signed`, `peer_tree_count`, `peers[]`, `stats`. |
|
||||
| `show_tree` | — | `my_node_addr`, `root`, `root_npub` (bech32 npub of the current tree root), `is_root`, `depth`, `my_coords[]`, `parent`, `parent_display_name`, `declaration_sequence`, `declaration_signed`, `peer_tree_count`, `peers[]`, `stats`. |
|
||||
| `show_sessions` | — | `sessions[]` — `remote_addr`, `npub`, `display_name`, `state` (`established`, `initiating`, `awaiting_msg3`, `unknown`), `is_initiator`, `last_activity_ms`, `stats`, optional `mmp`, `current_k_bit`, `is_draining`. |
|
||||
| `show_bloom` | — | `own_node_addr`, `is_leaf_only`, `sequence`, `leaf_dependent_count`, `leaf_dependents[]`, `peer_filters[]`, `stats`. |
|
||||
| `show_bloom` | — | `own_node_addr`, `is_leaf_only`, `sequence`, `leaf_dependent_count`, `leaf_dependents[]`, `peer_filters[]`, `uptree_fill_ratio` (fill ratio of the last filter actually sent to the tree parent), `uptree_estimated_count` (cardinality estimate of that uptree filter — this node's whole subtree under split-horizon, not the mesh; both are `null` for a root node or before the first announce), `stats`. |
|
||||
| `show_mmp` | — | `peers[]` (link-layer per peer), `sessions[]` (session-layer per session). Each entry includes loss/RTT/ETX/goodput, smoothed values, trends. |
|
||||
| `show_cache` | — | `count`, `max_entries`, `fill_ratio`, `default_ttl_ms`, `expired`, `avg_age_ms`, `entries[]` — per-destination coords, depth, age, last-used, optional `path_mtu`. |
|
||||
| `show_connections` | — | `connections[]` — pending handshakes: `link_id`, `direction`, `handshake_state`, `started_at_ms`, `idle_ms`, `resend_count`, optional `expected_peer`. |
|
||||
| `show_transports` | — | `transports[]` — `transport_id`, `type`, `state`, `mtu`, `name`, `local_addr`, optional `tor_mode`, `onion_address`, `tor_monitoring`, `stats`. |
|
||||
| `show_routing` | — | `coord_cache_entries`, `identity_cache_entries`, `pending_lookups[]`, `pending_tun_destinations`, `pending_tun_packets`, `recent_requests`, `retries[]`, `forwarding`, `discovery`, `error_signals`, `congestion`. |
|
||||
| `show_routing` | — | `coord_cache_entries`, `identity_cache_entries`, `pending_lookups[]`, `pending_tun_destinations`, `pending_tun_packets`, `recent_requests`, `retries[]`, `forwarding`, `discovery` (request/response sub-counters; includes `req_deduplicated` — requests suppressed as recent duplicates — and `req_dedup_cache_full` — requests admitted because the dedup cache was full), `error_signals`, `congestion`. |
|
||||
| `show_identity_cache` | — | `entries[]`, `count`, `max_entries`. Each entry: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `last_seen_ms`, `age_ms`. |
|
||||
| `show_listening_sockets` | — | `fips0_addr`, `firewall_active` (bool — `inet fips` table loaded), `sockets[]`. Each entry: `proto` (`tcp` / `udp`), `local_addr` (`::` or the node's fd00::/8 address), `port`, `pid` (nullable), `process` (nullable), `wildcard_bind` (bool — `local_addr == ::`), `filter` (`accept` / `drop` / `unknown` / `no_firewall`). Linux-only; returns an empty `sockets[]` on other platforms. |
|
||||
| `show_stats_list` | — | `metrics[]` (each with `name`, `unit`, `scope`), `fast_ring_seconds`, `slow_ring_minutes`, `peer_retention_seconds`. |
|
||||
| `show_metrics` | — | Flat snapshot of every counter family in the metrics registry: `forwarding`, `discovery`, `tree`, `bloom`, `congestion`, `errors`. Each value is that family's counter snapshot object. Counter-only — gauges/histograms that need the live node are excluded. Served off the main loop. Silent-rejection sites classify their reason as a typed `RejectReason` and increment the matching per-family counter exposed here — see [Rejection reasons](#rejection-reasons). |
|
||||
| `show_stats_history` | `metric` (req), `peer` (req for per-peer metrics), `window` (`<N>s` / `<N>m` / `<N>h`, default `10m`), `granularity` (`1s` / `1m`, default `1s`) | A single `Series`: `metric`, `unit`, `granularity_seconds`, `values[]`. |
|
||||
| `show_stats_all_history` | `peer` (optional npub), `window`, `granularity` | `granularity_seconds`, `window_seconds`, `peer`, `series[]` (one per metric). |
|
||||
| `show_stats_peers` | — | `peers[]`, `count`. Each entry: `npub`, `node_addr`, `display_name`, `is_active`, `first_seen_secs_ago`, `last_contact_secs_ago`. |
|
||||
@@ -124,11 +125,35 @@ The schema of each query response is pinned by snapshot tests in
|
||||
`src/control/snapshots/`; intentional schema changes regenerate those
|
||||
fixtures.
|
||||
|
||||
### Rejection reasons
|
||||
|
||||
Silent-rejection paths across the node classify why a message was
|
||||
dropped via a typed `RejectReason` rather than only logging it, so the
|
||||
*what* of a rejection is visible in the counter snapshots above. The
|
||||
top-level reason set has eight families, mirroring the protocol-layer /
|
||||
subsystem split of the metrics:
|
||||
|
||||
- **Tree** — spanning-tree `TreeAnnounce` processing rejections.
|
||||
- **Bloom** — bloom-filter `FilterAnnounce` processing rejections.
|
||||
- **Discovery** — discovery request / response processing rejections.
|
||||
- **Handshake** — Noise handshake state-machine rejections.
|
||||
- **Session** — FSP session state-machine rejections.
|
||||
- **Mmp** — MMP link-layer rejections.
|
||||
- **Forwarding** — forwarding-path rejections (no-route, TTL, MTU).
|
||||
- **Transport** — transport-layer rejections (admission caps, framing).
|
||||
|
||||
Each rejection increments the corresponding counter in its family's
|
||||
stats, surfaced through `show_metrics` (the `tree`, `bloom`,
|
||||
`discovery`, and `forwarding` families carry their own counters; the
|
||||
`errors` family and the remaining subsystem counters carry the rest).
|
||||
The full per-family variant list lives in `src/node/reject.rs`; it is
|
||||
not reproduced here to avoid duplicating the source.
|
||||
|
||||
### Mutating commands
|
||||
|
||||
| Command | Required params | Behaviour |
|
||||
| ------- | --------------- | --------- |
|
||||
| `connect` | `npub` (bech32), `address` (transport endpoint), `transport` (`udp`, `tcp`, `tor`, `ethernet`) | Asks the node to dial the peer over the named transport. Returns the API result on success or an error string on failure. |
|
||||
| `connect` | `npub` (bech32), `address` (transport endpoint), `transport` (`udp`, `tcp`, `tor`, `nym`, `ethernet`) | Asks the node to dial the peer over the named transport. The named transport must be configured and running. Returns the API result on success or an error string on failure. |
|
||||
| `disconnect` | `npub` (bech32) | Asks the node to drop the link to the named peer. |
|
||||
|
||||
Both commands run on the daemon's main task and may block briefly
|
||||
|
||||
@@ -34,6 +34,8 @@ module.
|
||||
| `connections_rejected` | Rejected inbound connections (limit exceeded) |
|
||||
| `connect_timeouts` | Connection timeout count |
|
||||
| `connect_refused` | Connection refused count |
|
||||
| `pool_inbound` | Current inbound connections held in the connection pool (gauge) |
|
||||
| `pool_outbound` | Current outbound connections held in the connection pool (gauge) |
|
||||
|
||||
## Ethernet
|
||||
|
||||
@@ -61,6 +63,23 @@ module.
|
||||
| `connections_accepted` | Accepted inbound connections via onion service |
|
||||
| `connections_rejected` | Rejected inbound connections (limit exceeded) |
|
||||
| `control_errors` | Tor control port errors |
|
||||
| `pool_inbound` | Current inbound connections held in the connection pool (gauge) |
|
||||
| `pool_outbound` | Current outbound connections held in the connection pool (gauge) |
|
||||
|
||||
## Nym
|
||||
|
||||
| Counter | Description |
|
||||
| ------- | ----------- |
|
||||
| `packets_sent` / `bytes_sent` | Successful sends |
|
||||
| `packets_recv` / `bytes_recv` | Successful receives |
|
||||
| `send_errors` / `recv_errors` | Send/receive failures |
|
||||
| `mtu_exceeded` | Packets rejected for MTU violation |
|
||||
| `connections_established` | Successful SOCKS5 connections through `nym-socks5-client` |
|
||||
| `connect_timeouts` | Connection timeout count |
|
||||
| `socks5_errors` | SOCKS5 protocol errors |
|
||||
|
||||
Nym is outbound-only (no inbound listener), so there are no
|
||||
`connections_accepted` / `connections_rejected` counters.
|
||||
|
||||
## Bluetooth
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
# FIPS v0.4.0
|
||||
|
||||
**Released**: 2026-06-DD (provisional)
|
||||
|
||||
v0.4.0 is the throughput-and-observability release on the v0.3.x wire
|
||||
format. It adds two new ways for nodes to find and reach each other (the
|
||||
Nym mixnet transport and opt-in mDNS LAN discovery), overhauls the data
|
||||
plane for higher single-node throughput and lower per-packet CPU, moves
|
||||
the entire operator read surface off the data-plane hot path so
|
||||
observability stays responsive under load, ships a reworked `fipstop`
|
||||
TUI, and hardens FMP and FSP rekey to be hitless under packet loss in
|
||||
both directions. It also folds in the accumulated mesh-convergence,
|
||||
admission-control, and packaging fixes from the maintenance line.
|
||||
|
||||
v0.4.0 is wire-compatible with v0.3.0. Mixed meshes interoperate; there
|
||||
is no flag-day upgrade. A deployed v0.3.0 node and an upgraded v0.4.0
|
||||
node peer, rekey, and route normally, so you can roll the upgrade out
|
||||
across a mesh in any order.
|
||||
|
||||
## At a glance
|
||||
|
||||
- New outbound Nym mixnet transport with a single-container demo and a
|
||||
new mixnet-relay example.
|
||||
- Opt-in mDNS / DNS-SD discovery on the local link.
|
||||
- Data-plane overhaul: off-task encrypt and decrypt worker pools, GSO,
|
||||
connected-UDP send path, copy-avoidance on receive, batched macOS
|
||||
receive.
|
||||
- The full `show_*` read surface now serves off the receive loop, so
|
||||
`fipsctl` and `fipstop` stay responsive on loaded nodes; a new
|
||||
counter-only `show_metrics` query enables a Prometheus scraper at no
|
||||
hot-path cost.
|
||||
- Reworked `fipstop` TUI on a machine-verified render-snapshot base.
|
||||
- Rekey is now hitless under loss and reordering in both directions.
|
||||
|
||||
## What's new
|
||||
|
||||
### Nym mixnet transport
|
||||
|
||||
FIPS can now peer over the [Nym](https://nymtech.net/) mixnet for
|
||||
metadata-resistant connectivity. The new `transports.nym` transport
|
||||
makes outbound connections through a `nym-socks5-client` SOCKS5 proxy
|
||||
that you run alongside the daemon (for example as a service running
|
||||
alongside the fips daemon, or as a sidecar container). The transport
|
||||
waits at startup for the nym-socks5-client to become ready before giving
|
||||
up.
|
||||
|
||||
This is a privacy and anonymity deployment mode chosen for its own
|
||||
properties. It mixes your FIPS traffic into the Nym cover-traffic
|
||||
network so that link-level observers cannot correlate which mesh peers
|
||||
are talking. A new `examples/sidecar-nostr-mixnet-relay/` demonstrates a
|
||||
FIPS-reachable Nostr relay peered across the mixnet end to end, and a
|
||||
single-container demo ships with the transport.
|
||||
|
||||
Enable it by adding a `transports.nym` instance and pointing it at your
|
||||
running nym-socks5-client. See the transports reference for the field
|
||||
set.
|
||||
|
||||
### mDNS LAN discovery
|
||||
|
||||
Nodes on a shared local link can now find each other with zero address
|
||||
configuration. The opt-in `node.discovery.lan` path runs an mDNS /
|
||||
DNS-SD responder and browser: each node advertises a FIPS service record
|
||||
on the link and adopts the peers it discovers. This complements the
|
||||
existing Nostr-mediated overlay discovery for the common case where the
|
||||
peers are simply on the same LAN.
|
||||
|
||||
Turn it on with `node.discovery.lan.enabled: true`. `service_type` and
|
||||
`scope` tune the advertised service record and which interfaces
|
||||
participate. Discovery on the local link needs no relay and no STUN.
|
||||
|
||||
### Data-plane throughput overhaul
|
||||
|
||||
The receive and send paths were reworked for higher single-node
|
||||
throughput and lower per-packet CPU, building on the v0.3.0
|
||||
crypto-backend swap:
|
||||
|
||||
- **Off-task encrypt and decrypt.** Per-peer encrypt and decrypt now run
|
||||
on dedicated worker tasks rather than inline on the receive loop, so a
|
||||
single busy peer no longer serializes the whole node's crypto.
|
||||
- **GSO and connected-UDP send.** The Linux send path uses generic
|
||||
segmentation offload and a connected-UDP socket where available,
|
||||
cutting syscall overhead on bulk flows.
|
||||
- **Copy-avoidance on receive.** The receive hot path avoids buffer
|
||||
copies it previously made per packet.
|
||||
- **Batched macOS receive.** macOS gains a `recvmsg_x` batched receive,
|
||||
mirroring the Linux `recvmmsg` batching from v0.3.0.
|
||||
- **Shared immutable-state context and an atomic metric registry.**
|
||||
Immutable per-node state moved into a single shared context, and
|
||||
counters live in an atomic metric registry that the new `show_metrics`
|
||||
query reads without touching the hot path.
|
||||
|
||||
These are all internal to the data plane and require no operator action.
|
||||
|
||||
### Observability off the hot path
|
||||
|
||||
Every read-only control query now renders from a snapshot published once
|
||||
per tick into a lock-free `ArcSwap`, served from the control accept task
|
||||
instead of round-tripping the data-plane receive loop. This covers
|
||||
`show_status`, `show_stats_*`, `show_peers`, `show_sessions`,
|
||||
`show_links`, `show_connections`, `show_transports`, `show_mmp`,
|
||||
`show_tree`, `show_bloom`, `show_cache`, `show_routing`,
|
||||
`show_identity_cache`, `show_acl`, `show_listening_sockets`, and the new
|
||||
`show_metrics`. Only the mutating `connect` and `disconnect` commands
|
||||
still reach the loop.
|
||||
|
||||
The practical effect: on a loaded node where the receive loop was busy,
|
||||
`fipsctl` and `fipstop` queries previously stalled or timed out (the
|
||||
five-second query pattern operators saw). They now answer promptly
|
||||
regardless of data-plane load. Per-entity snapshots reuse unchanged rows
|
||||
by pointer, so the per-tick publish cost stays bounded as peer and
|
||||
session counts grow.
|
||||
|
||||
A new **`show_metrics`** query (surfaced as `fipsctl stats metrics`)
|
||||
returns a counter-only snapshot of every metric family. It is the
|
||||
enabler for a Prometheus scraper that pulls node counters at no hot-path
|
||||
cost.
|
||||
|
||||
### Reworked fipstop TUI
|
||||
|
||||
`fipstop` gets a rendering, navigation, and read-surface overhaul on a
|
||||
machine-verified base: a render-snapshot harness asserts the exact text
|
||||
grid and per-cell style of every view against canned control-socket
|
||||
output. New daemon-resolved fields surface through the snapshots,
|
||||
including effective persistence, root and is-root state, a
|
||||
per-transport-type peer-count map, per-peer effective depth, the root
|
||||
npub, and the last-sent uptree filter fill ratio with the subtree size
|
||||
estimate.
|
||||
|
||||
A separate fix clears a garbled-screen problem on startup and stray
|
||||
bytes on quit, most visible over SSH and inside tmux: startup now forces
|
||||
a full repaint before the first draw, and quit stops and joins the
|
||||
stdin-poll thread before restoring the terminal, so post-raw-mode
|
||||
keystrokes no longer echo onto the restored screen.
|
||||
|
||||
### Rekey reliability
|
||||
|
||||
FMP and FSP session rekey are now hitless under packet loss and
|
||||
reordering in both directions:
|
||||
|
||||
- Inbound frames are authenticated against the pending session before
|
||||
the K-bit cutover promotes it, so a spoofed or stale frame cannot
|
||||
derail a rekey in progress.
|
||||
- Rekey message-1 retransmission is bounded, and the link-dead heartbeat
|
||||
is rekey-aware so an in-flight rekey is not mistaken for a dead link.
|
||||
- FSP session rekey holds connectivity across the rekey window under
|
||||
loss and reordering.
|
||||
- Dual-initiation races (both peers starting a rekey at once on a
|
||||
high-latency link) are desynchronized with symmetric jitter so the two
|
||||
sides converge on one session rather than fighting.
|
||||
- An exhausted retransmission-budget abort, an expected and self-limiting
|
||||
outcome on lossy or high-latency links, is logged at debug rather than
|
||||
warn.
|
||||
|
||||
The net operator takeaway: rekey completes cleanly without dropping
|
||||
traffic, even on lossy or high-latency links, and the log no longer
|
||||
cries wolf when a rekey gives up and retries.
|
||||
|
||||
## Behavior changes worth flagging
|
||||
|
||||
These affect operators on upgrade.
|
||||
|
||||
- **Bloom filter antipoison cap raised.** `node.bloom.max_inbound_fpr`
|
||||
moves from 0.05 to 0.10, accepting filters with a higher derived
|
||||
false-positive rate before rejecting them. This reduces spurious
|
||||
filter rejections on larger meshes while keeping the antipoison
|
||||
protection in place.
|
||||
- **TCP inbound cap honors `max_connections`.** The TCP inbound accept
|
||||
ceiling now resolves from explicit per-transport
|
||||
`max_inbound_connections`, then node-wide
|
||||
`node.limits.max_connections`, then the built-in default of 256.
|
||||
Previously the TCP inbound ceiling was hardwired to 256 and ignored
|
||||
`max_connections`, so raising it had no effect on inbound TCP.
|
||||
- **Static host aliases hot-reload.** `/etc/fips/hosts` now reloads on
|
||||
mtime change once per tick rather than only at startup, so display
|
||||
names in `fipsctl` and `fipstop` reflect edits without a daemon
|
||||
restart. The peer ACL reloads through the same lock-free snapshot
|
||||
mechanism.
|
||||
- **Quieter logs on busy public-mesh nodes.** Routine per-peer
|
||||
connection-lifecycle and capacity-cap events, no-route session-datagram
|
||||
drops, and exhausted rekey-budget aborts are demoted to debug, so
|
||||
genuinely notable info and warn lines are no longer drowned out.
|
||||
- **More visible drops.** Receive-path silent rejections now flow
|
||||
through typed reject-reason counters, and discovery counts requests
|
||||
dropped when the dedup cache is full (`req_dedup_cache_full`, visible
|
||||
via `show_routing`). Drops that were previously silent are now
|
||||
countable.
|
||||
- **Tor connect-refused accounting.** The Tor transport increments its
|
||||
`connect_refused` statistic (the "Refused" line in `fipstop`) on an
|
||||
actively-refused SOCKS5 connect, instead of recording every connect
|
||||
failure as a generic SOCKS5 error.
|
||||
|
||||
## Notable bug fixes
|
||||
|
||||
The CHANGELOG has the exhaustive list. This is the operator-relevant
|
||||
subset of fixes for behavior that shipped in v0.3.0.
|
||||
|
||||
- **Symmetric peer teardown on manual disconnect.** A manual
|
||||
`fipsctl disconnect` now sends the peer a scoped Disconnect so both
|
||||
ends tear down and re-handshake cleanly. Previously a manual
|
||||
disconnect tore down only the local side, leaving the peer with a
|
||||
stale session that was never re-adopted as a child and whose bloom
|
||||
filter was never re-recorded.
|
||||
- **Gateway holds long-lived and DNS-cached mappings.** `fips-gateway`
|
||||
no longer drops a virtual-IP mapping while traffic is still flowing.
|
||||
The mapping TTL clock previously advanced only on DNS re-query, so a
|
||||
busy long-lived or DNS-cached client could have its mapping reclaimed
|
||||
mid-flow. The tick now refreshes the mapping whenever conntrack reports
|
||||
active sessions and recovers a draining mapping to active when traffic
|
||||
resumes; only genuinely idle mappings drain.
|
||||
- **Accurate mesh-size estimate under filter overlap.** The mesh-size
|
||||
estimator now estimates the cardinality of the OR-union of self plus
|
||||
every connected peer's inbound filter, instead of summing per-filter
|
||||
cardinalities of tree peers. Summing assumed the filters were disjoint,
|
||||
so a stale or oversized parent filter or a routing loop inflated the
|
||||
reported mesh size and a tree rebalance flapped the count. OR-union
|
||||
deduplicates overlap, equals the old result in the disjoint case, and
|
||||
removes the estimate's dependence on tree-declaration cache freshness.
|
||||
- **Single-uplink node reattaches within a round-trip.** A node with one
|
||||
tree peer, which has periodic parent re-evaluation disabled, was left
|
||||
self-rooted and unreachable if its one-shot attaching TreeAnnounce was
|
||||
lost, until the next periodic re-broadcast. Tree-position exchange is
|
||||
now self-healing on the receive path: a node that hears an announce
|
||||
advertising a strictly worse root echoes its own declaration back,
|
||||
provoking the better-rooted peer to re-push its real position
|
||||
immediately.
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
Operator-actionable items moving from v0.3.0 to v0.4.0:
|
||||
|
||||
- **Wire-compatible, no flag day.** v0.4.0 peers with v0.3.0. Upgrade
|
||||
nodes in any order. During a rolling upgrade you may see some log lines
|
||||
on the upgraded side as it interacts with not-yet-upgraded peers;
|
||||
behavior is correct, log noise only.
|
||||
- **Bloom antipoison cap default changed.** `node.bloom.max_inbound_fpr`
|
||||
now defaults to 0.10 (was 0.05). If you set this explicitly, review
|
||||
whether you still want the old value.
|
||||
- **New optional config surfaces.** `transports.nym` (outbound Nym
|
||||
mixnet) and `node.discovery.lan` (mDNS LAN discovery) are both opt-in
|
||||
and off by default. Adding them is the only way to turn the new paths
|
||||
on.
|
||||
- **TCP inbound cap.** If you relied on the old hardwired 256 inbound-TCP
|
||||
ceiling, note it now honors `max_inbound_connections` then
|
||||
`node.limits.max_connections` then 256.
|
||||
- **New observability query.** `fipsctl stats metrics` (the
|
||||
`show_metrics` control query) returns a counter-only snapshot suitable
|
||||
for a scraper.
|
||||
|
||||
## Getting v0.4.0
|
||||
|
||||
- **Linux x86_64 / aarch64**: `.deb` and tarball at the
|
||||
[v0.4.0 release page](https://github.com/jmcorgan/fips/releases/tag/v0.4.0).
|
||||
- **Arch Linux**: `fips` from the AUR.
|
||||
- **macOS**: `.pkg` at the v0.4.0 release page.
|
||||
- **Windows**: ZIP at the v0.4.0 release page.
|
||||
- **OpenWrt**: `.ipk` at the v0.4.0 release page.
|
||||
- **From source**: `cargo build --release` from a checkout of the v0.4.0
|
||||
tag (Rust 1.94.1 per `rust-toolchain.toml`; `libclang-dev` is a
|
||||
required Linux build prerequisite).
|
||||
|
||||
The full per-commit changelog lives in
|
||||
[`CHANGELOG.md`](../../CHANGELOG.md). Issues and discussion at
|
||||
[github.com/jmcorgan/fips](https://github.com/jmcorgan/fips).
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed code, packaging work, bug reports, or
|
||||
reviews to this release.
|
||||
|
||||
- [@jcorgan](https://github.com/jmcorgan): release shepherd, high-level
|
||||
design, control read plane, rekey hardening, admission, bug fixes,
|
||||
testing, packaging, PR coordination, and issue resolution.
|
||||
- [@mmalmi](https://github.com/mmalmi): opt-in mDNS LAN discovery and
|
||||
data-plane performance work.
|
||||
- [@Origami74](https://github.com/Origami74): macOS packaging and
|
||||
website coordination.
|
||||
- [@dskvr](https://github.com/dskvr): AUR packaging.
|
||||
- [@oleksky](https://github.com/oleksky): Nym mixnet transport and the
|
||||
single-container mixnet demo.
|
||||
@@ -98,11 +98,10 @@ is what you want.
|
||||
Or via the daemon:
|
||||
|
||||
```sh
|
||||
sudo fipsctl show identities
|
||||
sudo fipsctl show status
|
||||
```
|
||||
|
||||
The first JSON entry has `local: true` and a `ula` field — that
|
||||
is your address.
|
||||
The JSON has an `ipv6_addr` field — that is your address.
|
||||
|
||||
For the rest of this tutorial we will write the address as
|
||||
`<your-fips0-addr>`. Substitute the actual `fd97:...` value
|
||||
|
||||
@@ -63,9 +63,11 @@ mesh address:
|
||||
dig npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98.fips AAAA +short
|
||||
```
|
||||
|
||||
You should see one AAAA record returning a `fd97:...` address.
|
||||
The prefix is the FIPS ULA range (`fd00::/8`, with `fd97:...`
|
||||
covering the address space derived from npubs).
|
||||
You should see one AAAA record returning an address such as
|
||||
`fd97:...`. The prefix is the FIPS ULA range (`fd00::/8`): only
|
||||
the leading `fd` byte is fixed, and everything after it is hash
|
||||
output derived from the npub, so the digits beyond `fd` vary per
|
||||
node.
|
||||
|
||||
The query went through `systemd-resolved` (or your platform
|
||||
equivalent), which routed `.fips` queries to the daemon's local
|
||||
|
||||
@@ -56,8 +56,11 @@ hostname on the public internet. There is no separate
|
||||
the tool takes a hostname, it accepts a `.fips` hostname.
|
||||
|
||||
> **Where the address comes from.** Every FIPS node's mesh
|
||||
> address is the SHA-256 of its public key, truncated to the
|
||||
> bottom 64 bits and prepended with `fd97:`. Names of the form
|
||||
> address is the first 16 bytes of SHA-256 of its public key,
|
||||
> with the leading byte replaced by `0xfd` (the `fd00::/8` ULA
|
||||
> prefix). The remaining bytes are hash output, so an address
|
||||
> like `fd97:...` is per-node — the `97` is part of the hash,
|
||||
> not a fixed prefix shared across nodes. Names of the form
|
||||
> `<npub>.fips` and any shortname mapped in `/etc/fips/hosts`
|
||||
> are aliases for that address. The daemon's local DNS
|
||||
> responder hands the answer back to your kernel without ever
|
||||
|
||||
Reference in New Issue
Block a user