diff --git a/docs/design/fips-nostr-discovery.md b/docs/design/fips-nostr-discovery.md index e4553fd..df65088 100644 --- a/docs/design/fips-nostr-discovery.md +++ b/docs/design/fips-nostr-discovery.md @@ -15,8 +15,9 @@ every supported platform and ships in every stock packaging artifact `.zip`). It is runtime-opt-in: the YAML configuration defaults to disabled (`node.discovery.nostr.enabled: false`), so the discovery runtime stays dormant — and opens no relay connections — until an -operator flips the flag and supplies a relay list. When disabled, nodes -behave exactly as before: only the static `peers[]` addresses are used. +operator flips the flag. Default relay and STUN-server lists ship in +the config; both are optional overrides. When disabled, nodes behave +exactly as before: only the static `peers[]` addresses are used. ## Role diff --git a/docs/getting-started.md b/docs/getting-started.md index 3ac2e50..f7e4152 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -29,9 +29,10 @@ running nodes can mesh. - A Linux, macOS, or Windows host. Linux is the most exercised platform; macOS and Windows installers are available. -- The pre-built installer for your platform (see the - [Releases page](../README.md#installation)), **or** a source - checkout if you want to build the installer yourself. +- The pre-built installer for your platform (see the project + README's [Quick start](../README.md#quick-start) section for + download links), **or** a source checkout if you want to build + the installer yourself. - For the source-build path only: a working Rust toolchain (the version pinned in `rust-toolchain.toml` is auto-installed by rustup), and the platform-specific build dependencies listed in @@ -63,7 +64,7 @@ per-platform installer: - Windows — `.zip` with service-install scripts - Generic systemd Linux — `.tar.gz` with an `install.sh` script -See the [project README's Installation section](../README.md#installation) +See the [project README's Quick start section](../README.md#quick-start) for download links and per-platform invocations. ### From source diff --git a/docs/how-to/enable-nostr-discovery.md b/docs/how-to/enable-nostr-discovery.md index 1ad5deb..2e91117 100644 --- a/docs/how-to/enable-nostr-discovery.md +++ b/docs/how-to/enable-nostr-discovery.md @@ -4,7 +4,8 @@ Nostr-mediated discovery lets FIPS nodes find each other (and punch through UDP NAT) using public Nostr relays as the signaling channel. The feature ships in every stock packaging artifact but is **off by default** — it activates when an operator sets -`node.discovery.nostr.enabled: true` and provides a relay list. See +`node.discovery.nostr.enabled: true`. Default relay and STUN-server +lists ship in the config; both are optional overrides. See [../design/fips-nostr-discovery.md](../design/fips-nostr-discovery.md) for the design and rationale; see [../reference/configuration.md](../reference/configuration.md) for the @@ -126,15 +127,20 @@ transports: udp: bind_addr: "0.0.0.0:2121" advertise_on_nostr: true - external_addr: "203.0.113.45:2121" # ← explicit + public: true # ← required, master switch + external_addr: "203.0.113.45:2121" # ← explicit address ``` `external_addr` accepts a bare IP (combined with the bind port) or a -full `host:port`. Setting both `public: true` and `external_addr` -together is allowed — the explicit override wins, with STUN as a -logging cross-check. If UDP is bound directly to a public IP rather -than to a wildcard, neither flag is needed; the daemon advertises -the bound endpoint. +full `host:port`. `public: true` is the master switch that gates UDP +advertisement; inside that branch, the daemon picks the advertised +address in precedence order: explicit `external_addr` (no STUN +observation), a non-wildcard `bind_addr`, or STUN auto-discovery. +Setting `external_addr` alongside `public: true` skips STUN entirely +— there is no logging cross-check. If UDP is bound directly to a +public IP rather than to a wildcard, neither `external_addr` nor STUN +is needed — but `advertise_on_nostr: true` and `public: true` are +still both required for the daemon to publish the endpoint. What this achieves: the node publishes a single `udp::2121` endpoint to the three default advert relays @@ -202,9 +208,17 @@ proceeds normally. > a `udp:nat` advert without signaling relays or STUN servers is > unreachable by construction. -Works best with full-cone NAT on at least one side. Symmetric NAT on -both sides is not reliably traversable with this protocol and will -time out after `punch_duration_ms`. +Hole-punching is best-effort. It works reliably when both sides are +full-cone or port-restricted NATs. Symmetric NAT on either side +typically defeats the punch — the public port a peer sees varies per +remote endpoint, so the address learned via STUN does not match the +mapping the peer actually needs. The punch attempt times out after +`punch_duration_ms`. `udp:nat` is the only NAT-traversal mechanism +in FIPS; when it can't succeed, there's no in-protocol substitute. +Being reachable then becomes a deployment-prerequisite question +rather than a transport question — a publicly reachable port (UDP +or TCP — both require the same kind of network resource) published +as a direct advert per Sub-scenario 2a or 2b. ### Sub-scenario 2b: TCP diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 761717d..a8fd3ed 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -59,7 +59,7 @@ peers: # Static peer 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 +management. See the [`fipsctl` reference](cli-fipsctl.md) for the command list. On Linux, the control socket is a Unix domain socket with filesystem diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 25d136f..8487982 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -25,7 +25,7 @@ cover every option. | 1 | [join-the-test-mesh.md](join-the-test-mesh.md) | Add one public test peer to your config, watch the link come up, ping that peer and a second mesh node it routes you to. The starting point for everything else. | | 2 | [persistent-identity.md](persistent-identity.md) | Pin your daemon to a stable Nostr keypair so your address stops changing on every restart. Other operators can now add you to their `peers:` lists; the services you host get a fixed name. | | 3 | [resolve-peers-via-nostr.md](resolve-peers-via-nostr.md) | Stop hard-coding peer addresses. Drop the address line from your peer entry and let the daemon look up the current endpoint from public Nostr relays at dial time. | -| 4 | [advertise-your-node.md](advertise-your-node.md) | Publish your own UDP endpoint to Nostr so any operator who knows your npub can reach you, with a short final section on `udp:nat` traversal for symmetric-NAT networks. | +| 4 | [advertise-your-node.md](advertise-your-node.md) | Publish your own UDP endpoint to Nostr so any operator who knows your npub can reach you, with a short final section on `udp:nat` best-effort hole-punching for nodes without a directly reachable UDP endpoint. | | 5 | [open-discovery.md](open-discovery.md) | Switch to `policy: open` and let your peer list populate itself from the ambient `fips-overlay-v1` namespace. Hands-off mesh participation. | | 6 | [reach-mesh-services.md](reach-mesh-services.md) | Drive ordinary IPv6 tools — `ping6`, `nc`, `traceroute6`, `curl`, `ssh` — at mesh nodes by `.fips`. Get a feel for the daemon's IPv6 adapter, which makes unmodified IPv6 software work over the mesh. | | 7 | [host-a-service.md](host-a-service.md) | Bring up an HTTP server bound to `fips0` so mesh nodes can reach it, with a deliberate exposure decision (mesh-only vs every interface), and the mesh firewall as a default-deny baseline. The peer ACL (a separate, transport-layer control over which npubs may peer with your node) is briefly mentioned alongside. | diff --git a/docs/tutorials/advertise-your-node.md b/docs/tutorials/advertise-your-node.md index abc8188..cb25f6f 100644 --- a/docs/tutorials/advertise-your-node.md +++ b/docs/tutorials/advertise-your-node.md @@ -8,8 +8,9 @@ your own endpoint(s), so any other operator who knows your npub can dial you the same way you dialed `test-us01`. The whole exercise should take about ten minutes if you have -a public IP or full-cone home NAT. A short final section -covers the alternative path for symmetric-NAT networks. +a public IP or a UDP listener that's reachable from outside. +A short final section covers `udp:nat` best-effort hole-punching +for the cases where direct UDP advertising isn't an option. ## What you'll build @@ -74,9 +75,13 @@ port to put in the advert: > - `public: true` — daemon does a one-shot STUN observation > against the configured STUN servers and uses the reflexive > IPv4 it learns. Right when your public IP is dynamic or -> you'd rather not pin it in config. Works for nodes with a -> directly-bound public IP and for nodes behind full-cone -> NAT (most home routers). +> you'd rather not pin it in config. Note: STUN observes the +> reflexive IP from an ephemeral socket, then pairs it with +> the listener's bind port for the advert — the advert is +> only useful if your listener really is reachable at that +> public IP/port, which the daemon can't tell from STUN +> alone. A manual probe from a second host is the only sure +> check. > - `external_addr: "[:]"` — explicit override. > Right when you already know your public IP — a static > residential IP, an Elastic IP behind 1:1 NAT, a cloud @@ -86,8 +91,9 @@ port to put in the advert: > to the public IP returns `EADDRNOTAVAIL`. > > If you bind UDP to a specific public IP rather than -> `0.0.0.0`, neither flag is needed — the daemon advertises -> whatever it's bound to. +> `0.0.0.0`, neither STUN nor `external_addr` is needed — but +> `advertise_on_nostr: true` and `public: true` are still both +> required for the daemon to publish the endpoint. Adverts don't sit on the relays forever: @@ -167,14 +173,17 @@ transports: udp: bind_addr: "0.0.0.0:2121" advertise_on_nostr: true + public: true external_addr: "203.0.113.45:2121" ``` Replace `203.0.113.45:2121` with your actual public IP and port. The bare-IP form `external_addr: "203.0.113.45"` is also -accepted; the daemon combines it with the bind port. You may -set both `public: true` and `external_addr` together — the -explicit override wins, with STUN as a logging cross-check. +accepted; the daemon combines it with the bind port. `public: +true` is still required as the master switch that gates UDP +advertisement; setting `external_addr` alongside it wins, and +STUN auto-discovery is skipped entirely (no logging +cross-check). `advertise_on_nostr: true` is the bit that says "include this transport in my published advert" — common to both paths. @@ -191,8 +200,11 @@ sudo systemctl status fips Status should show `active (running)`. Within a few seconds the daemon will: -1. Run a one-shot STUN observation against the default STUN - servers to learn its public IP. +1. Determine the address to advertise. If you set `external_addr`, + the daemon uses it directly and skips STUN. If you set only + `public: true`, the daemon runs a one-shot STUN observation + against the default STUN servers and uses the reflexive IPv4 it + learns. 2. Build a Kind 37195 advert listing `udp::2121` (and any other transports you have `advertise_on_nostr: true` on). @@ -200,12 +212,13 @@ daemon will: 4. Publish it to the three default advert relays. 5. Schedule a refresh every 30 minutes. -If STUN fails (for example, if the network blocks outbound -UDP/3478), the daemon emits a WARN line in the journal and -suppresses the UDP entry from the advert rather than publishing -a wrong address. The link to `test-us01` from the previous -tutorial keeps working regardless — only the publish side is -gated on STUN. +If you took the `public: true` path and STUN fails (for example, +the network blocks outbound UDP/3478), the daemon emits a WARN +line in the journal and suppresses the UDP entry from the advert +rather than publishing a wrong address. The link to `test-us01` +from the previous tutorial keeps working regardless — only the +publish side is gated on STUN, and only on the STUN path. The +`external_addr` path doesn't depend on STUN reachability at all. Quick sanity check on the journal: @@ -295,22 +308,24 @@ verifiable in Step 4. to them specifically or not. The test mesh's open-discovery nodes will pick you up automatically. -## If you're behind symmetric NAT +## If your direct UDP advert isn't reachable -`public: true` + STUN works on most home and office NATs (the -full-cone variety) and on nodes with a directly-bound public -IP. It does *not* work on symmetric NAT, where the NAT mapping -is keyed on (source-port, destination-host) so the IP/port -your STUN server saw isn't the IP/port a different peer would -see. +`public: true` advertises the IP STUN observes paired with your +listener's bind port. That advert is only useful if your listener +really is reachable at that public IP/port — STUN can confirm the +public IP but not that an unsolicited inbound packet to the bind +port will make it through. The most common cause of the listener +being unreachable is symmetric NAT (where the public port a peer +sees varies per remote endpoint), but other configurations can +have the same effect. -For symmetric-NAT networks the alternative is `udp:nat` mode, -which advertises a placeholder `udp:nat` endpoint along with -the daemon's signaling-relay and STUN-server lists, and -performs UDP hole-punching at dial time. Both sides need to be -running matching configs and at least one side needs a -non-symmetric NAT for the punch to succeed; symmetric on both -sides is not reliably traversable and will time out. +When direct UDP advertising can't be relied on, the alternative +is `udp:nat` mode, which advertises a placeholder `udp:nat` +endpoint along with the daemon's signaling-relay and STUN-server +lists, and performs UDP hole-punching at dial time. Hole-punching +is best-effort — it works reliably when both sides are full-cone +or port-restricted, and symmetric NAT on either side typically +defeats it. Both sides need matching configs. The minimal config switch: @@ -339,7 +354,7 @@ discovery: For the full setup including peer-side config and the punch- duration knob, see -[../how-to/enable-nostr-discovery.md § Capability 2c](../how-to/enable-nostr-discovery.md#sub-scenario-2c-udp-hole-punching-for-nodes-behind-nat). +[../how-to/enable-nostr-discovery.md § When the node is behind NAT](../how-to/enable-nostr-discovery.md#when-the-node-is-behind-nat). Separately from NAT considerations, FIPS supports running a node behind a Tor onion service as a deployment shape in its @@ -347,7 +362,7 @@ own right — chosen for the privacy, anonymity, and censorship-resistance properties it brings, not as a fallback when UDP or TCP fail. If those properties are an independent goal for your node, see -[../how-to/enable-nostr-discovery.md § Sub-scenario 2b](../how-to/enable-nostr-discovery.md#sub-scenario-2b-tor-onion-node) +[../how-to/enable-nostr-discovery.md § Tor onion node](../how-to/enable-nostr-discovery.md#tor-onion-node) and [../how-to/deploy-tor-onion.md](../how-to/deploy-tor-onion.md). @@ -364,10 +379,12 @@ If your advert doesn't appear on the relays: with a non-public address (e.g., `10.x.x.x` or `192.168.x.x`), STUN didn't see your real public IP — likely you're behind a CGNAT that NATs your STUN traffic too, or a - corporate firewall that proxies it. Switch to the - `external_addr` form from Step 2 with your actual public - IP, or replace `public: true` with the bound interface IP - directly under `bind_addr`. + corporate firewall that proxies it. Two correct fixes: + (a) keep `public: true` and add `external_addr: ` + (the explicit override wins and skips STUN); or (b) bind + directly to your public interface + (`bind_addr: :2121`) and keep `advertise_on_nostr: + true` and `public: true`. Don't drop those flags. - **Relay reachability.** `nak req` against a relay you can reach but no events return — possibly the publish failed @@ -379,9 +396,14 @@ If your advert doesn't appear on the relays: nak req ... wss://offchain.pub ``` -- **`advertise_on_nostr` typo.** YAML is case-sensitive and - silently ignores unknown keys. If `nak` returns no advert at - all, double-check the spelling on the UDP block and that +- **`advertise_on_nostr` typo.** YAML is case-sensitive. The + config parser rejects unknown keys via + `serde(deny_unknown_fields)` on the per-section structs, so a + misspelled field will refuse the daemon's start with a + parse-error line in the journal naming the unknown field. + If the daemon is running but `nak` returns no advert, the + field was accepted but something else is wrong; double-check + the spelling on the UDP block and that `discovery.nostr.advertise: true` is also set. ## What's next diff --git a/docs/tutorials/open-discovery.md b/docs/tutorials/open-discovery.md index e17a63d..a6d1f9a 100644 --- a/docs/tutorials/open-discovery.md +++ b/docs/tutorials/open-discovery.md @@ -110,8 +110,12 @@ You should be coming out of [advertise-your-node](advertise-your-node.md) with: - Persistent identity, advertising enabled - (`discovery.nostr.advertise: true`), UDP advertising on - Nostr (`transports.udp.advertise_on_nostr: true`). + (`discovery.nostr.advertise: true`), and either the + direct-UDP path + (`transports.udp.advertise_on_nostr: true`, + `transports.udp.public: true`) or the `udp:nat` path + (`transports.udp.advertise_on_nostr: true`, + `transports.udp.public: false`) from the previous tutorial. - A static `test-us01` peer entry that the daemon dials outbound; possibly an inbound `test-us03` peer (the open-discovery test mesh node that dialed in after seeing @@ -288,11 +292,12 @@ the previous tutorial: WebSocket connection to the relays is failing repeatedly, no adverts arrive. Look for relay-connection errors in `sudo journalctl -u fips -n 200`. -- **`policy: open` typo.** YAML accepts and ignores unknown - values silently. If `fipsctl show status` (or the daemon's - startup log) shows `policy: configured_only`, the YAML - didn't parse the new value — re-check spelling and - indentation. +- **`policy: open` typo.** YAML is case-sensitive, and the + `policy` field is a serde enum that rejects unknown values — + a misspelled value produces a config-parse error at startup + rather than a silent fall-back. If the daemon refuses to + start, check `sudo journalctl -u fips -n 200` for the + parse-error line naming the field and value. If too many peers are appearing and you want to dial down: diff --git a/docs/tutorials/resolve-peers-via-nostr.md b/docs/tutorials/resolve-peers-via-nostr.md index 3b887aa..284088d 100644 --- a/docs/tutorials/resolve-peers-via-nostr.md +++ b/docs/tutorials/resolve-peers-via-nostr.md @@ -125,9 +125,10 @@ Two knobs, one job each: `wss://offchain.pub`) and is now able to query and consume adverts. - `advertise: false` keeps the publish side off. Your daemon - will not publish an advert of its own at this stage. (This - is the default, but it is good practice to make the choice - explicit while you're learning.) + will not publish an advert of its own at this stage. The + default is `true`, so we are setting it explicitly to + disable advertising for this consume-only tutorial. The next + tutorial flips it back on. ## Step 3: Switch the peer entry to `via_nostr` @@ -270,8 +271,9 @@ If the link does not come up: [advertise-your-node](advertise-your-node.md) publishes your daemon's UDP endpoint on Nostr so other operators can add you to their `peers:` list with `via_nostr: true` and reach - you the way you just reached `test-us01`. Includes a section - on `udp:nat` NAT traversal for symmetric-NAT networks. + you the way you just reached `test-us01`. Includes a short + final section on `udp:nat`, the best-effort hole-punching + path for nodes without a directly reachable UDP endpoint. - **Discover peers with no prior configuration.** [open-discovery](open-discovery.md) switches your daemon to