8.7 KiB
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 <relay_npub> ──────────────────► │ 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 <key> │
│ | 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
nakand runs the existing 4-linesrc/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: trueandnode.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:
- Takes the relay's Nostr secret key, the UDP port it listens on, and its public IP (or auto-detect via an echo service).
- Constructs a kind 37195 event with:
dtag:fips-overlay-v1protocoltag:fips-overlay-v1versiontag:1expirationtag: now + 3600- content JSON:
{"identifier":"fips-overlay-v1","version":1,"endpoints":[{"transport":"udp","addr":"<ip>:<port>"}]}
- Signs and publishes with
nak event -k 37195 --sec <key>piped tonak publish(ornostcat). - 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 already does this. Document that the relay runs:
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:
- 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). - Runs
nak req -k 37195 -d fips-overlay-v1 -a <relay_npub>against the relays. - Parses the returned event's content JSON, extracts the first
udpendpoint'saddrfield. - Prints
ip portto stdout (suitable for piping intosrc/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:
#!/bin/bash
# Usage: echo "hello" | src/udp_nostr_send_via_npub.sh <relay_npub> [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
- Start the relay:
src/udp_nostr_recv.py+src/udp_nostr_relay_advertise.shwith a test key. - From a separate machine (or loopback), run
src/udp_nostr_send_via_npub.sh <relay_npub>with a test message. - Verify the message arrives at the relay.
- 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 or a new src/test_discovery.sh.
Step 6 — Document the combination
Add a section to README.md (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.mdModel B).
Deliverable: new documentation section.
Open questions / decisions
-
Advert kind reuse vs. new kind. Reusing kind 37195 with
d=fips-overlay-v1means FIPS nodes will see these adverts and may try to FMP-handshake the relay. Options:- (a) Use a different
dtag (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.
- (a) Use a different
-
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. -
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 |