# Plan: UDP Nostr + FIPS Discovery (Combination 1) ## Goal Combine UDP Nostr's no-handshake transmission with FIPS's Nostr-mediated endpoint discovery, so that: - The **relay** can move IPs freely and re-advertise its current UDP endpoint on Nostr (kind 37195 advert). Its npub is the stable address. - The **sender** looks up the relay's current endpoint by npub, then sends a single raw UDP datagram — no FIPS daemon, no Noise handshake, no connection state. - The no-handshake property of UDP Nostr is **fully preserved** on the data plane. FIPS is used only for discovery, never for transport. ## Architecture ``` Sender Relay │ │ │ 1. nak req -k 37195 -d fips-overlay-v1 │ │ -a ──────────────────► │ Nostr relay pool │ advert │ (wss://relay.damus.io, etc.) │ ◄──────────────────────────────────── │ │ endpoints: [{udp, 203.0.113.45:8889}] │ │ │ │ 2. extract udp endpoint from advert │ │ │ │ 3. nak event -k 1 -c "..." --sec │ │ | python3 src/udp_nostr_send.py │ │ 203.0.113.45 8889 │ │ ──────── single UDP datagram ────────► │ src/udp_nostr_recv.py │ (no handshake) │ (4-line listener) │ │ │ when IP changes: │ relay re-publishes kind 37195 │ with new endpoint ``` ### What runs where | Component | Runs FIPS? | Runs UDP Nostr? | Role | |-----------|:----------:|:---------------:|------| | Sender | No | Yes (send) | Fetches advert, sends raw UDP datagram | | Relay | Yes (discovery only) | Yes (recv) | Publishes kind 37195 advert, listens for UDP datagrams | | Nostr relays | — | — | Carry the kind 37195 adverts (existing public relays) | The relay does **not** need to run the full FIPS mesh. It only needs the discovery/advert-publishing piece. Two implementation paths for the relay side: - **Path A (minimal):** A small script that publishes kind 37195 adverts using `nak` and runs the existing 4-line [`src/udp_nostr_recv.py`](../src/udp_nostr_recv.py). No FIPS daemon at all — just re-use FIPS's advert *format* and the public Nostr relay pool. - **Path B (full FIPS):** Run the FIPS daemon with `transports.udp.advertise_on_nostr: true` and `node.discovery.nostr.advertise: true`, but **do not accept FMP connections** — only use FIPS for advert publishing + NAT-traversal + auto-re-advertise on IP change. The UDP Nostr listener runs alongside it on a separate port. Path A is simpler and keeps the relay side dependency-free. Path B gives you automatic re-advertisement on IP change and STUN-based NAT traversal for free. Start with Path A, upgrade to Path B if the relay is behind NAT. ## Implementation steps ### Step 1 — Relay: publish endpoint advert (Path A, minimal) Write a script `udp_nostr_relay_advertise.sh` that: 1. Takes the relay's Nostr secret key, the UDP port it listens on, and its public IP (or auto-detect via an echo service). 2. Constructs a kind 37195 event with: - `d` tag: `fips-overlay-v1` - `protocol` tag: `fips-overlay-v1` - `version` tag: `1` - `expiration` tag: now + 3600 - content JSON: `{"identifier":"fips-overlay-v1","version":1,"endpoints":[{"transport":"udp","addr":":"}]}` 3. Signs and publishes with `nak event -k 37195 --sec ` piped to `nak publish` (or `nostcat`). 4. Re-runs on a timer (e.g. every 30 min, or on IP-change detection) to keep the advert fresh. **Deliverable:** `udp_nostr_relay_advertise.sh` in this repo. ### Step 2 — Relay: run the UDP Nostr listener No changes needed — the existing [`src/udp_nostr_recv.py`](../src/udp_nostr_recv.py) already does this. Document that the relay runs: ```bash python3 src/udp_nostr_recv.py 0.0.0.0 8889 ``` alongside the advert publisher from Step 1. **Deliverable:** updated README section showing the relay-side setup. ### Step 3 — Sender: resolve endpoint from advert Write a script `udp_nostr_resolve.sh` (or `.py`) that: 1. Takes the relay's npub and an optional list of Nostr relays (default: the FIPS defaults — `wss://relay.damus.io`, `wss://nos.lol`, `wss://offchain.pub`). 2. Runs `nak req -k 37195 -d fips-overlay-v1 -a ` against the relays. 3. Parses the returned event's content JSON, extracts the first `udp` endpoint's `addr` field. 4. Prints `ip port` to stdout (suitable for piping into `src/udp_nostr_send.py`). **Deliverable:** `udp_nostr_resolve.py` in this repo. ### Step 4 — Sender: one-shot send-via-discovery command Write a wrapper `src/udp_nostr_send_via_npub.sh` that chains resolve + send: ```bash #!/bin/bash # Usage: echo "hello" | src/udp_nostr_send_via_npub.sh [relay_url ...] NPUB=$1; shift ENDPOINT=$(python3 src/udp_nostr_resolve.py "$NPUB" "$@") nak event -k 1 -c "$(cat)" --sec $(nak key generate) \ | python3 src/udp_nostr_send.py $ENDPOINT ``` **Deliverable:** `src/udp_nostr_send_via_npub.sh` in this repo. ### Step 5 — Test end-to-end 1. Start the relay: `src/udp_nostr_recv.py` + `src/udp_nostr_relay_advertise.sh` with a test key. 2. From a separate machine (or loopback), run `src/udp_nostr_send_via_npub.sh ` with a test message. 3. Verify the message arrives at the relay. 4. Change the relay's advertised IP (simulate move), re-publish, and verify the sender picks up the new endpoint on the next resolve. **Deliverable:** updated [`src/test_local.sh`](../src/test_local.sh) or a new `src/test_discovery.sh`. ### Step 6 — Document the combination Add a section to [`README.md`](README.md:1) (or a new `discovery.md`) explaining: - The problem this solves (endpoint blocking / relay mobility). - The architecture diagram above. - That FIPS is used for discovery only — the data plane remains handshake-free. - The residual exposure: the sender's Nostr relay lookup is a TCP/WSS handshake. Mitigations: cache adverts, use Tor for the lookup, or embed the endpoint in DNS (cross-reference [`docs/passive_sniffing_relay.md`](../docs/passive_sniffing_relay.md) Model B). **Deliverable:** new documentation section. ## Open questions / decisions 1. **Advert kind reuse vs. new kind.** Reusing kind 37195 with `d=fips-overlay-v1` means FIPS nodes will see these adverts and may try to FMP-handshake the relay. Options: - (a) Use a different `d` tag (e.g. `udp-nostr-v1`) so FIPS nodes ignore it. **Recommended** — cleanest separation. - (b) Use a different kind entirely (e.g. a new application-defined replaceable kind). More work, less reuse. - (c) Accept the stray handshake attempts; the relay just drops them (it's not running FMP). Noisy but harmless. 2. **Advert freshness vs. relay churn.** If the relay's IP changes but it can't re-publish (no connectivity to Nostr relays), senders will have a stale endpoint. Mitigation: short `expiration` (e.g. 15 min) + frequent re-publish. The sender falls back to cached adverts or fails gracefully. 3. **Sender-side relay-list bootstrapping.** The sender needs at least one Nostr relay URL to start the lookup. This is a small bootstrap problem — solved by shipping defaults (the FIPS list) or embedding a relay URL in a DNS TXT record / the relay's npub profile (kind 0). ## What this does NOT do - It does **not** run FIPS on the sender. The sender is still just `nak` + 4 lines of Python. - It does **not** add a handshake to the data plane. The UDP datagram is still fire-and-forget, signature-only. - It does **not** hide the sender's lookup of the relay endpoint. That lookup is a normal Nostr REQ over WSS (or Tor, if the sender chooses). This is a separate problem from the data-plane censorship resistance. - It does **not** provide two-way communication. The sender cannot receive replies unless the relay knows where to send them (which would require a handshake or a return-address in the event). ## File summary | File | Status | Purpose | |------|--------|---------| | `src/udp_nostr_relay_advertise.sh` | new | Relay: publish kind 37195 endpoint advert | | `src/udp_nostr_resolve.py` | new | Sender: fetch relay's current UDP endpoint by npub | | `src/udp_nostr_send_via_npub.sh` | new | Sender: one-shot resolve + send | | `src/udp_nostr_recv.py` | existing | Relay: unchanged 4-line listener | | `src/udp_nostr_send.py` | existing | Sender: unchanged 4-line sender | | `src/test_discovery.sh` | new | End-to-end test | | `README.md` | update | Document the discovery combination |