Files
fips/docs/tutorials/open-discovery.md
Johnathan Corgan 77fdd52fe0 docs: reviewer feedback pass on Nostr-discovery surface
Walk through reviewer feedback on the Nostr-discovery docs and
land 18 items.

Bulk patterns:

- `external_addr` / `public: true` semantics consistently
  misdescribed. The advert path is gated on `cfg.is_public()`;
  inside that branch the daemon picks an address by precedence
  (`external_addr`, non-wildcard `bind_addr`, STUN). The docs
  treated `public: true` and `external_addr` as alternatives when
  they are stacked: `public: true` is the master switch and
  `external_addr` populates the address inside it. Reconciled
  across `enable-nostr-discovery.md` and `advertise-your-node.md`:
  add `public: true` to the `external_addr` examples; replace
  "STUN as a logging cross-check" with "STUN is skipped entirely";
  fix "neither flag is needed" for direct public bind (both flags
  still required); make the publish-tutorial Step 3 conditional
  on the chosen Step 2 path (STUN runs only on the `public: true`
  path); rewrite the troubleshooting "wrong public IP advertised"
  bullet with two coherent fixes.

- `udp:nat` overpromised as a symmetric-NAT solution. Symmetric
  NAT on either side typically defeats the punch. Reframe
  `udp:nat` as best-effort hole-punching for nodes without a
  directly reachable UDP endpoint in the how-to, the publish
  tutorial (intro, callout, section heading rewrite from "If
  you're behind symmetric NAT" to "If your direct UDP advert
  isn't reachable"), the consume tutorial's "What's next"
  pointer, and `tutorials/README.md`. Promote reachability over
  named NAT classes: STUN can confirm the public IP but not that
  the listener-port mapping is open.

- YAML "silently ignores unknown keys" is wrong. Config parser
  rejects unknown fields via `serde(deny_unknown_fields)` on the
  per-section structs; misspelled fields refuse the daemon's
  start with a parse-error line in the journal. Fixed in the
  publish tutorial's troubleshooting and the open-discovery
  tutorial's `policy` typo bullet.

Mechanical fixes:

- Repoint stale anchors. `getting-started.md` and
  `configuration.md` linked to `#installation` / `#inspect` on
  the README; the README has no such headings. Repoint to
  `#quick-start` and `cli-fipsctl.md`. Two stale anchors in the
  publish tutorial pointing at non-existent sub-scenarios in the
  how-to (`#sub-scenario-2c-...`,
  `#sub-scenario-2b-tor-onion-node`) repointed to the correct
  anchors.

- Drop the `fipsctl show status` claim from the open-discovery
  troubleshooting bullet (`show_status` doesn't include
  `discovery.nostr.policy`). Replace with daemon startup logs.

- Fix the `advertise: false` parenthetical in the consume-only
  tutorial (`default_advertise()` returns `true`; we set `false`
  explicitly for the consume-only path).

- Drop the "supplies a relay list" overstatement in two
  activation paragraphs (the how-to and the design doc). Default
  relay / STUN-server lists ship in the config; both are
  optional overrides.

- Add the missing `transports.udp.public` entry to the
  open-discovery tutorial's prerequisites checklist. Tutorial
  users coming out of advertise-your-node could be on either the
  direct-UDP (`public: true`) or `udp:nat` (`public: false`)
  path; list both.

Files: docs/getting-started.md, docs/reference/configuration.md,
docs/how-to/enable-nostr-discovery.md, docs/tutorials/README.md,
docs/tutorials/advertise-your-node.md,
docs/tutorials/resolve-peers-via-nostr.md,
docs/tutorials/open-discovery.md,
docs/design/fips-nostr-discovery.md.
2026-05-10 21:56:09 +00:00

13 KiB

Open Discovery: Find Peers Without Configuration

After advertise-your-node, your daemon publishes its endpoint on Nostr and other open-discovery nodes on the test mesh have already started dialing you. This tutorial flips the symmetry: turn your own daemon into a consumer of every advert in the namespace, so any operator who's publishing becomes a candidate peer of yours.

The whole exercise should take about ten minutes. After it, you'll be a full participant in the ambient fips-overlay-v1 namespace — publishing your own advert and discovering everyone else's.

What you'll build

   ┌─────────────────────────────────────────────────────┐
   │  Nostr relays — fips-overlay-v1 namespace           │
   │   adverts from: test-us01..uk01, others             │
   └────────────────┬─────────────────────────▲──────────┘
                    │                         │
                    │ subscribe to all         │ your own
                    │ adverts in namespace     │ advert
                    │                         │
                    ▼                         │
   ┌────────────────────────────────────────────────────┐
   │  your fips daemon (policy: open)                   │
   │                                                    │
   │  peers list grows ambient as adverts arrive:       │
   │     test-us01     ← was static                     │
   │     test-us03     ← inbound (was already there)    │
   │     test-de01, test-es01, test-uk01, test-us04...  │
   │     plus any other publisher in the namespace      │
   └────────────────────────────────────────────────────┘

You will change one thing in /etc/fips/fips.yaml: under discovery.nostr, set policy: open (the default is configured_only). After restart, the daemon subscribes to every Kind 37195 advert in the fips-overlay-v1 namespace and queues the publishers for outbound connection attempts.

How open discovery works

Discovery policy. discovery.nostr.policy decides what the daemon does with incoming advert data. Two values:

  • configured_only (the default): the daemon only consumes adverts for peers it has explicitly listed with via_nostr: true. This is what you've been running through the previous two Nostr tutorials.
  • open: the daemon subscribes to every advert in the configured app namespace. Any publisher becomes a candidate peer, no peers: list entry needed.

Switching the policy doesn't disturb anything that was already working:

Open is additive, not exclusive. Switching to policy: open doesn't replace your static peers — both mechanisms run in parallel. Configured peers stay in your peers: block and continue to be dialed via their static addresses or via_nostr lookups; open-discovered peers stack on top from the ambient namespace. You can run open discovery with a populated peers: list (the path this tutorial walks, since you're keeping test-us01), with peers: [] for pure ambient discovery, or with a long peers: list and open layered on top to broaden reach.

The namespace is what scopes who's visible to whom:

The namespace is the scope. discovery.nostr.app defaults to fips-overlay-v1 — the namespace the public test mesh uses. Setting a different value (e.g., app: "my-experiment.v1") carves out a private discovery set: only nodes that share your app value find each other. For this tutorial we stay on the default and join the public namespace.

Open discovery is best-effort by design — not every discovered peer will connect:

Best-effort, by design. Many discovered peers will fail to connect — they may be offline, behind incompatible NAT, running a different protocol version, or have peer ACLs that reject you. That's normal for ambient discovery; connection attempts are best-effort and rate-limited by open_discovery_max_pending (default 64). Your peer list grows over time as candidates land in the cache, not all at once on restart.

The peer ACL is the admission-control surface, separate from discovery:

Open is admission-free under your peer ACL. Open discovery does not bypass the peer ACL — every candidate still has to pass it. By default the ACL accepts everyone, so any publisher in the namespace becomes a connection candidate. If you rely on a non-default ACL for admission control, verify it is set the way you want before enabling policy: open. See ../reference/security.md for the ACL format.

Step 1: Confirm your starting state

You should be coming out of advertise-your-node with:

  • Persistent identity, advertising enabled (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 your advert).

Capture the current peer count for comparison:

sudo fipsctl show peers | grep -c 'npub'

You'll likely see 1 (just test-us01) up to a handful, depending on how many open-discovery test mesh nodes have already dialed you.

Step 2: Switch the discovery policy to open

Open /etc/fips/fips.yaml and find the discovery.nostr block. Add (or change) the policy line:

node:
  identity:
    persistent: true
  discovery:
    nostr:
      enabled: true
      advertise: true
      policy: open

That's the only change. Notes on what you don't have to touch:

  • You don't have to drop the peers: block. Static peers and open-discovered peers coexist; static entries get priority for direct dialing, open-discovered ones are layered on top.
  • You don't have to set app. The default fips-overlay-v1 is the namespace the public test mesh uses; staying with the default is what gets you discovered.
  • You don't have to set open_discovery_max_pending. The default of 64 is plenty for a tutorial; only tune it if you see the daemon log open-discovery: max-pending reached, deferring.

Save the file.

Step 3: Restart and let discovery populate

sudo systemctl restart fips

Give the daemon a minute or two. Open discovery doesn't fire all at once — the daemon subscribes to the relays, accumulates adverts as they arrive (or as relays return historical events), and queues each publisher for a dial attempt.

Step 4: Inspect the discovered peer list

sudo fipsctl show peers

You should see considerably more entries than before:

  • test-us01 — still there, still using the configured static dial path.
  • test-us03 — same as before (the open-discovery test mesh node that dials you when it sees your advert).
  • test-us04, test-de01, test-es01, test-uk01 — the other test mesh nodes; your daemon picked their adverts up from the namespace and dialed them.
  • Plus any other operator publishing on fips-overlay-v1 (community nodes, other operators' experiments).

Each entry has its own connectivity state. Some will be active (handshake completed). Some will show as connecting and may transition to failed shortly after — that's normal; the publisher might be offline, the advert might be stale, or NAT traversal failed for that pair.

To get a list of just the active links:

sudo fipsctl show peers | jq '.peers[] | select(.connectivity == "active") | .npub'

The peer count will continue to drift over time as adverts expire and new ones arrive. This is steady-state behavior, not a transient.

Step 5: Confirm the mesh-wide reach

You can now reach any of the discovered nodes the same way you reached test-us01 and test-us02 in join-the-test-mesh. Pick one of the new test mesh nodes from the peer list and ping it by its shortname:

ping6 -c 4 test-uk01.fips

(test-uk01 is the United Kingdom test node; the installer's /etc/fips/hosts entry resolves it to the corresponding npub. Substitute any active peer's shortname or full <npub>.fips.)

Expect four replies. The packet path may go through your direct link to that peer (if the open-discovery dial succeeded and the link is up) or via a test-mesh forwarder (if the direct link is down but the destination is still reachable through the mesh). Either way, the npub-as-name addressing works the same way.

What you've learned

  • Open discovery is the consume side of full ambient participation. With policy: open plus advertising, your daemon both publishes its advert and consumes everyone else's in the namespace.
  • The namespace defines the scope. Everyone publishing with the same app value finds each other; different namespaces are isolated discovery sets.
  • Best-effort means failure is normal. Many discovered peers won't actually connect; that's expected and rate-limited by open_discovery_max_pending.
  • Static and open coexist. Configured peers keep working with their own dial paths; open-discovered peers stack on top.
  • The peer ACL still gates everything. Open is admission-free relative to discovery, not relative to your ACL — the ACL is what you'd use to restrict who can connect if you don't want a fully open posture.

Custom namespaces for private experiments

If you want to use FIPS open discovery for a private set of nodes — colleagues, a workshop cohort, a specific deployment — set a custom app value:

discovery:
  nostr:
    enabled: true
    advertise: true
    policy: open
    app: "my-team.experiment-1"

All nodes participating in the experiment use the same app string. Pick something distinctive — short identifiers risk colliding with other operators' experiments. Once your nodes use a custom app, they no longer find or are found by the public test mesh (the public mesh uses fips-overlay-v1).

Troubleshooting

If your peer list doesn't grow past the inbound peers from the previous tutorial:

  • Wait. Open discovery accumulates adverts; the first pass after restart can take a couple of minutes to populate on a new subscription.
  • Verify the namespace. With app: unset, the daemon uses fips-overlay-v1. If you set a custom app: for an experiment, your daemon is in a different namespace than the public test mesh and will only find peers using the same value.
  • Check relay reachability. Open discovery is a subscription rather than one-shot queries — if the 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 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:

  • Lower open_discovery_max_pending. Default 64; setting it to e.g. 16 caps in-flight connection attempts. Adverts beyond that wait in a queue.
  • Use a custom app. Move to a private namespace where only nodes you're coordinating with publish.
  • Use the peer ACL. See ../reference/security.md for the ACL format if you want explicit allow/deny rules.

What's next

  • Reach services on other mesh nodes. reach-mesh-services drives nc, traceroute6, curl, and ssh at peers by .fips name — any of the open-discovered peers in your list, or any node you reach through them.

  • Host a service of your own. host-a-service brings up an HTTP server addressable as <your-npub>.fips, bound to fips0 so the exposure is mesh-only, behind the mesh firewall.

  • ground-up-mesh — Bring up two devices on a shared physical link (Ethernet, WiFi, or Bluetooth) with no pre-existing IP infrastructure. The second deployment mode of FIPS, a parallel to the overlay-on-internet path the Nostr-discovery tutorials covered.

For the operator-style scenario reference covering all five shapes of Nostr discovery side-by-side:

For the wire format and discovery design: