Files
udp_nostr/plans/udp_nostr_fips_discovery.md
2026-08-12 16:34:22 -04:00

155 lines
8.7 KiB
Markdown

# 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 `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":"<ip>:<port>"}]}`
3. Signs and publishes with `nak event -k 37195 --sec <key>` 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 <relay_npub>` 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_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
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 <relay_npub>` 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 |