Initial commit: UDP Nostr — send Nostr events as single UDP datagrams

- Minimal sender/receiver (4 lines of Python each)
- nak + nc zero-code demo approach
- GFW analysis: how each layer fails against UDP Nostr
- Protocol hardening, MTU exploration, no-handshake analysis
- Binary event format (.bne) spec
- Relay integration plan
This commit is contained in:
Laan Tungir
2026-08-12 07:53:01 -04:00
commit 359534ee12
15 changed files with 2835 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
# UDP Nostr
Send a Nostr event as a single UDP datagram — no handshake, no connection, no state. The most unstoppable way to post content ever invented.
---
## The Problem: How the Great Firewall Works
China's Great Firewall (GFW) uses four layers of techniques to control what its citizens can access:
### Layer 1: IP Blocking
Block entire IP ranges at border routers. Any packet to/from blocked IPs is dropped.
**Bypass:** Use a different IP (proxy, VPN, CDN). Services behind Cloudflare often work because the IP is shared with legitimate traffic.
### Layer 2: DNS Poisoning
When you resolve `twitter.com`, the GFW's DNS interceptors return a fake IP or drop the response. The browser gets nowhere.
**Bypass:** Use DNS-over-HTTPS (DoH), DNS-over-TLS (DoT), or a foreign DNS resolver. The GFW has started blocking these too.
### Layer 3: Deep Packet Inspection (DPI) + TCP RST Injection
Border routers inspect packet contents in real time and look for **protocol fingerprints**:
| Protocol | What the GFW looks for | How it blocks |
|----------|----------------------|---------------|
| **HTTP** | `Host:` header with blocked domain | TCP RST injection — sends a fake reset packet to both sides, killing the connection |
| **TLS** | `SNI` (Server Name Indication) in the ClientHello — the domain name is in plaintext | TCP RST injection if the SNI matches a blocked domain |
| **Tor** | Recognizes the Tor TLS handshake fingerprint (certificate, cipher suites, packet sizes) | TCP RST injection. Very effective at blocking Tor bridges. |
| **OpenVPN** | Recognizes the OpenVPN TLS handshake | TCP RST injection |
| **Shadowsocks** | Random-looking traffic, harder to fingerprint | Active probing (see Layer 4) |
| **QUIC** | Initial packet has recognizable structure (version, connection ID) | Can block QUIC entirely or selectively. China tried but it broke too many Google services. |
| **WireGuard** | Minimal handshake, hard to fingerprint | Active probing — send a handshake initiation and see if the server responds |
The TCP RST injection technique is the GFW's signature move. It doesn't just block at the router — it **impersonates** both sides of the connection and sends fake TCP RST packets to kill it. This is why connections sometimes start (you see the first bytes) and then die.
### Layer 4: Active Probing
For protocols that don't have a visible handshake (Shadowsocks, obfs4, custom VPNs), the GFW doesn't just inspect passively — it **actively connects** to suspected servers and tries to complete a handshake. If the server responds with a valid protocol handshake, the IP is added to the blocklist.
This is the most sophisticated layer — and the most relevant to this project.
---
## The Key Asymmetry
The GFW's most powerful tool — active probing — works by detecting protocol handshakes. Every circumvention tool (VPN, Tor, Shadowsocks) has a handshake that can be detected. The GFW connects to the server, sees the handshake, and blocks the IP.
UDP Nostr has no handshake. The adversary can send a probe, but the relay's response to an invalid event is indistinguishable from a server that isn't running Nostr at all. The adversary cannot tell if the relay is a Nostr relay or just a random UDP service.
This is the fundamental advantage. Every other circumvention tool is playing a cat-and-mouse game of obfuscating its handshake. UDP Nostr eliminates the handshake entirely.
### How UDP Nostr Defeats Each Layer
| GFW technique | Can it block UDP Nostr? | Why |
|--------------|------------------------|-----|
| **IP blocking** | Yes — but only if they know the relay's IP | The relay can move. Blocking one IP doesn't block the protocol. |
| **DNS poisoning** | No | The sender doesn't need DNS. Send the UDP packet to the IP directly. |
| **DPI (passive)** | **No** | No protocol fingerprint. No handshake, no SNI, no TLS ClientHello, no recognizable structure. Just bytes. |
| **TCP RST injection** | **No** | RST is a TCP control packet. UDP has no RST. The GFW cannot kill a UDP "connection" because there is no connection. |
| **Active probing** | **No** | The adversary sends a fake event. The relay tries to verify the signature, fails, and drops the packet. No handshake to detect. The relay's response to an invalid event looks the same as random noise. |
| **Block all UDP** | Yes — but this breaks DNS, QUIC/HTTP3, VoIP, video calls, gaming, and more | China has not done this because it would break the internet for 1.4 billion people. |
---
## Quick Start: `nak` + Python (4 lines each)
The sender reads a signed Nostr event from stdin and sends it as one UDP datagram. The receiver listens and prints the JSON.
**Terminal 1 — receiver:**
```bash
python3 udp_nostr_recv.py 0.0.0.0 8889
```
**Terminal 2 — sender:**
```bash
nak event -k 1 -c "Hello via UDP Nostr!" --sec $(nak key generate) \
| python3 udp_nostr_send.py 127.0.0.1 8889
```
Or send to a remote server:
```bash
nak event -k 1 -c "Hello from local to laantungir.net via UDP!" --sec $(nak key generate) \
| python3 udp_nostr_send.py laantungir.net 8889
```
### The complete programs
**`udp_nostr_send.py`** — 4 lines of code:
```python
import socket, sys
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(sys.stdin.read().encode(), (sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 8888))
```
**`udp_nostr_recv.py`** — 4 lines of code:
```python
import socket, sys
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((sys.argv[1] if len(sys.argv) > 1 else "0.0.0.0", int(sys.argv[2]) if len(sys.argv) > 2 else 8888))
while True:
data, addr = sock.recvfrom(65535)
print(f"{len(data)} bytes from {addr[0]}:{addr[1]}")
print(data.decode())
print()
```
### What the receiver prints
```
375 bytes from 146.70.187.194:48422
{"kind":1,"id":"68b792a2dc969a7c44442f8d0c8ea4136e334e13294e0ca7e2706ebf1164d9e2","pubkey":"164e87cc5f5fa994bb345cc5c86ed526c433df49be10951c4e1927889859602d","created_at":1786534043,"tags":[],"content":"Minimal demo over real internet!","sig":"b28061b058346e75fcf45f82942b9eb500f4411b4ab4d91e0a4d344ae57c502085dcc830d42449e26a7952ec731ada10ff92fd8708bbf6f0b8460c72197416d1"}
```
---
## Quick Start: `nak` + `nc` (zero custom code)
For an even simpler demo, use `nc` (netcat) — available on virtually every Linux machine. No Python needed at all.
**Terminal 1 — receiver:**
```bash
nc -u -l 8889
```
**Terminal 2 — sender:**
```bash
nak event -k 1 -c "Hello via nc -u!" --sec $(nak key generate) \
| nc -u -w1 127.0.0.1 8889
```
The `-u` flag tells `nc` to use UDP. `-l` makes it listen. `-w1` closes the sender after 1 second of inactivity (otherwise `nc` hangs waiting for more stdin).
The receiver prints the raw Nostr event JSON:
```json
{"kind":1,"id":"1d43fe396e9c48b0f6c95290365e3fcf6e490e85a023a80f87ec9845d1cef838","pubkey":"205abcb7aa5ea9c6e9d09dbf9a5f50ed619f510d52888060e4ea9caaac310666","created_at":1786534242,"tags":[],"content":"Hello via nc -u!","sig":"1fbee9529194f43749b15ff5c75c11b541b06d04c799daf89473f5a3ba864a35f3ad63e0537b79bd468cadabf5d2fe26762ef1066ee69e30ed012c6fe344ac32"}
```
### `nc` vs Python: the single-packet guarantee
| Tool | Send | DF flag | Single-packet guarantee |
|------|------|---------|------------------------|
| `nc -u` | ✓ | ✗ | No — best effort only |
| Python (4 lines) | ✓ | ✓ | Yes — `EMSGSIZE` if it would fragment |
`nc` has two limitations:
1. **No DF (Don't Fragment) flag**`nc` doesn't expose `setsockopt()`, so you can't set `IP_MTU_DISCOVER`. Without DF, the kernel may silently fragment the packet and neither side knows.
2. **No guarantee of single datagram**`nc` reads stdin in chunks and sends each chunk as a separate UDP datagram. In practice, since `nak event` outputs one line and closes, `nc` usually reads it all at once and sends one datagram. But this is not guaranteed.
The Python sender exists specifically because it can call `sock.setsockopt(socket.IPPROTO_IP, 10, 2)` to set the DF flag — the only way to *prove* single-packet delivery. For a quick demo, `nc` works fine. For the actual protocol guarantee, you need the Python sender.
---
## What This Demonstrates
1. **No handshake** — The sender creates and sends the event in one shot. No prior communication with the receiver. No SYN, no TLS ClientHello, no key exchange.
2. **Self-validating** — The event carries its own proof of authorship (the signature). The receiver validates using only the pubkey in the event. No shared secrets, no session setup.
3. **Single datagram** — The entire event fits in one UDP packet (under 1472 bytes). This means it cannot be blocked without blocking all UDP traffic.
4. **Stateless** — The receiver does not store any state about the sender. It receives, prints, and forgets.
5. **No dependencies**`nak` + `nc` (zero custom code) or `nak` + 4 lines of Python.
## The Key Insight
Compare this to any TCP-based protocol:
| Protocol | Handshake | Round trips before data |
|----------|-----------|------------------------|
| HTTP/1.1 | TCP SYN + TLS | 3 (SYN, SYN-ACK, ACK + ClientHello, ServerHello+Cert, Finished) |
| HTTP/3 (QUIC) | QUIC handshake | 1 (but still requires response) |
| **Nostr over UDP** | **None** | **0 — fire and forget** |
The Nostr event is already signed before it touches the network. **The signature replaces the handshake.**
## Maximum Event Size: JSON vs. Binary (.bne)
A single UDP datagram can carry **1472 bytes** of payload (1500 Ethernet MTU - 20 IP header - 8 UDP header). Here's how the two formats compare for a kind 1 event with empty tags:
| Component | JSON | `.bne` Binary |
|---|---|---|
| `id` | 64 hex chars | *(computed from SHA256 hash — not on wire)* |
| `pubkey` | 64 hex chars | 32 raw bytes |
| `created_at` | 10 digit chars | 8 bytes (uint64) |
| `kind` | 1 digit char | 2 bytes (uint16) |
| `tags` (empty) | 2 chars `[]` | 2 bytes (count=0) |
| `content_length` | *(implicit from JSON)* | 4 bytes (uint32) |
| `sig` | 128 hex chars | 64 raw bytes |
| JSON structural chars | 68 bytes (`{"id":"...","pubkey":"..."...}`) | 0 |
| **Fixed overhead** | **337 bytes** | **113 bytes** |
| **Available for content** | **1135 bytes** | **1359 bytes** |
| **English words (~5.5 chars/word)** | **~206 words** | **~247 words** |
The `.bne` binary format gives **224 more bytes (20% more)** of content per datagram. The savings come from:
- **No hex encoding**: `pubkey` 64→32 bytes, `sig` 128→64 bytes (96 bytes saved)
- **No `id` on the wire**: computed from SHA256 hash of the event (64 bytes saved)
- **No JSON structural overhead**: no quotes, commas, colons, braces (68 bytes saved)
See [`max_single_packet_event.md`](../rethinking_nostr/max_single_packet_event.md) in `rethinking_nostr/` for the full `.bne` byte layout.
## Single-Packet Guarantee
### How the Sender Enforces It
The sender can set the **Don't Fragment (DF)** flag on the IP header:
```python
sock.setsockopt(socket.IPPROTO_IP, 10, 2) # IP_MTU_DISCOVER = DO
```
With DF set, the kernel checks the path MTU before sending. If the packet is larger than the path MTU, `sendto()` raises `OSError` (`EMSGSIZE`) and the sender knows **immediately** that the packet would have been fragmented. Without this flag, the IP layer fragments silently and neither sender nor receiver can tell.
### What the Receiver Can Know
The receiver **cannot** directly observe IP-level fragmentation. The OS reassembles fragments before delivering to `recvfrom()`. The receiver only sees the complete reassembled datagram. The size check is a heuristic — if the datagram is ≤ 1472 bytes, it *could* have been sent as one packet, but the receiver cannot prove it.
### The Asymmetry
| Role | Can they know? | How? |
|------|---------------|------|
| **Sender** | **Yes, definitively** | DF flag → `EMSGSIZE` if fragmentation needed |
| **Receiver** | **No, not definitively** | OS hides fragmentation; can only check size |
| **Adversary (on-path)** | **Yes** | Can observe IP fragments directly |
| **Adversary (off-path)** | **No** | Cannot see the packet at all |
The sender is the only party that can **enforce** single-packet delivery. The receiver can only **infer** it. This is why the DF flag is critical — it moves the guarantee from "probably" to "provably."
### What About the OS?
- **Linux**: `IP_MTU_DISCOVER` with `IP_PMTUDISC_DO` sets DF and performs path MTU discovery
- **macOS/BSD**: `IP_DONTFRAG` socket option (different constant, same effect)
- **Windows**: `setsockopt` with `IP_DONTFRAGMENT` (requires WinSock)
## Related Documents in This Directory
| Document | Description |
|---|---|
| [`protocol_hardening.md`](protocol_hardening.md) | The core idea: design protocols so interference requires violating physics or OS guarantees. Explains the MTU boundary, substrate constraints, and the adversary's dilemma. |
| [`mtu_exploration.md`](mtu_exploration.md) | Survey of 10 projects/people who have worked on MTU exploitation for censorship resistance: QUIC, CurveCP (djb), obfs4 (Tor), DNS tunneling, uTP (BitTorrent), WireGuard, CoAP/DTLS, binary events, IP fragmentation, tcpcrypt. |
| [`no_handshake.md`](no_handshake.md) | Analysis of the no-handshake property: why Nostr's self-validating events are fundamentally different from every protocol that announces itself with a handshake pattern. |
| [`udp_nostr_send.py`](udp_nostr_send.py) | Sender: reads signed event JSON from stdin (pipe from `nak`), sends as single UDP datagram. |
| [`udp_nostr_recv.py`](udp_nostr_recv.py) | Receiver: listens for UDP datagrams, prints the JSON. |
| [`udp_nostr_send.js`](udp_nostr_send.js) | Sender (JavaScript/Node.js): same logic using the built-in `dgram` module. |
| [`udp_nostr_recv.js`](udp_nostr_recv.js) | Receiver (JavaScript/Node.js): same logic using the built-in `dgram` module. |
+333
View File
@@ -0,0 +1,333 @@
# TCP vs. UDP: The Two Fundamental Methods of Sending Data Across the Internet
These are the two transport-layer protocols that sit directly above IP (Internet Protocol) in the network stack. They are the **substrate** — the physical/OS-level guarantees (or lack thereof) that every application protocol must build on.
---
## 1. TCP — Transmission Control Protocol
**What it is:** A **connection-oriented**, **reliable**, **ordered** byte stream between two computers.
### How It Works
1. **Three-way handshake** — Before any data flows, the sender and receiver exchange three packets:
- `SYN` → (sender says "I want to talk")
- `SYN-ACK` ← (receiver says "OK, I'm ready")
- `ACK` → (sender says "Great, let's go")
This creates **state** on both sides: sequence numbers, window sizes, buffers.
2. **Data transfer** — Bytes are numbered with sequence numbers. The receiver sends `ACK` packets confirming receipt. If the sender doesn't get an ACK within a timeout, it **retransmits**.
3. **Flow control** — The receiver tells the sender how much buffer space it has left. The sender slows down if the receiver is overwhelmed.
4. **Congestion control** — TCP dynamically adjusts its sending rate based on inferred network congestion (packet loss = slow down, success = speed up).
5. **Connection teardown** — A four-packet exchange (`FIN`, `FIN-ACK`, `ACK`) to close gracefully.
### Key Properties
| Property | Description |
|---|---|
| **Reliability** | Every byte arrives, in order, or the connection fails |
| **Ordering** | Bytes arrive in the exact order sent |
| **Connection-oriented** | Both sides maintain state |
| **Stream-oriented** | No message boundaries — just a continuous stream of bytes |
| **Detectable** | The SYN/SYN-ACK/ACK pattern is unmistakable to deep packet inspection |
### The Cost
- **Latency:** One round trip before any data can be sent
- **State overhead:** Both ends must maintain connection state
- **Head-of-line blocking:** A lost packet stalls all subsequent data
- **Visible handshake:** The SYN/SYN-ACK/ACK pattern can be fingerprinted and blocked
---
## 2. UDP — User Datagram Protocol
**What it is:** A **connectionless**, **unreliable**, **message-oriented** datagram service.
### How It Works
1. **No handshake** — The sender just puts bytes on the wire. No prior communication with the receiver. No state created on either side.
2. **Fire and forget** — The sender calls `sendto()` and the OS fires the datagram out the network interface. There is no acknowledgment, no retransmission, no guarantee of delivery.
3. **Message boundaries preserved** — Each `sendto()` call produces exactly one datagram. The receiver's `recvfrom()` returns exactly that datagram (or nothing). Messages are not split or merged.
4. **No flow control, no congestion control** — The application is responsible for managing its own sending rate.
### Key Properties
| Property | Description |
|---|---|
| **Unreliability** | Packets may be lost, duplicated, or arrive out of order |
| **No connection** | Zero state on either side |
| **Message-oriented** | Datagram boundaries are preserved |
| **Low latency** | Data can be sent immediately — zero round trips before the first byte |
| **Hard to detect** | A single UDP datagram is just a blob of bytes — no handshake pattern |
### The Cost
No delivery guarantees. The application must handle loss, reordering, and duplication itself (if it cares).
---
## The Critical Difference
| Property | TCP | UDP |
|---|---|---|
| **Handshake** | 3-way (SYN, SYN-ACK, ACK) | None |
| **State** | Connection state on both sides | Stateless |
| **Reliability** | Guaranteed delivery, in order | Best effort, no guarantees |
| **Message boundaries** | Stream (no boundaries) | Datagram (boundaries preserved) |
| **Latency before first byte** | 1 RTT minimum | Zero |
| **Detectable pattern** | Yes — unmistakable handshake | No — indistinguishable from noise |
| **Censorship resistance** | Low — handshake can be blocked | High — no pattern to block |
| **MTU awareness** | Fragments transparently | Application must manage packet size |
---
## The MTU Boundary (Why 1472 Bytes Matters)
The **Maximum Transmission Unit (MTU)** for Ethernet is **1500 bytes**. This is a physical/hardware constraint:
```
Ethernet frame: 1500 bytes (hardware limit)
- IP header: 20 bytes (OS adds this)
- UDP header: 8 bytes (OS adds this)
= UDP payload: 1472 bytes (what your application can send)
```
If you send more than 1472 bytes over UDP, the IP layer **fragments** the datagram into multiple packets. The receiver's OS reassembles them before delivering to the application. But fragmentation:
- Creates multiple packets that can be blocked individually
- Makes the communication visible (fragmented packets have identifiable headers)
- Fails if any fragment is lost (the entire datagram is lost)
If you keep your message **under 1472 bytes**, it fits in exactly one Ethernet frame. This is the **single-packet guarantee** — the message is atomic at the hardware level.
---
## The No-Handshake Property (Why It Matters for Censorship)
| Protocol | Handshake | Round trips before data | Detectable pattern |
|---|---|---|---|
| TCP | SYN, SYN-ACK, ACK | 1 | Yes — unmistakable |
| TLS | ClientHello, ServerHello+Cert, Finished | 2+ | Yes — certificate, cipher suites |
| HTTP/3 (QUIC) | QUIC Initial + Handshake | 1 | Partially — Initial packet has structure |
| WireGuard | Handshake initiation + response | 1 | Minimal but recognizable |
| **Nostr over UDP** | **None** | **0 — fire and forget** | **No pattern at all** |
A Nostr event is **self-validating** — it carries its own signature. The **signature replaces the handshake**. The receiver validates the event using only the pubkey in the event. No prior relationship, no shared secrets, no session setup.
This means:
- **No handshake pattern to fingerprint** — a single UDP datagram is just bytes on the wire
- **No active probing vulnerability** — the adversary cannot distinguish a Nostr relay from any other UDP service by sending a probe, because there's no handshake to respond to
- **No amplification attack vector** — the response (if any) is smaller than the request
- **Stateless relays** — no connection state to exhaust with SYN floods
---
## The Great Dichotomy Applied
From *TheGreatDichomety.md*: things that happen **inside computers** vs. **outside computers**.
- **TCP** is a complex **inside-computer** construct. It creates an abstraction (a reliable byte stream) that doesn't exist in the physical network. The OS kernel maintains connection state, sequence numbers, timers, retransmission buffers — all patterns of bits inside the computer. The handshake is a negotiation between two computers' internal state machines.
- **UDP** is closer to the **outside-computer** reality. It maps almost directly to what the network hardware does: fire packets at the wire and hope they arrive. No state, no negotiation, no abstraction. Just bytes.
The Nostr-over-UDP approach exploits this: by eliminating the handshake (the inside-computer negotiation), the communication becomes indistinguishable from the raw physical substrate. The adversary cannot distinguish your message from any other random noise on the wire, because at the physical level, that's exactly what it is.
---
## Summary Diagram
```mermaid
flowchart LR
subgraph Application
A[Nostr Event<br/>self-validating]
end
subgraph Transport
B[UDP<br/>no handshake<br/>no state]
C[TCP<br/>3-way handshake<br/>connection state]
end
subgraph Network
D[IP<br/>packets]
end
subgraph Physical
E[Ethernet frames<br/>MTU: 1500 bytes]
end
A --> B
A --> C
B --> D
C --> D
D --> E
style B fill:#4a4,color:#fff
style C fill:#a44,color:#fff
```
The green path (UDP) is the one explored in this project — it leaves no trace, requires no negotiation, and exploits the physical properties of the substrate (MTU boundaries, no handshake pattern) to resist censorship.
---
## References
- [`2Approaches.md`](2Approaches.md) — The cypherpunk problem and approaches to solving it
- [`udp_nostr_demo/README.md`](udp_nostr_demo/README.md) — UDP Nostr demo documentation
- [`udp_nostr_demo/no_handshake.md`](udp_nostr_demo/no_handshake.md) — The no-handshake property in depth
- [`udp_nostr_demo/mtu_exploration.md`](udp_nostr_demo/mtu_exploration.md) — MTU as a cypherpunk tool
- [`rethinking_nostr/max_single_packet_event.md`](rethinking_nostr/max_single_packet_event.md) — Binary event format byte layout
- [`TheGreatDichomety.md`](TheGreatDichomety.md) — Inside computers vs. outside computers
---
## 3. Raw IP — Can You Skip TCP and UDP Entirely?
**Short answer:** Yes, you can send data directly over IP using **raw sockets** (`SOCK_RAW`). But there are serious practical restrictions that make it nearly useless on the open internet.
### How It Works
The IP header has an 8-bit **protocol** field that identifies what transport protocol is carrying the payload:
| Protocol Number | Name | Common Use |
|---|---|---|
| 1 | ICMP | `ping`, traceroute, error reporting |
| 2 | IGMP | Multicast group management |
| 6 | TCP | Web, email, file transfer |
| 17 | UDP | DNS, video, gaming, Nostr |
| 132 | SCTP | Telephony, signaling |
| 33 | DCCP | Streaming, congestion-controlled UDP |
You can put **any value** in this field. If you write a raw socket program that sets protocol field to, say, 249, your packets will be sent out onto the wire with that protocol number. A receiver with a matching raw socket can read them.
### Raw Socket Code (Linux)
```python
import socket
# SOCK_RAW lets you craft your own IP packets
# IPPROTO_RAW (255) means you supply the entire IP header yourself
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
# Or use a custom protocol number (e.g., 249)
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, 249)
```
### The Three Levels of Socket
```
Application
|
|--- SOCK_STREAM (TCP) -- You give bytes, OS handles everything
|--- SOCK_DGRAM (UDP) -- You give datagrams, OS adds UDP + IP headers
|--- SOCK_RAW (IP) -- You supply IP header (or let OS add it partially)
|
Network interface (Ethernet)
```
### The Problems with Raw IP
#### 1. Root privileges required
On Linux, creating a raw socket requires `CAP_NET_RAW` capability or root. Normal user programs cannot use `SOCK_RAW`. This alone makes it impractical for most applications.
#### 2. Firewalls block non-standard protocols by default
Almost every firewall, NAT gateway, and router on the internet is configured to pass only TCP (protocol 6) and UDP (protocol 17). Everything else is dropped. Some also pass ICMP (protocol 1) for diagnostics.
If you send packets with protocol 249, they will be dropped by the first router that doesn't recognize the protocol. Your packets never reach the destination.
#### 3. NAT requires TCP or UDP
Network Address Translation (NAT) — used by virtually every home router and corporate firewall — works by tracking **ports**. TCP and UDP have 16-bit port numbers (0-65535) that NAT uses to map internal addresses to external ones.
Raw IP has no port concept. There is no way for a NAT to track which internal computer a raw IP packet belongs to. The NAT will either drop the packet or misroute it.
This is the killer problem. Even if you could get raw IP packets through firewalls, they cannot traverse NAT.
#### 4. OS network stacks are not designed for it
- **No demultiplexing:** The OS uses the protocol field to decide which socket gets the packet. If no socket is listening for protocol 249, the packet is dropped silently.
- **No buffering:** Raw sockets don't get the same buffer management as TCP/UDP.
- **No standard API:** Raw socket behavior varies between Linux, BSD, and Windows.
#### 5. Middleboxes interfere
Deep packet inspection (DPI) devices, intrusion detection systems, and enterprise firewalls are trained on TCP and UDP. They may drop or flag packets with unusual protocol numbers as anomalous or malicious.
### The Practical Reality
| Method | Traverses NAT? | Traverses Firewalls? | Works on open internet? |
|---|---|---|---|
| **TCP** | Yes | Yes (common ports) | Yes |
| **UDP** | Yes | Yes (common ports) | Yes |
| **ICMP (ping)** | Sometimes | Sometimes (often rate-limited) | Limited |
| **Raw IP (custom protocol)** | **No** | **No** | **No** |
### What About Protocols Like QUIC?
QUIC (HTTP/3) runs over **UDP**, not raw IP. It uses UDP port 443. This is why it works — it inherits all the NAT and firewall traversal properties of UDP. The "QUIC runs over UDP" design was a deliberate choice to ensure it would work on the existing internet.
### The Great Dichotomy Applied
- **TCP and UDP** are **inside-computer** abstractions that the OS provides to applications. They are implemented in the kernel. But they are also the only protocols that the **outside-computer** network infrastructure (routers, NATs, firewalls) has been built to handle.
- **Raw IP** is closer to the wire — you're speaking the language of routers directly. But the network infrastructure has evolved to expect TCP or UDP on top. The outside-computer world has standardized on these two protocols.
The asymmetry: you can send raw IP packets from your computer, but the network between you and the destination will almost certainly drop them. The internet is not a neutral packet-delivery service — it is a TCP/UDP delivery service with everything else as second-class citizens.
---
## Summary: The Full Protocol Stack
```mermaid
flowchart TD
subgraph "Application Layer"
HTTP[DNS / HTTP / Nostr / etc.]
end
subgraph "Transport Layer"
TCP[TCP<br/>protocol 6<br/>reliable, ordered, connection]
UDP[UDP<br/>protocol 17<br/>unreliable, stateless, datagram]
RAW[Raw IP<br/>protocol any<br/>you craft everything]
end
subgraph "Network Layer"
IP[IP<br/>packets with protocol field]
end
subgraph "Link Layer"
ETH[Ethernet / WiFi<br/>frames with MTU 1500]
end
HTTP --> TCP
HTTP --> UDP
HTTP -.-> RAW
TCP --> IP
UDP --> IP
RAW --> IP
IP --> ETH
style RAW fill:#a4a,color:#fff
style TCP fill:#a44,color:#fff
style UDP fill:#4a4,color:#fff
```
- **Green path (UDP):** Works on the open internet. Used by Nostr-over-UDP, DNS, QUIC, video streaming.
- **Red path (TCP):** Works on the open internet. Used by HTTP, email, SSH. Has detectable handshake.
- **Purple path (Raw IP):** Works only on controlled networks (LAN, VPN, data center). Cannot traverse NAT. Blocked by firewalls. Requires root.
For censorship-resistant communication, UDP is the sweet spot: it works on the real internet, has no handshake pattern, and can be made to look like random noise.
---
## References
- [`2Approaches.md`](2Approaches.md) — The cypherpunk problem and approaches to solving it
- [`udp_nostr_demo/README.md`](udp_nostr_demo/README.md) — UDP Nostr demo documentation
- [`udp_nostr_demo/no_handshake.md`](udp_nostr_demo/no_handshake.md) — The no-handshake property in depth
- [`udp_nostr_demo/mtu_exploration.md`](udp_nostr_demo/mtu_exploration.md) — MTU as a cypherpunk tool
- [`rethinking_nostr/max_single_packet_event.md`](rethinking_nostr/max_single_packet_event.md) — Binary event format byte layout
- [`TheGreatDichomety.md`](TheGreatDichomety.md) — Inside computers vs. outside computers
+652
View File
@@ -0,0 +1,652 @@
# Authoritative DNS Relay — Receiving Nostr Events via DNS Queries
## The Core Idea
Run an authoritative DNS server for a domain you control. A sender crafts a DNS query for a subdomain containing an encoded Nostr event. The global DNS system automatically routes the query to your server, where you extract the event and inject it into your relay.
```
Sender → Sender's DNS resolver (8.8.8.8, 1.1.1.1, ISP, etc.)
↓ "What is the IP of <base64_event>.yourdomain.com?"
Root DNS servers → TLD servers
↓ "yourdomain.com is managed by ns1.yourdomain.com at 1.2.3.4"
Sender's DNS resolver
↓ "Hey 1.2.3.4, what is the IP of <base64_event>.yourdomain.com?"
Your authoritative nameserver (1.2.3.4) ← YOU RECEIVE THE QUERY HERE
↓ "I don't have that record. NXDOMAIN."
Sender's DNS resolver → Sender
```
**You do NOT need to control the sender's DNS resolver.** The global DNS system automatically routes the query to your authoritative nameserver, regardless of which resolver the sender uses. The sender just types a domain name.
---
## Why This Matters
### The Problem It Solves
In the standard UDP Nostr model, the sender addresses a packet directly to the relay:
```
Sender → UDP datagram → Relay IP:Port
```
An observer on the network path sees the relay's IP and can identify the communication. In the authoritative DNS model:
```
Sender → DNS query → yourdomain.com (via standard DNS resolution)
```
An observer sees a DNS query for your domain. The content is hidden in the subdomain label. The relay is not a direct network destination — it's reached through the DNS system.
### The Key Advantage Over Passive Sniffing
The passive sniffing model (Model A in [`passive_sniffing_relay.md`](passive_sniffing_relay.md)) requires the relay to be physically on the network path between sender and destination. This is hard to achieve.
The authoritative DNS model requires **no special network positioning**. The DNS system delivers the query to your server automatically. You just need:
1. A domain name
2. A server with a public IP
3. DNS software configured to log or capture queries
---
## How DNS Resolution Works (The Full Chain)
When a sender queries `<base64_event>.yourdomain.com`, here is exactly what happens:
```
Step 1: Sender's application
→ Asks the OS resolver: "What is the IP of xyz.yourdomain.com?"
Step 2: OS resolver (stub resolver)
→ Checks local cache. If not found:
→ Forwards to configured DNS resolver (e.g., 8.8.8.8, 1.1.1.1, ISP's DNS)
Step 3: Sender's DNS resolver (recursive resolver)
→ Checks its own cache. If not found:
→ Asks a root nameserver: "Who manages .com?"
→ Root responds: "Ask a TLD server at a.gtld-servers.net"
→ Asks the .com TLD server: "Who manages yourdomain.com?"
→ TLD responds: "yourdomain.com is managed by ns1.yourdomain.com at 1.2.3.4"
→ Asks your server (1.2.3.4): "What is the IP of xyz.yourdomain.com?"
Step 4: Your authoritative nameserver (1.2.3.4) ← YOU ARE HERE
→ Receives the query
→ Logs the subdomain label (xyz...)
→ Responds with NXDOMAIN (no such record) or a fake IP
Step 5: Sender's DNS resolver
→ Receives the NXDOMAIN response
→ Returns it to the sender's application
→ Application sees: domain doesn't exist (normal)
```
The critical point: **Step 3 is automatic.** The sender's resolver does all the work of finding your server. You don't need to be on any special network path.
---
## How to Embed the Event
### DNS Label Encoding
A DNS query for a subdomain like:
```
<base64_event>.yourdomain.com
```
The sender encodes the Nostr event as a base64url string and uses it as a DNS label. Your server extracts the label from the query and decodes it.
#### DNS Label Constraints
| Constraint | Value | Impact |
|---|---|---|
| Max label length | 63 bytes | Event must fit in 63 bytes per label segment |
| Max total query length | ~255 bytes | Total encoded event + domain overhead |
| Character set | alphanumeric + hyphen | Base64url encoding required (no `+`, `/`, or `=`) |
| Case sensitivity | Case-insensitive | Use lowercase base64url |
#### Binary Event Fit
Using the `.bne` binary event format from [`max_single_packet_event.md`](../rethinking_nostr/max_single_packet_event.md):
| Format | Event Size | Base64url Size | Fits in Single Label? | Fits in Total Query? |
|---|---|---|---|---|
| JSON (kind 1) | ~400 bytes | ~533 bytes | No (exceeds 63) | No (exceeds 255) |
| Binary `.bne` (kind 1) | ~200 bytes | ~267 bytes | No (exceeds 63) | Borderline |
| Minimal `.bne` (no tags, short content) | ~120 bytes | ~160 bytes | No (exceeds 63) | **Yes** |
| Minimal `.bne` split across 3 labels | ~120 bytes | ~53 bytes/label | **Yes** | **Yes** |
#### Splitting Across Multiple Labels
If the event is too large for a single label, split it across multiple labels:
```
<part1>.<part2>.<part3>.yourdomain.com
```
Each label can hold up to 63 bytes. Three labels give ~189 bytes of base64url data, which decodes to ~141 bytes raw — enough for most single-packet events.
The sender splits the base64url string into chunks and joins them with dots. Your server extracts all labels before `yourdomain.com` and concatenates them.
#### Alternative: EDNS0 Option
EDNS0 (Extended DNS, RFC 6891) allows custom options in DNS packets. A Nostr event could be placed in a custom EDNS0 option:
```
DNS Query Header (12 bytes)
Question Section: <innocent_label>.yourdomain.com
EDNS0 OPT Pseudo-RR:
Option Code: 0xNSTR (custom, unassigned)
Option Data: <binary Nostr event>
```
This is more隐蔽 because the event is not visible in the subdomain label — it's in the EDNS0 option field. However, some DNS resolvers strip unknown EDNS0 options, so reliability may be lower.
---
## Setting Up the Authoritative DNS Server
### Option 1: Full DNS Server (nsd)
[`nsd`](https://www.nlnetlabs.nl/projects/nsd/about/) is a lightweight, authoritative-only DNS server. It does not do recursive resolution — it only answers queries for domains it is authoritative for.
**Installation:**
```bash
sudo apt-get install nsd
```
**Configuration (`/etc/nsd/nsd.conf`):**
```
server:
ip-address: 1.2.3.4
port: 53
zone:
name: yourdomain.com
zonefile: /etc/nsd/yourdomain.com.zone
```
**Zone file (`/etc/nsd/yourdomain.com.zone`):**
```
$ORIGIN yourdomain.com.
$TTL 3600
@ IN SOA ns1.yourdomain.com. admin.yourdomain.com. (
2024010101 ; serial
3600 ; refresh
900 ; retry
86400 ; expire
3600 ; minimum
)
@ IN NS ns1.yourdomain.com.
ns1 IN A 1.2.3.4
```
**Logging queries:** `nsd` can log all queries to syslog. Configure your syslog to capture DNS queries and pipe them to a script:
```bash
# In rsyslog config:
:programname, isequal, "nsd" /var/log/nsd-queries.log
```
Then a separate process tails this log file, extracts base64 labels, decodes them, and injects events into the relay.
### Option 2: Minimal Custom UDP Listener
You don't need a full DNS server. You can write a minimal UDP listener on port 53 that:
1. Listens for UDP datagrams on port 53
2. Parses the DNS query header to extract the question (subdomain)
3. Extracts the base64 label
4. Responds with a valid DNS response (NXDOMAIN)
5. Decodes the event and injects it into the relay
**Python example (conceptual):**
```python
import socket
import struct
import base64
def parse_dns_query(data):
"""Extract the queried domain name from a DNS query."""
# Skip DNS header (12 bytes)
pos = 12
labels = []
while True:
length = data[pos]
if length == 0:
break
pos += 1
labels.append(data[pos:pos+length].decode('ascii', errors='ignore'))
pos += length
return '.'.join(labels)
def build_nxdomain_response(data):
"""Build a DNS NXDOMAIN response for the given query."""
# Parse header
header = struct.unpack('!HHHHHH', data[:12])
query_id = header[0]
flags = 0x8183 # Response + NXDOMAIN
# Build response header + echo the question
response = struct.pack('!HHHHHH', query_id, flags, 1, 0, 0, 0)
response += data[12:12+len(data)-12] # Echo the question
return response
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 53))
while True:
data, addr = sock.recvfrom(512)
domain = parse_dns_query(data)
# Extract the first label (before yourdomain.com)
if domain.endswith('.yourdomain.com'):
label = domain.split('.')[0]
try:
# Decode base64url (add padding if needed)
padding = 4 - len(label) % 4
if padding != 4:
label += '=' * padding
event_bytes = base64.urlsafe_b64decode(label)
# Inject into relay...
except:
pass # Invalid encoding, silently drop
# Always respond with NXDOMAIN
response = build_nxdomain_response(data)
sock.sendto(response, addr)
```
**Important:** Running a UDP listener on port 53 requires root privileges. Use `setcap` to grant the binary the `CAP_NET_BIND_SERVICE` capability, or run as root and drop privileges after binding.
### Option 3: DNS Log Parser (Passive)
If you already have a DNS server running (e.g., for your website), you can simply enable query logging and parse the logs:
```bash
# Tail the DNS query log
tail -F /var/log/nsd-queries.log | while read line; do
# Extract domain from log line
domain=$(echo "$line" | grep -oP 'query: \K\S+')
if [[ "$domain" == *".yourdomain.com" ]]; then
label=$(echo "$domain" | cut -d. -f1)
# Decode and inject...
fi
done
```
---
## Domain Registration and Configuration
### Step 1: Register a Domain
Choose a domain that looks innocent. Examples:
| Domain | Looks Like | Notes |
|---|---|---|
| `cdn-pull.example` | CDN edge server | Generic infrastructure |
| `api-cache.example` | API caching layer | Generic infrastructure |
| `metrics.example` | Analytics endpoint | Generic infrastructure |
| `status.example` | Status page | Generic infrastructure |
Avoid anything that suggests Nostr, crypto, or censorship circumvention.
### Step 2: Configure Nameservers
At your domain registrar, set the nameservers to point to your server:
```
ns1.yourdomain.com → 1.2.3.4
ns2.yourdomain.com → 1.2.3.4 (or a second server for redundancy)
```
### Step 3: Set Up Glue Records
Most registrars require **glue records** — A records for the nameservers themselves. This is because the DNS system needs to know the IP of `ns1.yourdomain.com` before it can query `yourdomain.com`. The registrar handles this automatically when you specify the nameserver IPs.
### Step 4: Wait for Propagation
DNS changes can take 24-48 hours to propagate fully, though most resolvers update within a few hours.
---
## Security and Operational Considerations
### 1. Rate Limiting
DNS servers are exposed to the public internet. An attacker could flood your server with fake queries. Implement rate limiting:
```bash
# iptables rate limit for DNS
iptables -A INPUT -p udp --dport 53 -m limit --limit 100/s -j ACCEPT
iptables -A INPUT -p udp --dport 53 -j DROP
```
### 2. Amplification Attack Risk
DNS servers can be used for amplification attacks if they respond with large responses. Always respond with a minimal NXDOMAIN response (no additional data). Never include DNSSEC records, NS records, or other data in the response.
### 3. Log Rotation
DNS query logs can grow quickly. Implement log rotation:
```bash
# /etc/logrotate.d/nsd-queries
/var/log/nsd-queries.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
postrotate
systemctl restart rsyslog
endscript
}
```
### 4. DNSSEC
If you enable DNSSEC, your responses will be signed and verifiable. This adds legitimacy but also complexity. For a stealth relay, DNSSEC is optional — NXDOMAIN responses without DNSSEC are normal for domains that don't have DNSSEC enabled.
### 5. Firewall
Only expose port 53 (UDP). Do not expose SSH, HTTP, or any other service on the same IP if you want to maintain the appearance of a simple DNS server.
---
## Comparison with Other Approaches
| Property | Direct UDP | Bridge Pattern | Passive Sniffing | **Authoritative DNS** |
|---|---|---|---|---|
| Sender connects to relay? | Yes | Yes (via bridge) | No | **No** |
| Relay visible in traffic? | Yes | Yes (bridge IP) | No | **Yes (domain name)** |
| Content hidden? | Yes (encrypted) | Yes (encrypted) | Yes (in DNS label) | **Yes (in DNS label)** |
| Plausible deniability | None | None | High | **Moderate** |
| Sender sophistication | Low | Low (browser) | Medium | **Medium** |
| Relay sophistication | Low | Low | High (packet capture) | **Low (DNS server)** |
| Works in browser? | No | Yes | No | **No** |
| Event size limit | 1472 bytes | 1472 bytes | ~200 bytes | **~200 bytes** |
| Real-time delivery | Yes | Yes | Delayed | **Near real-time** |
| Legal risk for relay | Low | Low | Medium | **Low** |
| Needs network path access? | No | No | **Yes** | **No** |
| Needs domain name? | No | No | No | **Yes** |
---
## The Cypherpunk Angle
### 1. DNS as a Universal Transport
DNS is the one protocol that virtually no firewall blocks entirely. Blocking DNS would break the internet. This makes it an ideal censorship-resistant transport.
### 2. The Domain as a Dead Drop
The domain name functions as a **dead drop location**. Anyone who knows the domain can send events to it. The sender doesn't need to know the server's IP — the DNS system handles that.
### 3. Traffic Analysis Limitations
An observer sees: `Sender queried yourdomain.com`. This is indistinguishable from a normal DNS lookup for a website. The observer would need to:
- Inspect the full subdomain label (which may be encrypted with EDNS0)
- Know the encoding scheme
- Distinguish Nostr events from random noise
Without all three, the traffic looks normal.
### 4. Relationship to Other Approaches
| Approach | Relationship |
|---|---|
| Protocol hardening (approach 6) | DNS is a hardened protocol — it cannot be blocked |
| Steganography (approach 4) | The event is hidden inside a DNS query |
| Anonymity (approach 1) | Can be combined with Tor for sender anonymity |
| Decentralization (approach 2) | Multiple domains can point to multiple relays |
---
## Limitations
### 1. Event Size
DNS queries are limited to ~255 bytes total. This restricts the approach to small, single-packet events using the `.bne` binary format. Larger events must use a different transport.
### 2. One-Way Only
The sender sends a query and receives a DNS response (NXDOMAIN). The response cannot carry meaningful data back to the sender (it's just a DNS status code). This is a publish-only channel.
### 3. Domain Visibility
The domain name is visible in the DNS query. If an adversary maintains a list of known Nostr relay domains, they can flag queries to those domains. Using innocent-looking domains mitigates this.
### 4. No Browser Support
Browsers do not expose APIs for crafting arbitrary DNS queries. The sender needs a custom application or a browser extension that can make raw DNS queries.
### 5. Caching
DNS resolvers cache responses. If the sender queries the same subdomain twice, the second query may be served from cache and never reach your server. The sender should include a random component in each query to avoid caching:
```
<base64_event>.<random_nonce>.yourdomain.com
```
The random nonce ensures each query is unique and bypasses the cache.
---
---
## Testing from a Browser
You can test the authoritative DNS relay from a standard browser page with no special APIs. The browser performs a DNS lookup for **any hostname** you give it, and that lookup reaches your authoritative nameserver.
### Method 1: Image Pixel (Simplest)
```javascript
// Encode the event as base64url
const eventBase64 = btoa(JSON.stringify(event))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
// Trigger a DNS lookup by loading an image from the subdomain
new Image().src = `https://${eventBase64}.yourdomain.com/pixel.png`
```
The browser:
1. Extracts the hostname: `${eventBase64}.yourdomain.com`
2. Asks its DNS resolver for the IP
3. DNS resolver queries your authoritative nameserver ← YOU RECEIVE THE EVENT
4. Your server responds with NXDOMAIN or an IP
5. Browser makes HTTPS request to the IP (fails or succeeds — doesn't matter)
### Method 2: Fetch
```javascript
fetch(`https://${eventBase64}.yourdomain.com/collect`)
```
Same DNS flow. The `fetch` itself may fail (CORS, no server), but the DNS query already delivered the event.
### Method 3: Multiple Queries in Parallel (for Large Events)
```javascript
// Split a large event across multiple DNS queries
const chunks = splitIntoChunks(eventBase64, 50) // 50 bytes per chunk
const sessionId = Math.random().toString(36).slice(2)
chunks.forEach((chunk, i) => {
const subdomain = `${sessionId}.${i}.${chunk}.yourdomain.com`
new Image().src = `https://${subdomain}/pixel.png`
})
```
Each chunk triggers a separate DNS lookup. Your server reassembles them by `sessionId`.
### What the Browser Sees
The user sees nothing unusual — the page loads normally. The image loads fail silently (broken image icon if using `<img>`), or you can suppress errors by using `fetch()` with `catch()`. The DNS queries happen in the background.
### Limitation: DNS Caching
DNS resolvers cache responses. If you send the same subdomain twice, the second query may be served from cache and never reach your server. Mitigations:
- Include a random nonce in each query: `<nonce>.<event>.yourdomain.com`
- Use a unique session ID per batch of queries
- The nonce ensures each query is unique and bypasses the cache
---
## Combining Multiple Queries for Larger Events
The ~255 byte DNS query limit can be overcome by splitting a large event across multiple DNS queries and reassembling on the server.
### Protocol
Each query carries three pieces of information in the subdomain:
```
<session_id>.<sequence_number>.<chunk_data>.yourdomain.com
```
| Field | Description | Example |
|---|---|---|
| `session_id` | Random identifier for this batch of chunks | `a3f8k2` |
| `sequence_number` | Position of this chunk (0-indexed) | `0`, `1`, `2` |
| `chunk_data` | Base64url-encoded chunk of the event | `eyJjb250ZW50Ijoi...` |
### Sender Logic (JavaScript)
```javascript
async function sendLargeEvent(event, domain) {
const json = JSON.stringify(event)
const base64 = btoa(json).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
// Split into 50-byte chunks (leaves room for session_id, seq, and domain overhead)
const chunkSize = 50
const chunks = []
for (let i = 0; i < base64.length; i += chunkSize) {
chunks.push(base64.slice(i, i + chunkSize))
}
const sessionId = Math.random().toString(36).slice(2, 8)
// Fire all chunks as parallel DNS queries
const promises = chunks.map((chunk, i) => {
const subdomain = `${sessionId}.${i}.${chunk}.${domain}`
// Use Image() for fire-and-forget (no response needed)
return new Promise((resolve) => {
const img = new Image()
img.onload = img.onerror = resolve
img.src = `https://${subdomain}/pixel.png`
})
})
await Promise.all(promises)
return { sessionId, totalChunks: chunks.length }
}
```
### Receiver Logic (Server)
```python
# In-memory buffer for partial events
chunk_buffer = {} # session_id -> { total_chunks: N, chunks: {seq: data} }
def process_dns_query(domain):
# Parse: <session_id>.<seq>.<chunk>.yourdomain.com
parts = domain.split('.')
if len(parts) < 4:
return # Not our format
session_id = parts[0]
try:
seq = int(parts[1])
except ValueError:
return
chunk_data = parts[2]
# Initialize or update buffer
if session_id not in chunk_buffer:
chunk_buffer[session_id] = {}
chunk_buffer[session_id][seq] = chunk_data
# Check if we have all chunks (we don't know total yet — use timeout)
# For simplicity, assume complete after 5 seconds of no new chunks
```
### Determining Completion
The server doesn't know the total number of chunks in advance. Strategies:
| Strategy | How It Works | Tradeoff |
|---|---|---|
| **Timeout** | After receiving a chunk, wait N seconds. If no new chunks arrive, assume complete. | Simple. Adds latency. |
| **Total in first chunk** | First chunk includes total count: `<session>.0.<total_N>.<data>` | Requires special first-chunk format. |
| **Final chunk marker** | Last chunk has a special marker: `<session>.99.<data>.final` | Sender must know which chunk is last. |
| **Event ID as session** | Use the Nostr event ID as the session ID. Server knows the expected event size from the kind. | Only works for known event kinds. |
### Effective Size Limits
| Number of Queries | Total Raw Data | Total Base64 Data | Use Case |
|---|---|---|---|
| 1 | ~140 bytes | ~190 bytes | Minimal text note |
| 3 | ~420 bytes | ~570 bytes | Normal text note with tags |
| 5 | ~700 bytes | ~950 bytes | Long text note |
| 10 | ~1400 bytes | ~1900 bytes | Large event with metadata |
| 20 | ~2800 bytes | ~3800 bytes | Very large event |
With 10 DNS queries, you can send any Nostr event that fits in a single UDP packet (1472 bytes). With 20 queries, you can send events larger than a single UDP packet.
### What the Observer Sees
An observer sees 10 DNS queries to your domain in quick succession:
```
a3f8k2.0.eyJjb250ZW50IjoiSGVsbG8gV29ybGQhIn0.yourdomain.com
a3f8k2.1.Li4udGhpcyBpcyBhIGxvbmdlciB0ZXh0IG5vdGUgdGhhdCB3b3VsZ...
a3f8k2.2.G5vdCBmaXQgaW4gYSBzaW5nbGUgRFBTIHF1ZXJ5LCBzbyB3ZSBzcG...
...
```
This looks like a client resolving multiple subdomains — which is normal behavior for a web page loading resources from multiple CDN endpoints. The pattern is indistinguishable from:
- A web page loading 10 images from a CDN
- An analytics script tracking page load metrics
- A JavaScript widget making multiple API calls
### Relationship to the Bridge Pattern
The multi-query DNS approach can replace the bridge pattern entirely for browser-based sending:
```
Browser → Multiple DNS queries → Your authoritative DNS server → Event reassembly → Relay
```
No HTTP bridge needed. No UDP socket needed. The browser's built-in DNS resolver does all the work. The relay receives the fully reassembled event and serves it to subscribers via WebSocket.
This is the **most censorship-resistant browser-based approach** because:
- DNS cannot be blocked without breaking the internet
- Each individual query looks like normal web traffic
- The event is fragmented across multiple queries, making reassembly harder for an observer
- No direct connection to a known relay IP
---
## Summary
The authoritative DNS relay is a practical approach to censorship-resistant Nostr event transmission that:
1. **Requires no special network positioning** — the DNS system delivers queries to your server automatically
2. **Provides content hiding** — the event is encoded in the subdomain label
3. **Leverages existing infrastructure** — DNS is universally allowed and cannot be blocked
4. **Is testable from a browser**`new Image()` triggers a DNS lookup with no special APIs
5. **Supports large events via multi-query splitting** — 10 queries can deliver any single-packet event
6. **Works with the binary `.bne` format** — small events fit within DNS label constraints
7. **Eliminates the need for a bridge** — the browser's DNS resolver replaces the HTTP bridge entirely
The core insight: **the global DNS system is a free delivery network. Anyone can send data to your server by querying a subdomain, and the query looks like normal internet traffic.**
+157
View File
@@ -0,0 +1,157 @@
# Binary Nostr Events (.bne): Unified Binary Protocol Architecture
## 1. Overview & Motivation
In standard Nostr ([`NIP-01`](../nips/01.md:15)), an event is defined as a JSON string containing text-encoded metadata and content. Media and binary attachments (blobs) are stored on separate HTTP servers ([`BUD-01`](../blossom/buds/01.md:11), [`NIP-96`](../nips/96.md:13)), referenced via URLs inside event content or tags ([`NIP-94`](../nips/94.md:13), [`NIP-92`](../nips/92.md:7)).
This document outlines an architectural proposal for **Binary Nostr Events (`.bne`)**: a canonical, close-to-the-metal binary event envelope. By making the event structure binary at its foundation, the protocol eliminates the divide between "text events" and "binary blobs". Everything—social posts, file attachments, media, encrypted payloads, and protocol signals—becomes a binary-encoded event.
---
## 2. Binary Event Envelope Memory Layout
A Binary Event (`.bne`) is serialized into a byte sequence with fixed-width fields for fast zero-copy memory access and length-prefixed variables.
```
+---------------------------------------------------------------------------------+
| Offset | Field | Type | Size (Bytes) | Description |
+--------+----------------+--------------------+---------------+------------------+
| 0 | version | uint8 | 1 | Format version 1 |
| 1 | pubkey | byte[32] | 32 | secp256k1 key |
| 33 | created_at | uint64 (be) | 8 | Unix timestamp |
| 41 | kind | uint16 (be) | 2 | Event kind |
| 43 | tags_count | uint16 (be) | 2 | Number of tags |
| 45 | tags_bytes | variable | dynamic | TLV tag table |
| 45+T | content_length | uint32 (be) | 4 | Payload byte len |
| 49+T | content | byte[content_len] | dynamic | Raw binary body |
| 49+T+C | sig | byte[64] | 64 | Schnorr signature|
+---------------------------------------------------------------------------------+
```
### Tag Encoding (TLV)
Each tag in `tags_bytes` is encoded as:
- `element_count` (`uint8`): Number of string/byte elements in the tag array (e.g., `2` for `["e", "<id>"]`).
- For each element:
- `element_len` (`uint16`): Length of element in bytes.
- `element_data` (`byte[element_len]`): Raw string or hex bytes.
---
## 3. Cryptographic Verification & Event ID
The `id` of a Binary Event is computed by applying `SHA-256` over the canonical binary serialization of all fields **excluding** the `sig` field (offsets `0` through `49 + T + C`):
$$\text{id} = \text{SHA256}(\text{version} \parallel \text{pubkey} \parallel \text{created\_at} \parallel \text{kind} \parallel \text{tags} \parallel \text{content\_length} \parallel \text{content})$$
The `sig` field is a standard Schnorr signature over the 32-byte `id` digest using `pubkey`.
```mermaid
flowchart TD
subgraph Event Envelope Serialization
V[version: 1B]
PK[pubkey: 32B]
TS[created_at: 8B]
K[kind: 2B]
T[tags: variable]
CL[content_length: 4B]
C[content payload: variable]
S[sig: 64B]
end
V & PK & TS & K & T & CL & C -->|SHA256| ID[32-Byte Event ID]
ID & PK & S -->|Schnorr Verify| Valid{Valid Signature?}
```
---
## 4. Content Identification Strategy: Kind-Based Parsing
To maintain maximum performance without adding header flags, `.bne` parsers identify the content type using **Event Kinds** combined with **Magic Bytes**:
```
.bne Event Received
|
Check Event 'kind'
|
+---------------------+---------------------+
| |
Text Kind (0, 1, 30023...) Binary Kind (1063, 24242...)
| |
Decode content as UTF-8 Inspect content
| Magic Bytes / Header
Render text / JSON |
+----------+----------+
| |
JPEG (0xFFD8FF) PNG (0x89504E)
| |
Render Image Render Image
```
1. **Text & Protocol Kinds (`kind: 0, 1, 30023...`):**
- The `content` bytes are directly interpreted as raw UTF-8 text strings or JSON objects.
2. **Binary & Media Kinds (`kind: 1063` NIP-94 File Metadata, `kind: 24242` BUD-11...):**
- The `content` section contains raw binary payload bytes (e.g. JPEG, PNG, MP4, PDF, or encrypted bytes).
- Parsers check **magic bytes** at the start of the `content` buffer (e.g., `0xFF 0xD8 0xFF` for JPEG, `0x89 0x50 0x4E 0x47` for PNG, `0x25 0x50 0x44 0x46` for PDF) to identify media types instantly.
- Optional `m` (MIME) tags (`["m", "image/jpeg"]`) provide explicit MIME fallbacks for custom binary streams.
---
## 5. Media Polyglot Events (JPEG/EXIF, PNG Chunks, ID3)
Beyond wrapping media inside a `.bne` envelope, standard media formats can be structured as **Polyglot Files**—files that function as standard media files in operating systems while simultaneously serving as valid self-authenticated Nostr events!
```
+---------------------------------------------------------------------------+
| JPEG SOI (0xFFD8) | APP1 / EXIF Header (Nostr Metadata) | Compressed JPEG Data|
+---------------------------------------------------------------------------+
| Media Reader Sees: Valid JPEG Image |
| Nostr Relay Sees: Signed Event (Pubkey + Sig in APP1 + Image Content Hash)|
+---------------------------------------------------------------------------+
```
### Supported Media Metadata Containers
- **JPEG (`.jpg`):** Nostr event headers (`pubkey`, `created_at`, `kind`, `tags`, `sig`) are embedded inside an `APP1` (EXIF) or `COM` (Comment) segment. The signature signs the compressed image frame.
- **PNG (`.png`):** Embedded inside a custom ancillary chunk named `nOST` or standard `tEXt` / `iTXt` metadata chunks.
- **MP3 / Audio (`.mp3`):** Embedded inside an `ID3v2` frame (e.g., `GEOB` frame).
- **MP4 / WebM Video:** Embedded inside a custom `moov` / `meta` box atom.
### Advantages of Media Polyglots
- **Universal Viewability:** Double-clicking `photo.jpg` opens it in any image editor or web browser.
- **In-Band Signature Verification:** Opening `photo.jpg` in a Nostr-aware client extracts the EXIF metadata, verifies the author's Schnorr signature against the image payload, and renders author provenance directly over the media!
---
## 6. Unifying Events and Blobs
Under this model:
1. **Small & Medium Blobs as Direct Content:** Images, thumbnails, audio clips, and encrypted media are stored directly in `content`.
2. **Self-Authenticating Media:** The blob is authenticated by the event signature and addressable by event `id` or content hash tag (`["x", "<sha256>"]`).
3. **Multi-Chunk Large Blobs:** Files exceeding single event storage limits use sequential addressable events with index tags (`["chunk", "0"]`, `["chunk", "1"]`).
4. **Transport Agnosticism:** Binary events can be sent over WebSockets, TCP streams, Bluetooth LE, QUIC, UDP, or saved directly as static files (`.bne` or polyglot `.jpg`) on disk/CDNs without string escaping or Base64 encoding.
---
## 7. Compatibility & Transcoding Gateway
To allow smooth coexistence with JSON-based Nostr ([`NIP-01`](../nips/01.md:15)):
```
[ Binary Event Client ] <---> ( Binary Protocol ) <---> [ Transcoding Gateway ] <---> ( JSON Protocol ) <---> [ Legacy Relay ]
```
- **Binary (`.bne`) to JSON Transcoding:**
- `pubkey`, `id`, `sig` are converted from raw bytes to lowercase hex strings.
- `content` is UTF-8 decoded if text kind, or Base64/Data-URI encoded if binary kind.
- `tags` are unpacked into JSON string arrays.
- **Dual-Digest Mapping:**
- For native binary events, gateways add a tag `["binary_id", "<hex_id>"]` when publishing to legacy JSON relays so clients can map binary IDs to legacy JSON event IDs.
---
## 8. Performance Advantages
1. **Zero-Copy Parsing:** Relays and clients slice memory buffers directly without allocating JSON parse trees or escaping characters.
2. **Bandwidth Reduction:** Eliminates JSON structural overhead, hex encoding (50% key size reduction), and Base64 padding (33% content size reduction).
3. **Storage Efficiency:** Direct disk mmap for database indexes and fast streaming queries.
+65
View File
@@ -0,0 +1,65 @@
# Maximum Single-Packet `.bne` Event Example (`kind: 1`)
This document demonstrates the maximum size `kind: 1` Nostr event that fits into a **single 1,500-byte Ethernet IP frame** when converted to `.bne` binary format.
---
## 1. Byte Budget Breakdown
```
+-------------------------------------------------------------------------------+
| Component | Size (Bytes) |
+------------------------------------------------------+------------------------+
| Ethernet MTU Limit | 1500 bytes |
| Less IPv4 Header | - 20 bytes |
| Less UDP Header | - 8 bytes |
+------------------------------------------------------+------------------------+
| Maximum Usable UDP Datagram Size | 1472 bytes |
+------------------------------------------------------+------------------------+
| `.bne` Fixed Header (version, pubkey, created_at, | - 113 bytes |
| kind, tags_count, content_length, sig) | |
| Tag Array Overhead (`[["client", "bne-wire"]]`) | - 22 bytes |
+------------------------------------------------------+------------------------+
| NET AVAILABLE UTF-8 CONTENT CAPACITY | 1,337 bytes |
+------------------------------------------------------+------------------------+
| TOTAL `.bne` BINARY PAYLOAD SIZE | EXACTLY 1,472 BYTES |
+------------------------------------------------------+------------------------+
```
---
## 2. Equivalent JSON Event Representation
When encoded in standard Nostr JSON ([`NIP-01`](../nips/01.md:15)), this event is **1,728 bytes** (exceeding single-packet MTU due to hex expansion and JSON quotes/brackets). When transcoded to `.bne` binary format, it shrinks to **1,472 bytes**, fitting inside **1 single UDP packet**!
```json
{
"id": "426462725f7061636b65745f626f756e646172795f746573745f303030303030",
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"created_at": 1772019044,
"kind": 1,
"tags": [
["client", "bne-wire"]
],
"content": "In decentralized communication networks, the physical substrate of hardware and IP networking defines the ultimate boundaries of efficiency. Standard Ethernet frames cap Maximum Transmission Unit (MTU) size at 1500 bytes. Subtracting IPv4 headers (20 bytes) and UDP headers (8 bytes) leaves exactly 1472 bytes of unfragmented payload capacity. Standard Nostr JSON events are heavily bloated by hexadecimal text encodings: a 32-byte public key expands to 64 ASCII hex bytes, and a 64-byte Schnorr signature expands to 128 ASCII hex bytes. In addition, JSON structural syntax—double quotes, commas, brackets, and escaped newlines—consumes precious bandwidth. By converting Nostr events into canonical Binary Nostr Events (.bne), fixed header fields (pubkey, created_at, kind, content_length) and the Schnorr signature are packed as raw byte slices. The fixed overhead of a .bne envelope is precisely 113 bytes. When aligned to 64-byte CPU cache lines and single-packet IP datagram boundaries, a .bne event can carry up to 1337 bytes of pure UTF-8 content in a single packet. This enables zero-copy kernel eBPF packet routing, eliminates TCP head-of-line blocking, and allows entire social notes, voice snippets, or cryptographic proofs to travel across mesh networks, satellite links, or local UDP broadcasts in a single atomic network frame without fragmentation.",
"sig": "4b57c22b1797b109530ffe5d04cabac468b1a5942873a5141334ecbc77694fc968a1b941979ba13602fceb1dad8014ab6469c6ae9cef0b5668cc23ad1449e103"
}
```
---
## 3. Transcoded `.bne` Byte Wire Layout
```
Offset 0 [1B] : Version = 0x01
Offset 1 [32B] : Raw pubkey bytes = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Offset 33 [8B] : created_at = 0x00000000699FA87C (1772019044)
Offset 41 [2B] : kind = 0x0001
Offset 43 [2B] : tags_count = 0x0001
Offset 45 [22B] : Tag 0: ["client", "bne-wire"] TLV bytes
Offset 67 [4B] : content_length = 0x00000539 (1,337 bytes)
Offset 71 [1337B]: "In decentralized communication networks..." UTF-8 string bytes
Offset 1408[64B] : Raw Schnorr signature bytes = 4b57c22b1797b109530ffe5d04...
TOTAL WIRE SIZE = 1,472 BYTES (100% UNFRAGMENTED SINGLE IP UDP DATAGRAM)
```
+195
View File
@@ -0,0 +1,195 @@
# MTU as a Cypherpunk Tool
## What Others Have Done
Yes, many people have worked on this idea — exploiting the MTU boundary and the properties of UDP to create communication that is hard to block. Here are the most important examples.
---
## 1. QUIC / HTTP/3 (Google, IETF)
**The biggest success story.** QUIC (Quick UDP Internet Connections) was designed by Google in 2012 and is now an IETF standard (RFC 9000). It is the foundation of HTTP/3.
**What it does:** QUIC runs entirely over UDP on port 443. It is encrypted by default (TLS 1.3). It is designed to be a single-packet protocol for connection establishment — the entire handshake fits in one round trip, often in one packet.
**Why it matters for censorship resistance:** QUIC was not designed for censorship resistance, but it has become one by accident. Because QUIC traffic is:
- On port 443 (same as HTTPS)
- Encrypted (TLS 1.3)
- Over UDP (not TCP)
- Indistinguishable from random bytes
...blocking QUIC means blocking HTTP/3, which means breaking a significant and growing portion of the web. China has tried to block QUIC and largely failed because it breaks too many Google services.
**Current status:** QUIC is used by Google (YouTube, Search, Chrome), Meta (Facebook, Instagram), Cloudflare, and many CDNs. It is estimated to carry 30-50% of all internet traffic.
**Lesson:** The most effective censorship-resistant protocol is one that billions of people use for legitimate purposes. Your protocol becomes hard to block because blocking it would break the internet.
---
## 2. CurveCP / MinimaLT (Daniel J. Bernstein)
**The most explicit cypherpunk take on this idea.** Daniel J. Bernstein (djb) — the same person who created Curve25519, NaCl, and qmail — designed CurveCP specifically as a censorship-resistant transport protocol.
**What it does:** CurveCP runs over UDP. Every message is a single UDP datagram. The protocol is designed so that:
- The server has no state until the client proves they can receive packets (anti-amplification)
- The handshake is a single exchange of two packets
- Everything is encrypted and authenticated
- The protocol is minimal — the specification is a few pages
**Why it matters:** djb explicitly designed CurveCP to be hard to block. He wrote about the "TCP/IP blocking problem" — TCP connections have visible handshakes (SYN, SYN-ACK, ACK) that can be detected and blocked. UDP has no handshake. A single UDP datagram is just a blob of bytes. There is nothing to pattern-match.
**Current status:** CurveCP never saw wide adoption. It was superseded by QUIC in practice. But the ideas live on. djb's design principles (minimal, single-packet, encrypted-by-default, no handshake pattern) are the blueprint for protocol hardening.
**Key paper:** "MinimaLT: Minimal-latency Networking Through Better Security" (2013) by djb et al.
---
## 3. obfs4 (Tor Project, Yawning Angel)
**Making Tor traffic look like nothing in particular.** obfs4 is the fourth generation of Tor's pluggable transport system. It is designed to make Tor traffic indistinguishable from random bytes.
**What it does:** obfs4 uses the NTOR handshake (a cryptographic protocol) over a custom transport that:
- Uses the IAT (Inter-Arrival Time) obfuscation technique to make packet timing look random
- Encrypts everything with a key derived from a shared secret
- Can run on any port, but commonly port 443
- Uses a "scramble suit" that makes the traffic pass statistical tests for randomness
**Why it matters:** obfs4 is the most widely deployed censorship circumvention tool after VPNs. It is used by millions of people in China, Iran, Russia, and other censored countries. It works because the adversary cannot distinguish obfs4 traffic from random noise without the key.
**Current status:** obfs4 is the default pluggable transport in Tor Browser. It is actively maintained. China has tried to block it using active probing (connecting to a suspected obfs4 bridge and seeing if it responds with a valid obfs4 handshake), which has led to a cat-and-mouse game of obfs4 vs. active probing.
**Lesson:** obfs4 shows that making traffic look like random noise is effective, but the adversary can use active probing to find bridges. This is why protocol hardening needs to be combined with anonymity (the bridge's IP address must be hidden).
---
## 4. DNS Tunneling (Many Implementations)
**Hiding data in the one protocol that cannot be blocked.** DNS is essential internet infrastructure. Without it, the internet does not work. This makes it an ideal carrier for covert communication.
**What it does:** DNS tunneling encodes data in DNS queries and responses. A client sends a DNS query for a domain like `data1234.evil.com`. The resolver (controlled by the attacker) extracts the data from the query and sends back a response with more data encoded in the response.
**Why it matters:** DNS is almost never blocked. It runs on UDP port 53. It is processed by every network device. Blocking DNS breaks everything. DNS tunneling is slow and detectable with statistical analysis, but it is extremely reliable.
**Current status:** DNS tunneling is used by malware for command-and-control, by censorship circumvention tools (like DNSCat, Iodine, OzymanDNS), and by some VPN protocols. It is well-known to defenders, who can detect it by analyzing DNS query patterns (unusual domain lengths, high query frequency, unusual TLDs).
**Lesson:** DNS tunneling shows the power of hiding in essential infrastructure, but also the limits — the adversary can detect anomalies in the pattern of queries even if they cannot read the content.
---
## 5. uTP / BitTorrent (LSD, Arvid Norberg)
**Making your protocol look like friendly background noise.** uTP (Micro Transport Protocol) was designed for BitTorrent to be "internet-friendly" — it yields to other traffic. But it also has censorship resistance properties.
**What it does:** uTP runs over UDP. It uses delay-based congestion control (not loss-based like TCP). This means it looks like random UDP traffic to a DPI box. There is no TCP handshake, no sequence numbers in the clear, no window scaling.
**Why it matters:** BitTorrent traffic is frequently throttled or blocked by ISPs. uTP was designed partly to evade this throttling. By running over UDP and looking like random traffic, uTP makes it harder for ISPs to identify and throttle BitTorrent traffic.
**Current status:** uTP is the default transport for modern BitTorrent clients (uTorrent, Transmission, libtorrent). It has been successful at evading ISP throttling, but not at evading deep packet inspection by sophisticated adversaries.
---
## 6. WireGuard (Jason A. Donenfeld)
**The minimal VPN.** WireGuard is a VPN protocol designed to be simple, fast, and hard to block.
**What it does:** WireGuard runs over UDP. It uses a single port (usually 51820, but can be any port). Every packet is encrypted and authenticated. There is no handshake visible to an observer — the first packet already contains encrypted data. The protocol is ~4000 lines of code (compared to ~400,000 for OpenVPN).
**Why it matters:** WireGuard's minimalism makes it hard to fingerprint. There is no TLS handshake, no certificate exchange, no negotiation. The packets are just encrypted blobs. An adversary cannot distinguish WireGuard traffic from random UDP traffic without the key.
**Current status:** WireGuard is included in the Linux kernel (5.6+). It is used by many VPN providers (Mullvad, IVPN, ProtonVPN). It is being standardized by the IETF. It is not designed specifically for censorship resistance, but its minimalism makes it naturally hard to block.
**Lesson:** WireGuard shows that simplicity is a censorship resistance feature. The fewer protocol features you have, the fewer patterns the adversary can fingerprint.
---
## 7. CoAP / DTLS (IETF)
**Protocols for constrained devices.** CoAP (Constrained Application Protocol) is designed for IoT devices with limited memory and bandwidth. It runs over UDP.
**What it does:** CoAP is essentially HTTP for UDP. It uses a simple binary format, supports multicast, and can be encrypted with DTLS (Datagram TLS). Messages are designed to fit in single UDP datagrams.
**Why it matters:** CoAP shows that the single-datagram design is not just for censorship resistance — it is also the natural design for constrained environments. This means censorship-resistant protocols can piggyback on legitimate IoT traffic.
**Current status:** CoAP is used in IoT, smart home, and industrial automation. It is not widely used for censorship resistance, but the protocol design is compatible with the approach.
---
## 8. The Binary Events Protocol (This Repo)
**Your own work in `rethinking_nostr/`.** The binary events protocol (`.bne` files) is designed to fit in single UDP datagrams under the MTU limit.
**What it does:** The protocol uses a fixed binary header (113 bytes) with fields aligned to CPU cache lines (64 bytes). The total event fits in 1472 bytes, meaning it can be sent as a single unfragmented UDP datagram. The header includes the kind, pubkey, and signature at fixed byte offsets, allowing kernel-level eBPF programs to filter and verify events at wire speed.
**Why it matters:** This is a concrete implementation of the protocol hardening approach. It combines:
- MTU boundary exploitation (single datagram)
- CPU cache line alignment (64-byte boundaries for Schnorr signatures)
- Kernel-level processing (eBPF packet filtering)
- Content-addressed storage (filesystem deduplication)
**Current status:** In development. The design documents in `rethinking_nostr/` lay out the substrate constraints and the protocol architecture.
---
## 9. IP Fragmentation Attacks (Various)
**Using fragmentation to bypass DPI.** If a packet is fragmented (split into multiple pieces), some DPI systems cannot reassemble and inspect it in time.
**What it does:** The attacker sends a message as multiple IP fragments. The first fragment contains the header, and subsequent fragments contain the payload. Some DPI systems only inspect the first fragment and pass the rest without inspection. By carefully crafting fragments, the attacker can hide data in later fragments.
**Why it matters:** Fragmentation attacks show that even within the IP layer itself, there are substrate properties that can be exploited. The adversary must either reassemble all fragments (expensive) or risk missing data hidden in fragments.
**Current status:** Fragmentation attacks are well-known and most modern DPI systems handle them. But they still work against older or simpler systems.
---
## 10. tcpcrypt (IETF)
**Encrypting TCP itself.** tcpcrypt is a protocol that adds encryption to TCP at the transport layer, without requiring changes to applications.
**What it does:** tcpcrypt adds a cryptographic handshake to TCP connection setup. The handshake is designed to be indistinguishable from a normal TCP handshake to a passive observer. The encryption is transparent to applications — they just see a normal TCP connection.
**Why it matters:** tcpcrypt shows that even TCP — the most inspected protocol on the internet — can be hardened. If all TCP connections were encrypted by default, the adversary would have to inspect every TCP connection to find the ones they want to block.
**Current status:** tcpcrypt never saw wide adoption. It was standardized as RFC 8548 but is not widely deployed. The lesson is that protocol hardening works best when it is adopted by mainstream applications, not just by cypherpunk tools.
---
## Summary Table
| Protocol | Transport | Key Technique | Censorship Resistance | Adoption |
|---|---|---|---|---|
| QUIC/HTTP3 | UDP port 443 | Encrypted by default, looks like random bytes | High (accidental) | Very high (30-50% of web traffic) |
| CurveCP | UDP | Single-packet handshake, minimal design | High (intentional) | None (superseded by QUIC) |
| obfs4 | TCP (any port) | Random-looking traffic, IAT obfuscation | High (intentional) | High (Tor Browser default) |
| DNS tunneling | UDP port 53 | Hide in essential infrastructure | Medium (detectable) | Low (malware, circumvention) |
| uTP | UDP | Delay-based congestion control, no handshake | Low (evades throttling) | High (BitTorrent default) |
| WireGuard | UDP (any port) | Minimal protocol, no visible handshake | Medium (accidental) | Growing (Linux kernel, VPNs) |
| Binary events | UDP | MTU-bound, cache-line-aligned, eBPF | High (intentional) | In development |
| IP fragmentation | IP | Hide data in fragments | Low (known to defenders) | N/A (attack technique) |
| tcpcrypt | TCP | Encrypt TCP handshake | Low (not deployed) | None |
---
## The Pattern
Looking at all these examples, a clear pattern emerges:
**The most successful censorship-resistant protocols are not designed for censorship resistance.** They are designed for performance (QUIC), simplicity (WireGuard), or privacy (obfs4). The censorship resistance is a side effect of good design.
The protocols that *are* designed for censorship resistance (CurveCP, binary events) have low adoption. The protocols that *accidentally* provide censorship resistance (QUIC, WireGuard) have high adoption.
This suggests a strategy: **design protocols that are better than the alternatives for legitimate reasons, and let the censorship resistance be a free benefit.** If your protocol is faster, simpler, and more private than TCP, people will use it for normal things. Once billions of people use it, it becomes impossible to block.
---
## Open Questions
1. **Can we design a protocol that is explicitly for censorship resistance but also useful for normal things?** QUIC succeeded because it was faster, not because it was censorship-resistant. Can we find another performance advantage that also provides censorship resistance?
2. **What is the next MTU?** The 1500-byte MTU is a legacy of 1980s Ethernet. Jumbo frames (9000 bytes) exist but are not universal. Is there a new substrate boundary we can exploit? IPv6? 5G? Starlink?
3. **Can we make censorship-resistant protocols that piggyback on QUIC?** QUIC is already deployed at massive scale. Can we design a protocol that looks like QUIC but is actually something else? This is the obfs4 approach applied to QUIC.
4. **What about the application layer?** Most censorship happens at the application layer (blocking specific websites, keywords, or content). Protocol hardening at the transport layer does not help if the adversary is blocking by domain name or content hash. How do we combine transport-layer hardening with application-layer evasion?
5. **Is there a thermodynamic limit to DPI?** Deep packet inspection requires energy proportional to the amount of traffic inspected. As internet traffic grows exponentially, the energy cost of total surveillance grows exponentially. Is there a point where DPI becomes thermodynamically impossible? This is the question from `first_thoughts.md` — the "thermodynamic cost of tyranny."
+155
View File
@@ -0,0 +1,155 @@
# The No-Handshake Property of Nostr Events
## The Core Insight
A Nostr event is **self-validating**. It contains everything needed to verify its authenticity:
- **Pubkey** — who claims to have sent it
- **Signature** — cryptographic proof that the pubkey's owner authorized this exact content
- **Content** — the message itself (possibly encrypted)
- **Timestamp** — when it was created
- **Kind** — what type of event it is
No prior relationship is needed between the sender and the relay. No handshake. No session. No connection state. The relay can receive the event, verify the signature, and decide whether to accept it — all without ever having communicated with the sender before.
This is different from almost every other protocol on the internet.
---
## What Other Protocols Require
| Protocol | Handshake Required | Visible Pattern | State Created |
|---|---|---|---|
| **TCP** | 3-way handshake (SYN, SYN-ACK, ACK) | Yes — unmistakable | Yes — connection state on both sides |
| **TLS** | Multiple round trips (ClientHello, ServerHello, cert, key exchange, Finished) | Yes — certificate, cipher suites, etc. | Yes — session state |
| **QUIC** | 1-RTT handshake (Initial, Handshake packets) | Partially — Initial packet has recognizable structure | Yes — connection state |
| **WireGuard** | 1-RTT handshake (initiation, response) | Minimal — but still a recognizable exchange | Yes — session state |
| **CurveCP** | 2-packet handshake (cookie, server response) | Minimal — but still an exchange | Yes — server must track cookies |
| **HTTP** | Requires TCP + TLS first | Yes — full handshake stack | Yes — connection + session |
| **Nostr event (over UDP)** | **None** | **No pattern at all** | **None — stateless** |
Every other protocol requires some kind of handshake to establish a connection, exchange keys, or negotiate parameters. Nostr events need none of this because the event is **already signed** before it is sent. The signature replaces the handshake.
---
## Why This Helps
### 1. No Handshake Pattern to Fingerprint
A TCP handshake is unmistakable: SYN, SYN-ACK, ACK. A QUIC Initial packet has a recognizable structure (version, connection ID, TLS ClientHello). Even minimal handshakes have patterns that can be detected by deep packet inspection.
A single Nostr event as a UDP datagram has **no handshake pattern**. It is just a blob of bytes. The adversary cannot distinguish it from any other UDP traffic without decrypting it.
### 2. No Active Probing Vulnerability
This is the most important advantage.
**obfs4's weakness:** obfs4 bridges are vulnerable to **active probing**. The adversary connects to a suspected bridge and sends data. If the server responds with a valid obfs4 handshake, the adversary knows it is a bridge. This is how China blocks many Tor bridges.
**Nostr's strength:** With a no-handshake protocol, the adversary can send a fake event to a suspected relay. The relay will try to verify the signature, fail (because the event is invalid), and either drop the packet or send back an error. But there is no handshake to distinguish. The relay's response to an invalid event looks the same as its response to random noise. The adversary cannot tell if the relay is a Nostr relay or just a server that happens to be listening on that port.
The adversary's active probing tools are designed to detect **protocols that respond to a probe with a recognizable handshake**. A no-handshake protocol does not respond with a handshake. It responds with nothing (if the event is invalid) or with a simple acknowledgment (if the event is valid). Neither response is distinguishable from any other UDP service.
### 3. Fire-and-Forget
The sender does not need to wait for a response. The event is sent as a single UDP datagram. If it arrives, the relay processes it. If it doesn't arrive, the sender may never know — but for many use cases (broadcasts, ephemeral messages, signaling), this is acceptable.
This is ideal for UDP because UDP is inherently fire-and-forget. There is no retransmission, no acknowledgment, no flow control. The protocol matches the transport.
### 4. No Amplification Attack Vector
Amplification attacks (like DNS amplification, NTP amplification) work because a small request triggers a large response. The attacker spoofs the source IP and sends a small query, and the server sends a large response to the victim.
Nostr events cannot be used for amplification because:
- The event is already the largest thing being sent
- The response (if any) is smaller than the event (a simple acknowledgment or error)
- The signature verification happens before any response is sent, so the relay can drop invalid events without responding at all
### 5. Stateless Relays
Because no handshake is needed, the relay does not need to maintain any state about the sender. It receives a UDP datagram, verifies the signature, stores the event, and forgets the sender. This makes relays simple, scalable, and resistant to state-exhaustion attacks (like SYN floods).
### 6. Hard to Distinguish from Noise
A single UDP datagram containing an encrypted Nostr event is indistinguishable from random bytes to a passive observer. The adversary sees:
- A UDP packet on some port
- 100-1400 bytes of seemingly random data
- No recognizable protocol structure
- No handshake
- No plaintext headers (if the event is encrypted)
This is the steganography approach combined with protocol hardening. The message is hidden in the noise of the network.
---
## The Asymmetry
The adversary's problem is now much harder:
| Adversary Action | Cost | Effectiveness |
|---|---|---|
| **Passive observation** | Low — just watch the network | Low — cannot distinguish events from noise |
| **Deep packet inspection** | High — must inspect every UDP packet | Low — cannot read encrypted events |
| **Active probing** | Medium — send probes to suspected relays | Low — no handshake to detect |
| **Block all UDP** | Destructive — breaks the internet | High — but destroys the network |
| **Block specific ports** | Medium — breaks legitimate services | Low — events can use any port |
| **Statistical analysis** | High — must analyze traffic patterns | Medium — can detect unusual volumes, but not content |
The adversary's best option is statistical analysis (detecting unusual traffic volumes or patterns), but this is expensive and imprecise. It cannot determine the content of the communication, only that *some* communication is happening.
---
## The Tradeoffs
### No Delivery Guarantee
UDP does not guarantee delivery. Packets can be dropped, reordered, or duplicated. The sender does not know if the event arrived.
**Mitigations:**
- For important events, the sender can send multiple copies
- The sender can request an acknowledgment (a separate UDP packet back)
- The sender can use a higher-level protocol (like a simple ACK scheme) on top of UDP
- For broadcast events, delivery is not critical — the event is a signal, not a conversation
### No Congestion Control
UDP does not have built-in congestion control. A sender could flood a relay with events.
**Mitigations:**
- The relay can rate-limit by pubkey (using a simple counter per pubkey)
- The relay can rate-limit by IP address
- The event itself contains a timestamp, so the relay can reject events that are too old or too frequent
- Proof-of-work (Nostr's NIP-13) can be required for events, making flooding expensive
### Signature Verification Cost
The relay must verify the Schnorr signature on every event. This is computationally expensive compared to just accepting a TCP connection.
**Mitigations:**
- Schnorr signature verification is fast (~microseconds on modern hardware)
- The relay can use SIMD/AVX vector instructions for batch verification
- The relay can use eBPF to filter events at the kernel level before signature verification
- The relay can reject events based on other criteria (kind, pubkey, timestamp) before verifying the signature
### Large Events
Events larger than 1472 bytes cannot fit in a single UDP datagram. They would need fragmentation, which adds complexity and creates a pattern that can be fingerprinted.
**Mitigations:**
- Most Nostr events are small (text notes, reactions, metadata) and fit easily in 1472 bytes
- Large events (files, images) can use a separate transport (TCP, or a reference in the event)
- The protocol can define a "large event" type that is sent over TCP while small events use UDP
---
## The Bigger Picture
The no-handshake property is not just a technical detail. It is a **fundamental asymmetry** between Nostr and every other protocol.
Every other protocol requires the sender and receiver to **agree to communicate** before they can communicate. This agreement leaves traces — handshake packets, connection state, session keys. These traces can be detected, fingerprinted, and blocked.
Nostr events require no agreement. The sender creates the event, signs it, and sends it. The relay receives it, verifies it, and stores it. No prior relationship. No negotiation. No traces of an agreement.
This is the cypherpunk ideal: **communication without permission, without negotiation, without trace.**
The handshake is the point of vulnerability in most protocols. Nostr eliminates the handshake entirely. The signature replaces it. And a signature, unlike a handshake, is indistinguishable from random noise.
+423
View File
@@ -0,0 +1,423 @@
# Passive Sniffing Relay — Plausible Deniability for Nostr Event Transmission
## The Core Idea
A relay that **overhears** Nostr events from traffic passing by, rather than being the explicit destination of the packet. The sender's packet is addressed to an innocent destination (e.g., `8.8.8.8`), and the relay copies the event out of the traffic without stopping or modifying the packet.
```
Sender → [Internet] → Innocent Destination (e.g., 8.8.8.8:53)
Watching Relay
(copies the Nostr event
without stopping the packet)
```
The sender's claim: *"I was sending a DNS query to Google. I have no idea what that Nostr relay is talking about."*
---
## Why This Matters
### The Problem with Direct UDP
In the standard UDP Nostr model:
```
Sender → UDP datagram → Relay (port 443)
```
An observer on the network path sees:
- Source IP: sender's IP
- Destination IP: relay's IP
- Protocol: UDP
- Port: 443
Even if the content is encrypted, the observer knows the sender is talking to a Nostr relay. In an authoritarian regime, that's enough to flag the user.
### The Passive Sniffing Inversion
```
Sender → UDP datagram → 8.8.8.8:53 (DNS)
Watching Relay
(somewhere on the path)
```
An observer sees:
- Source IP: sender's IP
- Destination IP: 8.8.8.8
- Protocol: UDP
- Port: 53 (DNS)
This is indistinguishable from a normal DNS query. The relay is invisible in the traffic — it's not a destination, it's a **passive observer** on the path.
---
## How to Embed the Event
### DNS Query Encoding
A DNS query for a subdomain like:
```
<base64_event>.nostr.example.com
```
The sender crafts a DNS query where one of the labels contains the base64-encoded Nostr event. The watching relay, which is sniffing DNS traffic on the path, extracts the event.
#### DNS Label Constraints
| Constraint | Value | Impact |
|---|---|---|
| Max label length | 63 bytes | Event must fit in 63 bytes per label |
| Max total query length | ~255 bytes | Total encoded event + domain overhead |
| Character set | alphanumeric + hyphen | Base64url encoding required |
#### Binary Event Fit
Using the `.bne` binary event format from [`max_single_packet_event.md`](../rethinking_nostr/max_single_packet_event.md):
| Format | Event Size | Base64 Size | Fits in DNS? |
|---|---|---|---|
| JSON (kind 1) | ~400 bytes | ~533 bytes | No — exceeds 255 byte limit |
| Binary `.bne` (kind 1) | ~200 bytes | ~267 bytes | Borderline — may exceed 255 bytes |
| Minimal `.bne` (no tags, short content) | ~120 bytes | ~160 bytes | **Yes — fits comfortably** |
A minimal binary event with a short message (e.g., 40 characters of content) fits in ~120 bytes raw, ~160 bytes base64-encoded. This leaves ~95 bytes for the domain name overhead.
#### Splitting Across Multiple Labels
If the event is too large for a single label, it can be split across multiple labels:
```
<part1>.<part2>.<part3>.nostr.example.com
```
Each label can hold up to 63 bytes. Three labels give ~189 bytes of base64 data, which is ~141 bytes raw — enough for most single-packet events.
### Alternative: EDNS0 Option
EDNS0 (Extended DNS) allows custom options in DNS packets. A Nostr event could be placed in a custom EDNS0 option, making the query look like it's from a resolver that supports some exotic extension. This is more隐蔽 but requires the relay to understand EDNS0 parsing.
### Alternative: CoAP / QUIC / Custom UDP
DNS is the most natural choice because it's universally allowed and looks innocent. But any UDP protocol could work:
| Protocol | Port | Deniability | Complexity |
|---|---|---|---|
| DNS | 53 | Excellent — every device makes DNS queries | Low — simple query format |
| QUIC/HTTP3 | 443 | Good — looks like web traffic | High — requires TLS handshake |
| NTP | 123 | Good — every device syncs time | Medium — need valid NTP format |
| CoAP | 5683 | Moderate — IoT protocol | Medium |
| Custom | Any | Poor — unknown protocol stands out | Low |
---
## Two DNS Models: Passive Sniffing vs. Authoritative Server
There are two fundamentally different ways to receive data via DNS. The document above describes **Model A**, but **Model B** is simpler and more practical.
### Model A: Passive Sniffing (The Document Above)
```
Sender → DNS query to 8.8.8.8 → [Watching Relay sniffs the query] → 8.8.8.8 responds normally
```
- **You do NOT own the DNS endpoint.** The query is addressed to someone else (e.g., `8.8.8.8`).
- **The relay is a passive observer** on the network path between sender and `8.8.8.8`.
- The relay uses `libpcap` or `AF_PACKET` sockets to capture packets it wasn't addressed to.
- **No DNS server needed.** The relay just watches traffic go by.
- **Constraint:** The relay must be physically on the network path. This is the hard part.
### Model B: Authoritative DNS Server (Simpler)
```
Sender → Sender's DNS resolver (8.8.8.8, 1.1.1.1, etc.)
↓ "What is the IP of <base64_event>.yourdomain.com?"
Root DNS servers → TLD servers
↓ "yourdomain.com is managed by ns1.yourdomain.com at 1.2.3.4"
Sender's DNS resolver
↓ "Hey 1.2.3.4, what is the IP of <base64_event>.yourdomain.com?"
Your authoritative nameserver (1.2.3.4) ← YOU RECEIVE THE QUERY HERE
↓ "I don't have that record. NXDOMAIN."
Sender's DNS resolver → Sender
```
- **You DO own the DNS endpoint.** You run an authoritative nameserver for `yourdomain.com`.
- The sender queries a subdomain like `<base64_event>.yourdomain.com`.
- Your DNS server receives the query, extracts the event from the subdomain label, and responds with whatever it wants (NXDOMAIN, a fake IP, etc.).
- **No packet capture needed.** The DNS server receives the query normally through the standard DNS resolution chain.
- **You do NOT need to control the sender's DNS resolver.** The global DNS system automatically routes the query to your authoritative nameserver, regardless of which resolver the sender uses (8.8.8.8, 1.1.1.1, ISP's DNS, etc.).
- **Constraint:** The sender's DNS query goes to your nameserver, which is a known destination. Traffic analysis can see the sender is querying your domain. But the content is hidden in the subdomain label.
### Comparison
| Property | Model A: Passive Sniffing | Model B: Authoritative DNS |
|---|---|---|
| Relay needs to be on path? | **Yes** — must overhear traffic | **No** — receives queries normally via DNS chain |
| Destination visible to observer | `8.8.8.8` (innocent) | `yourdomain.com` (your domain) |
| Plausible deniability | **High** — looks like normal DNS to Google | **Moderate** — querying a specific domain |
| Legal risk | **Medium** — packet capture laws | **Low** — running a DNS server is normal |
| Reliability | **Low** — depends on being on the path | **High** — query arrives at your server guaranteed |
| Complexity | **High** — packet capture, path positioning | **Low** — just run a DNS server |
| Sender needs | Custom DNS query tool | Custom DNS query tool |
| Relay needs | Root/raw socket access | Standard DNS server software (nsd, bind) |
### What You Need for Model B
1. **A domain name** (e.g., `nostr-relay.example`)
2. **A server with a public IP** running DNS software (e.g., `nsd`, `bind`, or a custom UDP listener on port 53)
3. **Domain registrar configuration** pointing `nostr-relay.example` to your server's IP
4. **A script** that polls the DNS logs, extracts base64 events from subdomain labels, decodes them, and injects them into the relay
The sender can be on any network, using any DNS resolver, anywhere in the world. The query will find your server through the standard DNS resolution chain.
---
## Tor Compatibility
### Can You Send a Single UDP Packet Over Tor?
**No, not directly.** Tor is TCP-only. The Tor network transports data as TCP streams, not UDP datagrams. There is no UDP support in the Tor protocol.
Tor works by building a **circuit** of encrypted TCP connections:
```
Your client → Entry node (TCP) → Middle node (TCP) → Exit node (TCP) → Destination (TCP)
```
Each hop is a TCP connection. The data is stream-oriented. There is no concept of a datagram boundary.
### What You Can Do Instead
| Method | Works? | Notes |
|---|---|---|
| Raw UDP over Tor | **No** | Tor protocol doesn't support it |
| TCP over Tor, then UDP from exit | **Yes** | Run a bridge on the exit side |
| UDP over TCP tunnel over Tor | **Yes** | Encapsulate UDP in TCP, send over Tor |
| Tor + Bridge Server | **Yes** | See below |
### The Bridge Pattern Over Tor
This is the most practical approach for sending UDP over Tor:
```
Sender → Tor (TCP) → Your Bridge Server (exit side) → UDP datagram → Relay
```
1. Sender connects to your bridge server via Tor (TCP connection)
2. Bridge server receives the Nostr event over the Tor TCP stream
3. Bridge server fires a UDP datagram to the relay
4. Relay receives the UDP datagram normally
**The relay sees the bridge server's IP, not the sender's.** The sender's IP is hidden by Tor.
### Passive Sniffing (Model A) + Tor
The passive sniffing model **does not work over Tor** because:
1. Tor encrypts the entire DNS query inside the circuit
2. The exit node decrypts it and sends the actual DNS query
3. The sniffing relay would need to be on the path between the **exit node** and `8.8.8.8`
4. You don't control where the exit node is — it's randomly chosen by the Tor network
So passive sniffing only works for **non-Tor traffic** where you can position yourself on the path.
### Authoritative DNS (Model B) + Tor
This **does work**:
```
Sender → Tor → DNS query for <event>.yourdomain.com → Your DNS server
```
The sender's DNS query goes through Tor, exits at a random exit node, and reaches your DNS server. Your DNS server sees the exit node's IP, not the sender's.
**Caveat:** DNS over Tor is unusual. Most people don't route DNS through Tor. It may stand out to an observer who knows what to look for. However, the content of the query (the event in the subdomain) remains hidden.
---
## How to Get on the Path
This is the fundamental challenge. The relay must be positioned somewhere between the sender and the innocent destination.
### 1. Public Wi-Fi Hotspot (Most Practical)
Run a free Wi-Fi hotspot. All DNS traffic from connected users passes through the hotspot's router.
```
User's laptop → Wi-Fi Router (your relay) → ISP → 8.8.8.8
```
**Plausible deniability for the relay operator:** *"I just run a free Wi-Fi hotspot. I don't inspect traffic."*
**Plausible deniability for the sender:** *"I was just using the free Wi-Fi at the coffee shop."*
**Advantage:** No technical sophistication required from the sender — they just connect to Wi-Fi and use Nostr normally.
**Disadvantage:** Limited geographic range. Only works for users physically near the hotspot.
### 2. Self-Hosted Innocent Server
The sender runs a server that pretends to be something innocent (a personal blog, a file host, a game server). The server also runs a packet capture that watches for Nostr events in incoming traffic.
```
Sender → Sender's own server (port 443, HTTPS) → (event extracted, forwarded to relay)
Responds with innocent content (blog page, file, etc.)
```
The sender sends UDP packets to their own server, which:
1. Responds with whatever the innocent service would respond with
2. Silently extracts the Nostr event and injects it into the relay network
**Advantage:** Full control over the sniffing point. No legal risk.
**Disadvantage:** The sender's server is a known destination. Traffic analysis could correlate the sender with their server.
### 3. BGP Peering / AS Operation (Impractical for Individuals)
Run your own autonomous system (AS) and peer with an ISP. All traffic passing through your network is visible.
**Advantage:** Can see traffic from many users.
**Disadvantage:** Extremely expensive and complex. Requires physical infrastructure.
### 4. Compromised Router (Illegal)
Gain access to a router on the path and install packet capture software.
**Advantage:** Can see traffic from many users.
**Disadvantage:** Illegal. High risk. Ethically problematic.
### 5. Physical Tap (Illegal)
Physically tap a fiber optic cable or network switch.
**Advantage:** Can see all traffic on that link.
**Disadvantage:** Illegal. Requires physical access. High risk.
---
## Comparison with Traditional Relay Architectures
| Property | Direct UDP | Bridge Pattern | Passive Sniffing |
|---|---|---|---|
| Sender connects to relay? | Yes | Yes (via bridge) | **No** |
| Relay visible in traffic? | Yes | Yes (bridge IP) | **No** |
| Plausible deniability | None | None | **High** |
| Sender sophistication | Low | Low (browser) | Low (Wi-Fi) or Medium (self-host) |
| Relay sophistication | Low | Low | Medium (packet capture) |
| Works in browser? | No | Yes | **No** (needs raw socket) |
| Event size limit | 1472 bytes | 1472 bytes | ~200 bytes (DNS constraint) |
| Real-time delivery | Yes | Yes | **Delayed** (depends on sniffing window) |
| Legal risk for relay | Low | Low | **Medium** (packet capture laws) |
---
## The Cypherpunk Implications
### 1. Inversion of the Connection Model
Every existing communication system requires the sender to explicitly address the recipient. The passive sniffing relay **inverts** this — the recipient finds the sender's message in the noise of the internet.
This is analogous to dead drops in physical espionage: an agent leaves a message in a pre-arranged location, and another agent retrieves it later. No direct handoff.
### 2. Traffic Analysis Resistance
Even if an adversary can observe all traffic to and from the sender, they cannot determine which packets contain Nostr events. Every packet is addressed to an innocent destination. The adversary would need to:
- Decrypt the content (if encrypted)
- Know the encoding scheme
- Distinguish Nostr events from random noise
Without all three, the traffic is indistinguishable from normal internet activity.
### 3. The Wi-Fi Hotspot as a Cypherpunk Tool
A free Wi-Fi hotspot is a natural censorship circumvention tool:
- Anyone can use it without suspicion
- Traffic is mixed with all other users' traffic
- The operator has plausible deniability
- No registration or identity required
- Can be mobile (a phone hotspot) or fixed
A network of such hotspots, each running a passive sniffing relay, creates a **mesh of dead drops** — users can publish Nostr events from any hotspot without ever connecting to a known relay.
### 4. Relationship to Protocol Hardening
This approach is a form of **protocol substrate hardening** (approach 6 from [`2Approaches.md`](../2Approaches.md)) — it makes the transport medium itself resistant to censorship by hiding the communication inside a protocol that cannot be blocked (DNS).
It also incorporates **steganography** (approach 4) — the existence of the communication is hidden within innocent-looking DNS traffic.
---
## Open Questions
### 1. Reliability
DNS queries are not guaranteed to pass through any particular path. How does the sender know the relay will see the query?
- **Answer:** They don't. This is a fire-and-forget model, like UDP itself. The sender publishes and hopes a relay picks it up. Multiple relays on multiple paths increase the probability.
### 2. Latency
DNS queries are resolved quickly (milliseconds). The relay must capture the packet in real-time. Is this feasible?
- **Answer:** Yes, with `libpcap` or `AF_PACKET` sockets. The relay doesn't need to store all traffic — it can filter for DNS queries matching the expected pattern and ignore everything else.
### 3. Legal Risk
Packet capture without consent is illegal in many jurisdictions. How does the relay operator avoid legal liability?
- **Answer:** The Wi-Fi hotspot model provides a legal framework — the operator can argue they are providing a service and not inspecting content. The DNS queries are captured transiently for the purpose of routing, and the Nostr event extraction is incidental.
### 4. Event Size
DNS queries are limited to ~255 bytes. How do you send larger events?
- **Answer:** You don't. This approach is limited to single-packet events that fit in the `.bne` binary format. Larger events must use the standard bridge pattern or direct UDP.
### 5. Bidirectional Communication
The passive sniffing model is one-way (sender → relay). How does the relay communicate back to the sender?
- **Answer:** It doesn't need to. Nostr events are self-validating. The relay stores the event and serves it to subscribers. The sender doesn't need a response.
### 6. Multiple Relays
How does the sender ensure multiple relays receive the event?
- **Answer:** The sender can send multiple DNS queries, each addressed to a different innocent destination, with the same event embedded. Each query passes through different paths and may be captured by different relays.
---
## Relationship to the Bridge Pattern
The passive sniffing relay is **not a replacement** for the bridge pattern — it's a complementary approach for different threat models:
| Scenario | Best Approach |
|---|---|
| User wants to publish from a browser | Bridge pattern (HTTP → UDP) |
| User is in a low-risk environment | Direct UDP (native app) |
| User is in a high-risk environment | Passive sniffing (DNS tunneling) |
| User wants maximum reach | All three simultaneously |
The bridge pattern solves the **browser constraint**. The passive sniffing relay solves the **traffic analysis constraint**. Together, they cover a wide range of censorship scenarios.
---
## Summary
The passive sniffing relay is a novel approach to censorship-resistant Nostr event transmission that:
1. **Inverts the connection model** — the relay finds the sender's traffic, not the other way around
2. **Provides plausible deniability** — the sender appears to be making innocent DNS queries
3. **Leverages existing infrastructure** — DNS is universally allowed and cannot be blocked
4. **Works with the binary `.bne` format** — small events fit within DNS label constraints
5. **Is deployable today** — a Wi-Fi hotspot with packet capture is simple to set up
The core insight: **if you cannot hide the destination, make the destination innocent and let the relay find you.**
+161
View File
@@ -0,0 +1,161 @@
# Protocol Substrate Hardening
## The Core Idea
Design the communication protocol so that interfering with it requires breaking something essential. The adversary faces a dilemma: either allow your communication, or break something that everyone needs.
This is different from the other approaches. Anonymity hides *where* you are. Encryption hides *what* you say. Decentralization makes *copies* unstoppable. Protocol hardening makes the *medium itself* resistant to discrimination.
---
## The Envelope Analogy
Imagine the internet is like a postal system. Messages are sent in **packets** (envelopes). The postal system has rules about what kinds of packets it will carry.
Most communication protocols use big, complicated packets that require special handling. The postal system has to open them, read them, repackage them, and send them in multiple pieces (**fragmentation**). This gives the adversary many opportunities to inspect and block them.
**Protocol hardening is the opposite.** You design your message to fit in the smallest, most standard packet possible — one that the postal system processes without even thinking about it. Your packet looks exactly like millions of other packets. The postal system handles it automatically, at full speed, without ever looking inside.
Now the adversary wants to stop your packet. Their options are:
1. **Deep packet inspection (DPI)** — Open every single packet that goes through the postal system. This is incredibly expensive. There are billions of packets every day. And if your packet is encrypted, they can't even read it without breaking the seal first.
2. **Block all packets of that size** — But that size is used for essential things: DNS lookups, VoIP calls, video game traffic, QUIC/HTTP3 connections. Stopping them would break the internet itself.
3. **Do nothing** — Your packet goes through.
---
## The Size Limit: MTU
There is a maximum size for the simplest, fastest kind of packet on the internet. It's called the **MTU — Maximum Transmission Unit**. For most networks, the MTU is **1500 bytes** (roughly 1500 characters of text). This is not an arbitrary choice — it comes from the physical hardware of the network itself. Every network cable, every router, every switch on earth is built to handle packets up to this size automatically, at full speed.
If your message fits in a single **UDP datagram** (a simple, no-frills packet) under this size limit, it gets processed at the hardware level without any special handling. No one has to read it, repackage it, or even think about it. It just goes through.
The technical breakdown: Ethernet MTU (1500) minus IP header (20 bytes) minus UDP header (8 bytes) = **1472 bytes** of usable payload. This is the magic number. If your message is 1472 bytes or less, it fits in a single, unfragmented UDP datagram.
The adversary cannot block your message without either:
- Inspecting every single packet on the network (impossibly expensive), or
- Blocking all UDP traffic (which breaks DNS, VoIP, video calls, gaming, QUIC/HTTP3, and more), or
- Letting your message through.
---
## The General Principle
**Make your message look exactly like the most common, most essential traffic on the network.** When you do this, the adversary cannot block you without blocking everything.
The internet has several natural boundaries — **substrate constraints** — that are processed automatically by hardware and operating systems:
| What It Is | Technical Term | Natural Size | Why It Matters |
|---|---|---|---|
| The smallest internet packet | **MTU** (Maximum Transmission Unit) | 1500 bytes | Fits in one piece, processed at hardware speed by every network device on earth |
| A common encrypted connection | **TCP port 443** (HTTPS) | Any size | Used by almost every secure website; blocking it breaks the web |
| A common directory lookup | **UDP port 53** (DNS) | Small packets | Used by every device to find websites; blocking it breaks the internet |
| A computer's memory chunk | **OS page** | 4096 bytes | The operating system handles it automatically via **memory-mapped I/O (mmap)** |
| A computer's fast-access unit | **CPU cache line** | 64 bytes | The processor handles it in one clock cycle; a Schnorr signature is exactly 64 bytes |
Each of these is a **discrimination boundary**. If your message fits within the boundary, the adversary must either:
- Inspect every message crossing that boundary (expensive), or
- Block all messages crossing that boundary (destructive), or
- Let your message through.
---
## Real-World Examples
### 1. Hide in Encrypted Web Traffic (HTTPS on Port 443)
Most secure websites use **TCP port 443** (HTTPS). If you make your communication look like a secure web connection, the adversary cannot block you without blocking all secure websites. They would have to inspect every secure connection on the network to find yours, which means decrypting every one of them — computationally impossible.
**Tor obfs4 bridges** exploit this. They make Tor traffic look like HTTPS traffic on port 443. The adversary cannot block the bridge without blocking all HTTPS traffic to that server.
### 2. Hide in Directory Lookups (DNS Tunneling)
Every time you visit a website, your computer looks up the address using **DNS (Domain Name System)** on **UDP port 53**. These lookups are small, fast, and essential. If you encode your message in a DNS query (**DNS tunneling**), the adversary must either:
- Inspect every DNS query (expensive, and they're now encrypted with **DNS-over-HTTPS**)
- Block all DNS lookups (breaks the internet)
- Let your message through
### 3. Hide in Video Call Traffic (QUIC/HTTP3)
Modern video calls use **QUIC** (a Google-developed protocol) which runs over **UDP on port 443**. QUIC is the foundation of **HTTP/3**, the newest version of the web protocol. If your message uses the same type of connection, the adversary cannot tell the difference without inspecting every QUIC packet on the network.
### 4. Hide in Network Diagnostics (ICMP)
**ICMP (ping)** is essential for network diagnostics. Some protocols tunnel data over ICMP echo requests. Blocking ICMP breaks network troubleshooting. The adversary must either inspect every ping packet or let them through.
### 5. Use the Operating System's Guarantees (Kernel-Level)
The operating system kernel (Linux, Windows, macOS) has built-in guarantees about how files are created and stored. These are **substrate-enforced rules** — they are not promises from your application, they are guarantees from the kernel itself:
- **Atomic creation (`O_CREAT | O_EXCL`):** The kernel guarantees that a file is either created completely or not at all. There is no in-between state. This is used for **deduplication** — if the file already exists, the creation fails, so you know you have a unique copy. To violate this, the adversary would need to modify the kernel.
- **Atomic replacement (`rename()`):** The kernel guarantees that replacing a file happens in a single, instant step. This is used for **replaceable events** (like updating your profile) — the old file is replaced atomically. To violate this, the adversary would need to modify the kernel.
- **Zero-copy transfer (`mmap` + `sendfile`):** Data can be sent from storage to the network without any software ever touching it. The hardware (**DMA — Direct Memory Access**) does it directly from the OS page cache to the network interface card. To intercept it, the adversary would need to modify the hardware itself.
These are not enforced by your application (which can be bypassed, corrupted, or replaced). They are enforced by the operating system and the hardware. The adversary cannot violate them without compromising the entire system.
---
## The Asymmetry
The adversary has a fundamental resource problem:
- **Sending a message is cheap.** A single UDP datagram costs almost nothing in energy or compute.
- **Inspecting messages is expensive.** The adversary must process every packet on the network, decide whether to block it, and do so in real time (**line speed**). The cost grows with the total bandwidth of the network.
- **Blocking all messages is destructive.** The adversary breaks the very thing they are trying to control.
This is a **cost asymmetry** similar to the computational asymmetry of encryption. The person sending the message pays a small, fixed cost. The adversary pays a cost that grows with the total size of the internet — which is enormous and growing every year.
If your message looks like legitimate traffic, the adversary's cost approaches the total bandwidth of the entire internet. This is unbounded and grows over time.
---
## The Relationship to Other Approaches
Protocol hardening is different from the other approaches:
| Approach | What it protects | How |
|---|---|---|
| Anonymity | Location of the computer | Hide where you are |
| Decentralization | Existence of the data | Make unstoppable copies |
| Encryption | Content of the message | Make it unreadable |
| Steganography | Fact of communication | Make it invisible |
| Physical transport | Network path | Don't use the network |
| **Protocol hardening** | **The medium itself** | **Make discrimination impossible** |
Protocol hardening is the only approach that makes the adversary's *discrimination* impossible, rather than hiding something from them. It forces the adversary into a dilemma where their only options are to do nothing or to break the network.
---
## Combining Protocol Hardening with Other Approaches
The strongest systems combine protocol hardening with other approaches:
- **Single UDP datagram + encryption:** The message fits in one unfragmented packet (protocol hardening) and the payload is encrypted (encryption). The adversary must inspect every UDP packet on the network to even see if it's your message, and they can't read it without the key.
- **Single UDP datagram on port 443 + encryption:** Now your traffic looks like QUIC/HTTP3 traffic. The adversary must inspect every QUIC packet on the network to find your messages. This is computationally impossible.
- **Kernel-level deduplication + content-addressed storage:** The kernel guarantees atomic file creation (`O_EXCL`). The data is stored by its **hash** (a cryptographic fingerprint). The adversary cannot corrupt the data without changing its hash, which changes the filename, which is a different file. To corrupt a specific file, the adversary must know its hash, which requires reading it first — a circular problem.
- **Single datagram + encryption + ephemeral keys:** The message is sent once, in a single UDP packet, with a key that is used once and never again (**forward secrecy**). The adversary must inspect every packet on the network in real time, decrypt it (impossible without the key), and decide whether to block it — all before the packet reaches its destination. If they fail, the message is delivered and the key is thrown away.
---
## The Limits
Protocol hardening is not a silver bullet:
1. **It requires deep expertise.** You must understand how the network, the operating system, and the hardware actually work at a deep level — kernel internals, CPU architecture, memory alignment, cache lines, MTU boundaries.
2. **It depends on the platform.** A technique that exploits Linux kernel guarantees may not work on Windows or macOS. A technique that aligns to x86-64 cache lines may not work on ARM processors.
3. **The adversary can escalate.** If the adversary controls the internet infrastructure (your ISP, the backbone routers), they can block all UDP traffic regardless of the consequences. This is destructive, but a sufficiently determined adversary may do it anyway.
4. **The adversary can attack the foundation.** If the adversary can modify the operating system (via malware, forced updates, or supply chain attacks), the kernel's guarantees are no longer reliable.
5. **It does not provide anonymity.** Protocol hardening does not hide your IP address. The adversary can still see where packets are coming from and going to. You need anonymity (TOR, I2P) for that.
Protocol hardening is best used as one layer in a multi-layer defense. It raises the cost of discrimination, forcing the adversary to use more expensive and more destructive methods, which in turn makes them easier to detect and resist.
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# Test the UDP Nostr sender/receiver locally
set -e
PORT=8891
cd "$(dirname "$0")"
# Kill any leftover process on the port
fuser -k ${PORT}/udp 2>/dev/null || true
sleep 0.5
# Start receiver in background, capture output
python3 -u udp_nostr_recv.py 0.0.0.0 $PORT > /tmp/udp_recv_test.txt 2>&1 &
RECV_PID=$!
sleep 1
# Send an event
nak event -k 1 -c "Hello via UDP Nostr!" --sec $(nak key generate) 2>/dev/null | python3 udp_nostr_send.py 127.0.0.1 $PORT
# Wait for receiver to process
sleep 3
# Show receiver output
echo ""
echo "========== RECEIVER OUTPUT =========="
cat /tmp/udp_recv_test.txt
echo "========== END =========="
# Cleanup
kill $RECV_PID 2>/dev/null || true
wait $RECV_PID 2>/dev/null || true
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* UDP Nostr Receiver (JavaScript)
*
* Listens for Nostr events as single UDP datagrams.
* No handshake required. No connection state. Just receive and print.
*
* SINGLE-PACKET DETECTION:
* The receiver cannot directly observe IP-level fragmentation the OS
* reassembles fragments before delivering to the callback. But we can
* check the datagram size against the Ethernet MTU boundary (1472 bytes).
*/
const dgram = require("dgram");
const { argv } = require("process");
// Maximum UDP payload for a single unfragmented Ethernet frame
const MAX_SINGLE_PACKET = 1472;
// ── Parse arguments ─────────────────────────────────────────────────────
const bindHost = argv[2] || "0.0.0.0";
const bindPort = parseInt(argv[3]) || 8888;
// ── Create socket ───────────────────────────────────────────────────────
const sock = dgram.createSocket("udp4");
sock.on("listening", () => {
const addr = sock.address();
console.log(`Listening for Nostr events on UDP ${addr.address}:${addr.port}`);
console.log("No handshake required. Waiting for datagrams...\n");
});
sock.on("message", (data, rinfo) => {
// ── Single-packet check ──────────────────────────────────────────────
// The callback receives exactly one UDP datagram. If the sender set the
// DF flag and the path MTU was sufficient, this arrived as one IP packet.
console.log(`Received ${data.length} bytes from ${rinfo.address}:${rinfo.port}`);
if (data.length > MAX_SINGLE_PACKET) {
console.log(` ⚠ Datagram exceeds Ethernet MTU (${MAX_SINGLE_PACKET}B)`);
console.log(" This means IP-level fragmentation occurred on the path.");
console.log(" The OS reassembled the fragments before delivery.");
} else if (data.length === MAX_SINGLE_PACKET) {
console.log(` ✓ Exactly ${MAX_SINGLE_PACKET}B — fits one Ethernet frame`);
} else {
console.log(" Smaller than max — either a smaller event or truncated");
}
// ── Parse the event ──────────────────────────────────────────────────
let event;
try {
event = JSON.parse(data.toString("utf-8"));
} catch (e) {
console.log(` Could not parse JSON: ${e.message}\n`);
return;
}
// Validate required fields
const required = ["id", "pubkey", "created_at", "kind", "tags", "content", "sig"];
const missing = required.filter((f) => !(f in event));
if (missing.length > 0) {
console.log(` Missing fields: ${missing.join(", ")}\n`);
return;
}
// Print event summary
console.log(` Kind: ${event.kind}`);
console.log(` Pubkey: ${event.pubkey.slice(0, 16)}...`);
console.log(` Created: ${event.created_at}`);
console.log(` Content: ${event.content.slice(0, 80)}`);
console.log(` Event ID: ${event.id.slice(0, 16)}...`);
console.log(` Signature: ${event.sig.slice(0, 16)}...`);
console.log(" (Signature verification skipped — assuming valid)\n");
});
sock.on("error", (err) => {
console.log(`Socket error: ${err.message}`);
sock.close();
});
// ── Bind ────────────────────────────────────────────────────────────────
sock.bind(bindPort, bindHost);
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""UDP Nostr Receiver — listens for Nostr events as single UDP datagrams.
Usage: python3 udp_nostr_recv.py [bind_host] [port]"""
import socket, sys
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((sys.argv[1] if len(sys.argv) > 1 else "0.0.0.0", int(sys.argv[2]) if len(sys.argv) > 2 else 8888))
while True:
data, addr = sock.recvfrom(65535)
print(f"{len(data)} bytes from {addr[0]}:{addr[1]}")
print(data.decode())
print()
+205
View File
@@ -0,0 +1,205 @@
# UDP Nostr Relay — Architecture Plan
## Overview
A web page where a user signs in with their Nostr key, creates a message, and sends it as a single UDP datagram to a relay. The relay validates the signature, stores the event, and serves it to subscribers via the standard Nostr WebSocket protocol.
---
## Key Constraints Discovered
### 1. Browsers Cannot Send UDP
This is the critical constraint. Browsers have **no JavaScript API** for sending raw UDP datagrams to arbitrary hosts. The only UDP-like capability is WebRTC, which is designed for peer-to-peer media streams via ICE/STUN/TURN — not for firing a single datagram at a server.
**Implication:** The web page cannot send UDP directly. It must send the signed event to a **bridge** via HTTP or WebSocket, and the bridge fires the UDP datagram to the relay.
### 2. TCP and UDP Ports Are Separate Namespaces
A server can have **both** a TCP listener and a UDP listener on the **same port number** without conflict. This is how QUIC/HTTP3 works — it's UDP on port 443 alongside the HTTPS server on TCP port 443.
**Implication:** You can run a UDP Nostr receiver on port 443 even if your web server (nginx, caddy) is already using TCP port 443. They do not interfere.
### 3. The Relay Must Serve Subscribers via WebSocket
Nostr relays communicate with subscribers via WebSocket (NIP-01). The UDP receiver is just the **ingress** path. The relay still needs a WebSocket server for subscribers to connect and receive events.
---
## Architecture
```mermaid
flowchart LR
subgraph "User's Browser"
WP[Web Page<br/>Nostr sign-in + compose]
EXT[Nostr Extension<br/>nos2x / Alby / nsec]
end
subgraph "Relay Server"
BR[Bridge<br/>HTTP endpoint<br/>receives signed event]
UDPR[UDP Receiver<br/>port 443 or 8888<br/>listens for datagrams]
VAL[Signature Validator<br/>verifies event sig]
STORE[Event Store<br/>in-memory array]
WS[WebSocket Server<br/>port 8008<br/>Nostr relay protocol]
end
subgraph "Subscriber"
SUB[Nostr client<br/>connects via WebSocket]
end
WP -- "HTTP POST /publish" --> BR
WP -- "sign with nsec" --> EXT
BR -- "fire UDP datagram" --> UDPR
UDPR -- "raw event bytes" --> VAL
VAL -- "valid event" --> STORE
STORE -- "new event" --> WS
WS -- "NIP-01 EVENT message" --> SUB
style BR fill:#48a,color:#fff
style UDPR fill:#4a4,color:#fff
style WS fill:#a84,color:#fff
```
### Data Flow
1. **User composes message** in the web page, signs it with their Nostr key (via browser extension or in-page nsec)
2. **Web page POSTs** the signed JSON event to the bridge HTTP endpoint
3. **Bridge** fires the event as a single UDP datagram to the relay's UDP receiver (could be localhost or a remote address)
4. **UDP Receiver** receives the datagram, checks it fits in one packet (under 1472 bytes)
5. **Signature Validator** verifies the Schnorr signature against the pubkey in the event
6. **Event Store** appends the valid event to an in-memory array
7. **WebSocket Server** broadcasts the new event to all connected subscribers per NIP-01
---
## Component Specifications
### 1. Web Page (`udp_nostr_web/`)
**Files:**
- `index.html` — The page layout
- `app.js` — Nostr key management, event creation, signing, HTTP POST
- `style.css` — Minimal styling
**Capabilities:**
- Sign in with nsec (text input) or browser extension (nos2x/Alby via `window.nostr`)
- Compose a text message
- Create a Nostr event (kind 1) with the message as content
- Sign the event
- POST the signed JSON to the bridge at `/publish`
- Display the response (success/failure, event ID, size check)
**Constraints enforced client-side:**
- Warn if content exceeds ~1135 characters (would exceed 1472 byte UDP limit)
- Show the total JSON size before sending
### 2. Bridge + UDP Receiver + Relay (Single Node.js Process)
**File:** `udp_nostr_relay.js`
This is a single process that combines three roles:
#### A. HTTP Bridge (port 3000)
| Endpoint | Method | Body | Response |
|---|---|---|---|
| `/publish` | POST | Signed Nostr event JSON | `{ ok: true, id, size }` or `{ ok: false, error }` |
On receiving a valid event:
1. Validates the JSON structure (required fields present)
2. Fires the event as a UDP datagram to the UDP receiver (localhost:UDPPORT)
3. Sets the DF flag (Don't Fragment) to ensure single-packet delivery
4. Returns success/failure to the browser
#### B. UDP Receiver (port 443 or 8888)
- Listens for UDP datagrams
- Checks size ≤ 1472 bytes (single-packet guarantee)
- Parses JSON
- Validates required Nostr fields
- Verifies the Schnorr signature using the pubkey in the event
- If valid: stores the event and broadcasts to WebSocket subscribers
- If invalid: silently drops (no response — no handshake to probe)
#### C. WebSocket Server (port 8008)
- Implements the Nostr relay protocol (NIP-01 subset):
- `REQ` message: subscriber requests events (returns stored events + streams new ones)
- `EVENT` message: subscriber publishes an event (alternative to UDP path)
- `CLOSE` message: subscriber unsubscribes
- On new valid event (from UDP or WebSocket), broadcasts `EVENT` to all matching subscribers
### 3. Signature Verification
Uses the `@noble/secp256k1` library (pure JS, no native deps) to verify Schnorr signatures:
```
event_id = sha256(serialized_event_without_id_and_sig)
valid = schnorr_verify(event_id, sig, pubkey)
```
The event ID is computed from the serialized event fields `[0, pubkey, created_at, kind, tags, content]` per NIP-01.
---
## Port Strategy
| Port | Protocol | Service | Notes |
|---|---|---|---|
| 443 | **UDP** | Nostr UDP Receiver | Blends with QUIC/HTTP3 traffic. Does NOT conflict with TCP 443 (HTTPS). |
| 3000 | TCP | HTTP Bridge | Receives events from the web page. Could also be 443 if using a reverse proxy. |
| 8008 | TCP | WebSocket Relay | Standard Nostr relay port. Subscribers connect here. |
### Why Port 443 for UDP?
- **Censorship resistance:** UDP on port 443 is indistinguishable from QUIC/HTTP3 traffic to a passive observer. Blocking it would break a significant portion of the web.
- **No conflict:** TCP and UDP have separate port namespaces. Your HTTPS server (nginx on TCP 443) and the UDP Nostr receiver (UDP 443) coexist peacefully.
- **Practical consideration:** Some cloud providers and firewalls may block UDP on non-standard ports. Port 443 is universally allowed.
### Tradeoff
If you already have a service using **UDP on port 443** (e.g., a QUIC-enabled web server), you cannot have two UDP listeners on the same port. In that case, use a nearby port like 4443 or 8888.
---
## Implementation Plan
### Step 1: Create the Relay (`udp_nostr_relay.js`)
A single Node.js process with three components:
```
udp_nostr_relay/
├── relay.js # Main entry point — starts all three servers
├── package.json # Dependencies: @noble/secp256k1, ws
├── udp_receiver.js # UDP listener, signature validation, event storage
├── http_bridge.js # HTTP endpoint for browser POST
├── ws_relay.js # WebSocket Nostr relay for subscribers
└── event_store.js # In-memory event storage with subscription matching
```
### Step 2: Create the Web Page (`udp_nostr_web/`)
```
udp_nostr_web/
├── index.html # The page
├── app.js # Nostr key mgmt, signing, HTTP POST
└── style.css # Minimal styling
```
### Step 3: Test the Full Flow
1. Start the relay: `node relay.js`
2. Open the web page in a browser
3. Enter nsec or connect extension
4. Compose a message and send
5. Verify the event appears in the relay
6. Connect a Nostr client (e.g., `noscl` or another browser tab) via WebSocket to port 8008
7. Verify the subscriber receives the event
---
## Open Questions for You
1. **Port preference:** Do you want the UDP receiver on port 443 (blends with QUIC) or a simpler port like 8888 (no potential conflicts)?
2. **Nostr key handling:** Should the web page use a browser extension (nos2x/Alby) for signing, or accept an nsec directly? Extension is more secure but requires the user to have it installed.
3. **Relay location:** Is the relay on the same machine as the web page (localhost demo) or a remote server? This affects the bridge-to-UDP path.
4. **Persistence:** Should events persist across relay restarts (simple JSON file or SQLite) or is in-memory sufficient for the demo?
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* UDP Nostr Sender Maximum Event Demo (JavaScript)
*
* Sends the largest possible Nostr event that fits in a single UDP datagram
* (1472 bytes). Tags are empty, kind is 1, content is filled to the max.
*
* SINGLE-PACKET GUARANTEE:
* Sets the Don't Fragment (DF) flag on the IP header. If the path MTU
* is too small, send() will throw an error and the sender knows the
* packet would have been fragmented.
*/
const dgram = require("dgram");
const { argv, exit } = require("process");
// ── Maximum content size ────────────────────────────────────────────────
// 1472 (UDP payload) - 337 (fixed overhead) = 1135 characters of content
const CONTENT_MAX = 1135;
// ── Build the event ─────────────────────────────────────────────────────
const contentText =
"The cypherpunk problem: how do two computers communicate freely " +
"when the network between them is controlled by an adversary? " +
"The standard approaches are anonymity (hide which computer is talking) " +
"and decentralization (make copies everywhere so you can't stop them all). " +
"But there is a third approach that is less explored: protocol substrate " +
"hardening. Design the communication protocol so that interference requires " +
"violating physics or OS-level guarantees. The simplest example: fit your " +
"message in a single UDP datagram under 1472 bytes. At this size, your " +
"message is exactly one Ethernet frame. The adversary cannot block it " +
"without blocking all UDP traffic on that port. They cannot fragment it. " +
"They cannot reassemble it. They cannot probe for a handshake because " +
"there is no handshake. The Nostr event is self-validating: it carries " +
"its own signature. The signature replaces the handshake. This is the " +
"key insight. Every other protocol announces itself with a handshake. " +
"TCP has SYN. TLS has ClientHello. QUIC has its Initial packet. " +
"WireGuard has its handshake initiation. All of these create a pattern " +
"that can be fingerprinted and blocked. A Nostr event over UDP has no " +
"pattern. It is just bytes on the wire. The receiver validates the " +
"signature using only the pubkey in the event. No shared secrets. No " +
"session setup. No state. This is the no-handshake property. It is the " +
"fundamental advantage of Nostr over every other protocol for censorship " +
"resistance. Combine this with the MTU boundary and you have a protocol " +
"that cannot be blocked without blocking all UDP traffic. The adversary " +
"faces a dilemma: block UDP entirely and break the internet, or let your " +
"messages through. This is the asymmetry we exploit.";
// Truncate to max
const content = contentText.slice(0, CONTENT_MAX);
const event = {
id: "a7e7c0a8f8c8f8c8f8c8f8c8f8c8f8c8f8c8f8c8f8c8f8c8f8c8f8c8f8c8f8c8",
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: content,
sig: "deadbeef".repeat(16),
};
const eventJson = JSON.stringify(event);
const eventBytes = Buffer.from(eventJson, "utf-8");
// ── Print the full event ────────────────────────────────────────────────
console.log("=".repeat(60));
console.log("MAXIMUM NOSTR EVENT — Single UDP Datagram");
console.log("=".repeat(60));
console.log();
console.log(eventJson);
console.log();
console.log(`Total size: ${eventBytes.length} bytes / 1472 max`);
console.log(`Content: ${content.length} characters`);
console.log();
// ── Send ────────────────────────────────────────────────────────────────
const targetHost = argv[2] || "127.0.0.1";
const targetPort = parseInt(argv[3]) || 8888;
const sock = dgram.createSocket("udp4");
// ── Enforce single-packet delivery (Don't Fragment) ─────────────────────
// IPPROTO_IP = 0, IP_MTU_DISCOVER = 10, IP_PMTUDISC_DO = 2
// These are Linux constants. On other platforms this may fail silently.
try {
sock.setOption(0, 10, 2);
console.log("DF flag set — kernel will reject if fragmentation is needed");
} catch (e) {
console.log("Note: could not set DF flag — packet may fragment silently");
}
sock.send(eventBytes, targetPort, targetHost, (err) => {
if (err) {
console.log(`FAILED: ${err.message}`);
console.log("The packet would have been fragmented — path MTU is too small.");
console.log("Try a smaller payload, or use a different network path.");
exit(1);
} else {
console.log(`Sent ${eventBytes.length} bytes to ${targetHost}:${targetPort}`);
console.log("No handshake. No connection. Fire and forget.");
}
sock.close();
});
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
"""UDP Nostr Sender — reads signed event JSON from stdin, sends one datagram.
Usage: nak event -k 1 -c "hello" --sec <nsec> | python3 udp_nostr_send.py <host> [port]"""
import socket, sys
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(sys.stdin.read().encode(), (sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 8888))