nextwave's avatar
nextwave
npub145z4...gem6
Just making sure everyone is having a good time on the Merry-go-round.
nextwave's avatar
nextwave 6 days ago
Say what you will about Bush being functionally retarded, but the people backing him actually understood how to plan and sell their illegal war. Trump's Zionist handlers have now given up on all of their objectives and rationales for the war at this point, including liberating the Iranian people, and have chosen simultaneously to tuck tail and threaten to bomb a bunch of civilian infrastructure and making all the Iranian people suffer.
nextwave's avatar
nextwave 1 week ago
# Sovereign Sockets ## What is a Sovereign Socket? A sovereign socket is a network endpoint whose identity is a cryptographic keypair, not a domain name, not an IP address, not a certificate issued by a third party. It is self-generated, self-authenticating, and unforgeable. No registry, certificate authority, or DNS authority can revoke it, reassign it, or impersonate it. The socket owns itself. Traditional sockets derive their identity from infrastructure you don't control. A TCP socket is identified by an IP address assigned by your ISP and a port managed by your OS. A WebSocket endpoint is identified by a URL, a domain name registered through ICANN, resolved by third-party DNS servers, and authenticated by a certificate signed by a certificate authority. At every layer, someone else holds the keys to your identity. A sovereign socket inverts this. Identity is a 32-byte public key that you generate locally. You prove ownership by completing a cryptographic handshake, not by presenting a certificate signed by a trusted third party, but by demonstrating knowledge of the corresponding private key. The network address (IP and port) is a transport detail, like a phone number. It can change. The identity cannot be taken from you. If you use Nostr, you already understand this concept at the application layer. Your nsec is yours. Your npub is your identity. No one issued it to you, and no one can revoke it. A sovereign socket extends that same principle down to the transport layer. The connection itself is authenticated by keys you own, through a protocol that requires no third-party infrastructure to establish trust. Instead of connecting to `wss://relay.example.com`, you connect to: ``` wire://wpub1rkljs3eq0zxqt8g6dmv7pcnw5afy29hejud5hsfcxpl84rg3ty9kwn66zf@95.217.5.195:55900 ``` The public key is the identity. The IP address is just a hint for how to reach it. ## The Current Nostr Stack and Its Trust Dependencies When a Nostr client connects to a relay, the application layer is beautifully sovereign, events are signed by the user's key, verifiable by anyone, censorable by no one who doesn't control the relay itself. But the transport layer underneath tells a different story. Consider what actually happens when your client opens `wss://relay.example.com`: **DNS resolution.** Your client asks a DNS resolver (often your ISP's, or Google's, or Cloudflare's) to translate `relay.example.com` into an IP address. That domain name is registered through ICANN's hierarchical system, a registrar, a registry, a TLD operator. Any of these entities can seize, suspend, or redirect the domain. Your DNS resolver sees which relay you're connecting to and when. If the domain is seized, the relay's identity is gone. **TLS certificate.** WebSocket Secure requires the relay to present a TLS certificate signed by a certificate authority. The CA system is a trust hierarchy rooted in a few dozen organizations whose root certificates are bundled into your operating system. CAs can be compelled by governments to issue fraudulent certificates. CAs can revoke a relay's certificate, making it unreachable to clients that perform revocation checks. The certificate itself is logged in public Certificate Transparency logs, creating a permanent public record of the relay's domain. **TCP connection.** The WebSocket runs over TCP, a connection-oriented protocol designed in 1981. TCP connections have state that middleboxes, firewalls, ISP equipment, censorship infrastructure, can track and manipulate. A TCP connection can be reset by injecting a RST packet. It suffers from head-of-line blocking: a single lost packet stalls all data behind it, even if that data is unrelated. **WebSocket framing.** On top of TCP and TLS, the WebSocket protocol adds its own framing layer. This exists primarily for browser compatibility, it was designed to tunnel bidirectional communication through HTTP infrastructure. It adds overhead and complexity that serves no purpose outside the browser. **Relay identity = domain name.** The relay's identity in the Nostr ecosystem is its WebSocket URL. If the operator loses the domain, through seizure, expiration, or registrar dispute, the relay loses its identity, its reputation, its place in clients' relay lists. The operator never truly owned the endpoint. The registrar did. The dependency chain looks like this: ``` Your Nostr event (sovereign, signed by your key) ↓ WebSocket frame (browser compatibility layer) ↓ TLS (certificate authority trust hierarchy) ↓ TCP (stateful, injectable, blockable) ↓ DNS (ICANN, registrars, resolvers, all third parties) ↓ IP (assigned by ISP/hosting provider) ``` Your signed event at the top is sovereign. Everything beneath it is not. ## How Wiresocket Replaces This Stack Wiresocket is a UDP-based event-stream protocol that uses the Noise IK handshake pattern (the same cryptographic framework used by WireGuard) to establish encrypted, mutually authenticated sessions between peers identified solely by their public keys. Here is how it maps to each layer of the current stack: **DNS → out-of-band key exchange.** A wiresocket relay's identity is its X25519 public key, 32 bytes. This key can be shared through any channel: a Nostr event, a QR code, a text message, a printed string. The client needs the relay's public key and an IP address (or any route to the relay). No registrar is involved. No one can seize a public key. Where today a relay publishes `wss://relay.example.com`, a wiresocket relay publishes: ``` wire://wpub1j83kf5n4vhx20gwsm6ct9raz7elpq8yd3uf0nw2xk5rcjg7et4qhaysmv@203.0.113.42:9000 ``` That string contains everything a client needs to connect: the relay's permanent identity and a current network address. If the relay moves to a new host, only the address after the `@` changes. The identity before it stays forever. **TLS/CAs → Noise IK handshake.** Wiresocket authenticates both peers using a two-message Noise IK handshake. The client already knows the relay's public key and proves it during the first message. The relay decrypts the client's public key from that same message and can authenticate the client directly, by public key, not by session cookie or OAuth token. No certificate authority is consulted. No certificate is presented. Trust is established solely between the two keypairs. **TCP → UDP.** Wiresocket runs over UDP. There is no connection state for middleboxes to track or inject into. No three-way handshake to block. No RST packets to forge. Wiresocket handles reliability at the application layer where it's needed, per-channel, using selective acknowledgments, avoiding TCP's head-of-line blocking where a lost packet in one stream stalls all others. **WebSocket → wiresocket frames.** Wiresocket's binary frame format is purpose-built for event streaming. Each frame carries one or more events, each with a type byte and a binary payload. A single connection supports up to 65,535 multiplexed logical channels. Fragmentation, reassembly, and optional per-channel reliability are built into the protocol. There is no HTTP upgrade negotiation, no text/binary mode distinction, no masking overhead. **Domain-based identity → key-based identity.** When a relay moves to a new IP address, new hosting provider, new country, new network, its identity doesn't change. The public key is the identity. Clients that know the relay's key can reconnect to the new address. There is no DNS propagation delay, no certificate reissuance, no downtime while the world's DNS caches expire. The new stack: ``` Your Nostr event (sovereign, signed by your key) ↓ Wiresocket frame (binary event-stream, multiplexed channels) ↓ Noise IK encryption (mutual auth by keypair, no CAs) ↓ UDP (stateless, no connection tracking) ↓ IP hint (transport detail, not identity) ``` Every layer is either sovereign or a stateless transport detail. ## Privacy Benefits The trust dependencies in the current stack are not just points of failure, they are points of surveillance. Each one leaks metadata about who is communicating with whom. **No DNS lookups.** When you connect to `wss://relay.example.com`, your DNS resolver sees the relay's domain name. Your ISP logs it. DNS-level censorship and surveillance systems record it. With wiresocket, you connect to an IP address directly. No domain name is resolved. No relay identity is leaked to any resolver. Your client connects to `wire://wpub1j83kf5...@203.0.113.42:9000` and the only thing your network sees is a UDP packet to `203.0.113.42`. **No SNI metadata.** TLS, despite encrypting application data, transmits the server's hostname in plaintext during the handshake via the Server Name Indication (SNI) extension. This is visible to every network observer between client and server. It is the primary mechanism by which nation-state firewalls identify and block specific services. Wiresocket's Noise IK handshake contains no hostname, no server identifier, no plaintext metadata. An observer sees UDP packets to an IP address, nothing more. **Encrypted from the first byte.** In the Noise IK handshake, the client's static public key is encrypted in the very first message. The server's identity is never transmitted at all, the client already knows it. There is no plaintext certificate chain, no negotiation of cipher suites, no protocol version advertisement. The handshake is indistinguishable from random data to a passive observer. **No certificate transparency logs.** Every TLS certificate issued by a public CA is logged in Certificate Transparency logs, public, append-only ledgers that anyone can search. This means that every domain a relay operator registers is publicly discoverable, permanently. Sovereign sockets have no certificates. There is nothing to log. **IP address decoupling.** Because identity is key-based, a relay can change IP addresses freely, moving between hosting providers, rotating through Tor exit nodes, or operating behind a shifting set of addresses. Clients reconnect by public key, not by address. The relay's network location becomes fluid while its identity remains fixed. A relay that was at `wire://wpub1j83kf5...@203.0.113.42:9000` can move to `wire://wpub1j83kf5...@198.51.100.7:9000` and clients recognize it as the same relay, because it is. **Mutual authentication without identity servers.** In the current stack, if a relay wants to know who you are (NIP-42 AUTH), it must implement an authentication flow over the already-established WebSocket, a protocol exchange on top of an unrelated transport layer. With wiresocket, the relay receives your public key during the handshake itself, encrypted and authenticated. Authentication is not a feature bolted on top, it is the handshake. No OAuth endpoints, no login forms, no session cookies, no additional round trips. **No protocol fingerprint.** WebSocket connections begin with an HTTP upgrade request, a distinctive pattern that deep packet inspection (DPI) systems can easily identify and block. TLS handshakes have their own fingerprint (JA3/JA4). Wiresocket's UDP packets carry no distinguishing protocol markers visible to passive observers. ## What This Means for Nostr **Relay sovereignty.** A relay operator generates a keypair and that keypair is the relay's permanent identity. The operator can move between hosting providers, change IP addresses, operate from multiple locations simultaneously, the identity follows the key, not the infrastructure. No domain registrar can seize it. No certificate authority can revoke it. No hosting provider can deplatform the identity itself, only the current IP, which can be replaced. **Client sovereignty.** Clients authenticate to relays during the handshake using their own keypair. This is not an application-layer bolt-on like NIP-42, it is part of establishing the connection. The relay knows the client's public key before any Nostr event is exchanged. Access control, rate limiting, and paid relay authorization can all be implemented at the transport layer, based on cryptographic identity rather than IP addresses or bearer tokens. **Censorship resistance.** The DNS→CA→TCP dependency chain is a series of chokepoints, each controlled by a small number of entities, each subject to legal and extralegal pressure. Remove them and the only thing an adversary can target is the IP address itself. IP addresses can be changed, tunneled through VPNs or Tor, multiplexed across CDNs, or distributed through gossip. The relay's identity survives all of these transitions because it was never coupled to the network location in the first place. **Lighter infrastructure.** Running a Nostr relay today requires: a domain name (annual renewal), TLS certificates (Let's Encrypt automation, renewal cron jobs), a reverse proxy for TLS termination (nginx, caddy), and the relay software itself. With wiresocket, the relay software opens a UDP socket with its private key. That's it. No certificate management, no reverse proxy, no domain registration. The barrier to running a relay drops to: a machine with a public IP and a keypair. **Multiplexed channels.** A single wiresocket connection supports 65,535 logical channels. A relay could dedicate separate channels to different subscription filters, separate channels for ephemeral versus persistent events, a channel for binary blob transfer (images, video segments), a channel for real-time presence or typing indicators, all over one encrypted connection, without the overhead of opening and managing multiple WebSocket connections. **Built-in DoS mitigation.** Wiresocket implements WireGuard's cookie mechanism for handshake flood protection. Under load, the server responds with a cookie reply that requires the client to prove IP address ownership, without the server maintaining any per-client state. This is fundamentally more efficient than TCP SYN flood mitigation, which requires kernel-level intervention or specialized hardware. ## The Path Forward Sovereign sockets do not replace Nostr's application-layer protocol. Events are still signed with your nsec. Filters still work. NIPs still define the vocabulary. What changes is the transport layer beneath, the part that currently depends on DNS, certificate authorities, and TCP. A relay's identity would become its public key, optionally accompanied by one or more IP address hints, similar to how Lightning Network nodes are identified by a public key plus a network address. Where today a client's relay list contains WebSocket URLs: ``` wss://relay.example.com wss://nostr.otherdomain.io wss://paid-relay.site/ws ``` It would instead contain sovereign socket addresses: ``` wire://wpub1rkljs3eq0zxqt8g6dmv7pcnw5afy29hejud5hsfcxpl84rg3ty9kwn66zf@95.217.5.195:55900 wire://wpub1j83kf5n4vhx20gwsm6ct9raz7elpq8yd3uf0nw2xk5rcjg7et4qhaysmv@203.0.113.42:9000 wire://wpub1n60sw2xk5fvpj83hqetcraz7l4gmyd3u09nw28k5rcjg7elmq4tqka43gh@198.51.100.7:4433 ``` No domains. No certificates. Just keys and addresses. Client libraries would need a wiresocket transport adapter alongside their existing WebSocket transport. Relay software could adopt wiresocket as an alternative listener, running both transports simultaneously during any transition period. A NIP could formalize the `wire://` URI scheme and define how relay public keys are published in kind-10002 relay list events. The application layer stays sovereign. The transport layer becomes sovereign too.
nextwave's avatar
nextwave 1 week ago
American planes of American design, flying American weapons, supported by American navigation and guidance systems, refueled by American airtankers, defended by American air defense systems deployed all over the middle east, protected by Americans, everything paid for by American taxpayers. And the news headlines read, "Israeli bombers take out such and such a target". America is acting like the field servants who injure the boar before the hunt so the king can make the killing blow. "Excellent shot mi'lord."
nextwave's avatar
nextwave 2 weeks ago
image Fractal Data Compression Every piece of data has a shape. Fractal data compression makes that shape visible, and in doing so, makes the data smaller. The system works by splitting a message into fixed-size chunks and scanning for repetition. Each unique chunk becomes an affine transform in an Iterated Function System (IFS), the mathematical engine behind fractal geometry. An IFS defines a fractal through a small set of geometric operations, rotations, scalings, and translations, that are applied recursively to generate infinitely detailed structure from a finite description. When the message's byte patterns are mapped to these transforms, the resulting fractal is not an arbitrary visualization. It is the data itself, rendered as geometry. Every curve, cluster, and tendril in the image corresponds to a specific byte pattern from the original message. Compression emerges naturally from this process. When data contains repeated patterns, and most real-world data does, those patterns map to the same transform. The word "the" appearing fifty times in a document does not require fifty entries in the fractal's definition. It requires one transform, referenced fifty times by a compact index. The fractal's dictionary stores each unique pattern once, and a sequence of small indices records the order in which they appear. The dictionary plus the index sequence is the seed: a compact representation that is often significantly smaller than the original data, yet sufficient to reconstruct it exactly. The visual result is telling. Highly compressible data, text with repeated phrases, images with uniform regions, produces regular, self-similar fractals with clear symmetry and recognizable structure. This is because fewer unique transforms mean the IFS reuses the same geometric operations at every scale, which is precisely what makes a fractal self-similar. Random or incompressible data, by contrast, produces chaotic, asymmetric shapes with no discernible pattern. You can literally see the compressibility of data in the geometry of its fractal. Regularity is redundancy. Symmetry is compression. The encoding is lossless. The seed contains every byte of the original data, packed into the dictionary entries and their index sequence. Decoding is deterministic: unpack the dictionary, replay the index sequence, and the original message is recovered bit for bit. No information is lost, approximated, or discarded. The fractal is simply a different representation of the same data, one that happens to be visual, self-similar, and often smaller than the original. This approach inverts the usual relationship between data and visualization. Typically, compression is invisible, a smaller file that looks the same when decompressed. Here, the compression is the visualization. The fractal does not illustrate the data; it is the data, and its structure directly encodes the redundancy that makes compression possible. The medium is the message, and the message is the fractal.
nextwave's avatar
nextwave 2 weeks ago
image 010025047720616569736E6F74726C75646367666D6870792C76622E77540A6B7149536A464748412D7A74128404D08400120200101119128904800E1862C00C708F00215E19208B00314050120318A10030A2472880840012062852C01D00805C62092D60184024800480801D24433012494803C12C200E2063C034818A14B00F0811D300444330211300114B0061D008800E24150620314D11300214C04408B00314004011A0C55806100805022040470C90C748018E00404A1013421000492465C40074023C01C601508044809104808B00314014A3C220628401705211300314C24A2C314D00D2032490C535300E2120C535300620054168314D4C03C168314D0074023C00401070512420031403C115200C28310314211665961008044818B28C1C318500638010128404D08400C0450140484801033450CE0CC0451C94805411022C018500808D0C61412401C804B0C70C614400114B0015010C905524200314D2022C30851C45807C62000C51070453024C07070490C114010128404D08400638708500314C24A2C200E0851422400452C03412090CC4C05D00C908080220F04500404A1013421003C14803820472880800405490852C018E01008854400114B0044433021002850DB2820071801D00832002463012431C30845806100C400B0D40881031D200518700614948008540114C08400740200E24150620044818E0C908054A1C004910600808E2423071001D008030A2472880490040CD14338330114C08018E00404A1013421000C500B0CE38220214700418C0C21C308459965C14004B2C31C31850071801D008320030A24314121200145108125300404A10134210030114004910601508004010628830200638014A1C80C70C61530112065032C314D0112061C20C500114B0021040851C30490140C704F0C51160211970940884C00C70031000CF4462070451C01C600C1851032C22001D00806CA0490C748018E00314D2022C30851C400A1022D30011001063C200C18F3C220C0C124948044818B28C08B00404A1013421003C14803061470431404480840885011C35021000452C004B2C31C350211602210030614428F0881005423063C200F1880804020491D08CC18510C0C62844C01D00880800C400100D2065C314D00B08F0452C03862000481C31011412400452C018834114330010128404D0801911C31851001D00470112031880C70E40806CA0490C74800452C010A1070431415432431D2580
nextwave's avatar
nextwave 2 weeks ago
# Wiresocket Protocol Specification > **DRAFT** — This document reflects implementation as of 2026-02-26 and is pending formal review. Version: 1 (`noisePrologue = "wiresocket v1"`) --- ## 1. Overview Wiresocket is a WireGuard-inspired, encrypted UDP event-stream protocol. It provides a bidirectional, multi-channel event stream between a client and server over a single UDP socket. **Key properties:** - **Transport**: UDP (connectionless) - **Encryption**: Noise IK over X25519 / ChaCha20-Poly1305 / BLAKE2s - **Authentication**: Both static keys (server always; client optionally) - **Multiplexing**: up to 256 independent channels per session (channel IDs 0–255) - **Fragmentation**: frames exceeding one UDP datagram are split across up to 65 535 fragments - **DoS mitigation**: WireGuard-style cookies (XChaCha20-Poly1305) - **Replay protection**: 4096-entry sliding window - **Reliable delivery**: optional per-channel sequencing, cumulative + selective ACKs (SACK), retransmit with exponential backoff, and window-based flow control - **Rate limiting**: optional per-session send rate cap (token bucket; configurable bytes-per-second) All multi-byte integers are **little-endian** unless stated otherwise. --- ## 2. Cryptographic Primitives | Primitive | Algorithm | Notes | |---|---|---| | DH | X25519 (RFC 7748) | Low-order-point rejection enforced | | AEAD (handshake) | ChaCha20-Poly1305 (RFC 8439) | | | AEAD (transport) | ChaCha20-Poly1305 | One AEAD instance per direction, cached per session | | AEAD (cookie reply) | XChaCha20-Poly1305 | 24-byte random nonce | | Hash | BLAKE2s-256 | | | MAC | Keyed BLAKE2s-256 | `BLAKE2s(key=K, data=D)` | | KDF | HKDF-BLAKE2s | See §2.1 | | Keypairs | X25519 | RFC 7748 clamping applied | ### 2.1 KDF ``` prk = MAC(key=ck, data=input) T[1] = MAC(key=prk, data=0x01) T[i] = MAC(key=prk, data=T[i-1] || byte(i)) ``` `kdf2(ck, input)` returns `(T[1], T[2])`. `kdf3(ck, input)` returns `(T[1], T[2], T[3])`. ### 2.2 Initial chaining key and hash ``` initialCK = "Noise_IK_25519_ChaChaPoly_BLAKE2s" (zero-padded to 32 bytes) initialH = HASH(initialCK || "wiresocket v1") ``` Both are computed once at process startup. ### 2.3 AEAD nonce Transport AEAD nonces are 12 bytes: ``` nonce[0:4] = 0x00 0x00 0x00 0x00 nonce[4:12] = counter (uint64 LE) ``` ### 2.4 MAC1 derivation ``` mac1_key = HASH("mac1----" || receiver_static_pub) MAC1 = MAC(mac1_key, message_body)[0:16] ``` `message_body` is everything in the serialised message before the MAC fields. ### 2.5 MAC2 derivation ``` MAC2 = MAC(key=cookie[0:16] zero-extended to 32 bytes, data=message_body)[0:16] ``` --- ## 3. Packet Types The first byte of every UDP datagram is a **type tag**: | Value | Name | Size (bytes) | |---|---|---| | 1 | HandshakeInit | 148 | | 2 | HandshakeResp | 92 | | 3 | CookieReply | 64 | | 4 | Data | 16 + ciphertext | | 5 | Disconnect | 32 (16 header + 16 AEAD tag) | | 6 | Keepalive | 32 (16 header + 16 AEAD tag) | | 7 | DataFragment | 16 + ciphertext | --- ## 4. Wire Formats ### 4.1 HandshakeInit (type 1, 148 bytes) Sent by the initiator to begin a session. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x01 1 3 reserved (zero) 4 4 sender_index uint32 LE — initiator's session token 8 32 ephemeral initiator ephemeral public key (X25519) 40 48 encrypted_static AEAD(initiator static pub key, 32 plain + 16 tag) 88 28 encrypted_timestamp AEAD(TAI64N timestamp, 12 plain + 16 tag) 116 16 mac1 MAC1 keyed by responder's static pub 132 16 mac2 MAC2 keyed by cookie (zeros if no cookie) ``` `sender_index` is a random 32-bit token chosen by the initiator. It is used as `receiver_index` in all subsequent packets the server sends to this session. `encrypted_timestamp` carries a 12-byte TAI64N timestamp (`uint64 BE seconds` || `uint32 BE nanoseconds`). The TAI epoch offset applied is `0x4000000000000000`. The responder rejects timestamps more than ±180 seconds from its own clock. ### 4.2 HandshakeResp (type 2, 92 bytes) Sent by the responder in reply to a valid HandshakeInit. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x02 1 3 reserved (zero) 4 4 sender_index uint32 LE — responder's session token 8 4 receiver_index uint32 LE — echoes initiator's sender_index 12 32 ephemeral responder ephemeral public key (X25519) 44 16 encrypted_nil AEAD(empty plaintext) — 0 plain + 16 tag 60 16 mac1 MAC1 keyed by initiator's static pub 76 16 mac2 MAC2 keyed by cookie (zeros if no cookie) ``` ### 4.3 CookieReply (type 3, 64 bytes) Sent by the server instead of HandshakeResp when under load. The cookie must be used in MAC2 of the retried HandshakeInit. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x03 1 3 reserved (zero) 4 4 receiver_index uint32 LE — echoes initiator's sender_index 8 24 nonce random 24-byte XChaCha20 nonce 32 32 encrypted_cookie XChaCha20-Poly1305(cookie, 16 plain + 16 tag) ``` The XChaCha20-Poly1305 key is the MAC1 from the HandshakeInit zero-extended to 32 bytes. Only the genuine initiator (who knows its own MAC1) can decrypt the cookie. ### 4.4 Data (type 4) Carries one encrypted Frame. Total size: `16 + len(plaintext) + 16`. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x04 1 3 reserved (zero) 4 4 receiver_index uint32 LE — recipient's session token 8 8 counter uint64 LE — monotonic send counter 16 N+16 ciphertext ChaCha20-Poly1305(frame_bytes, N plain + 16 tag) ``` AAD is empty. The nonce is constructed from `counter` (see §2.3). ### 4.5 Disconnect (type 5, 32 bytes) Authenticated graceful shutdown notification. Same header layout as Data with an AEAD over empty plaintext. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x05 1 3 reserved (zero) 4 4 receiver_index uint32 LE 8 8 counter uint64 LE 16 16 AEAD tag ChaCha20-Poly1305(empty) — 0 plain + 16 tag ``` ### 4.6 Keepalive (type 6, 32 bytes) Liveness probe. Same layout as Disconnect with type 6. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x06 1 3 reserved (zero) 4 4 receiver_index uint32 LE 8 8 counter uint64 LE 16 16 AEAD tag ChaCha20-Poly1305(empty) — 0 plain + 16 tag ``` A keepalive is sent whenever no data has been exchanged for `KeepaliveInterval` (default 10 s). The counter participates in replay protection. ### 4.7 DataFragment (type 7) Carries one encrypted fragment of a large Frame. Total size: `16 + 8 + len(frag_data) + 16`. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x07 1 3 reserved (zero) 4 4 receiver_index uint32 LE 8 8 counter uint64 LE 16 N+24 ciphertext ChaCha20-Poly1305(frag_plain, (8+M) plain + 16 tag) ``` The **plaintext** inside the ciphertext has its own sub-header: ``` Plain offset Size Field ------------ ---- ----- 0 4 frame_id uint32 LE — unique per frame within a session 4 2 frag_index uint16 LE — zero-based index of this fragment 6 2 frag_count uint16 LE — total number of fragments (1–65535) 8 M frag_data raw fragment bytes ``` `frame_id` is a monotonically increasing counter scoped to the sending session. All fragments of the same frame share the same `frame_id`. --- ## 5. Noise IK Handshake The handshake follows the **Noise IK** pattern (`Noise_IK_25519_ChaChaPoly_BLAKE2s`): ``` Pre-messages: -> s (responder's static public key known to initiator out-of-band) Messages: -> e, es, s, ss (HandshakeInit) <- e, ee, se (HandshakeResp) ``` ### 5.1 Symmetric State Both sides maintain a symmetric state `(ck, h, k, nonce)`: - `ck` — chaining key, starts as `initialCK` - `h` — transcript hash, starts as `initialH` - `k` — current AEAD key (empty until first `mixKey`) - `nonce` — per-key counter, reset to 0 on each `mixKey` **`mixHash(data)`**: `h = HASH(h || data)` **`mixKey(dh_out)`**: `(ck, k) = kdf2(ck, dh_out)` ; `nonce = 0` **`encryptAndHash(plain)`**: `c = AEAD_Seal(k, nonce++, h, plain)` ; `mixHash(c)` ; return `c` **`decryptAndHash(cipher)`**: `p = AEAD_Open(k, nonce++, h, cipher)` ; `mixHash(cipher)` ; return `p` ### 5.2 Initiator: CreateInit Starting state: `ck = initialCK`, `h = HASH(initialCK || "wiresocket v1")`. ``` Pre-message: mixHash(responder_static_pub) Message construction: mixHash(e_pub) # -> e (ck, k) = kdf2(ck, DH(e_priv, s_resp_pub)) # -> es encrypted_static = encryptAndHash(s_init_pub) # -> s (ck, k) = kdf2(ck, DH(s_init_priv, s_resp_pub)) # -> ss encrypted_timestamp = encryptAndHash(TAI64N()) mac1 = MAC1(responder_static_pub, msg_body_without_macs) mac2 = MAC2(cookie, msg_body_without_mac2) # zero if no cookie ``` ### 5.3 Responder: ConsumeInit ``` Pre-message: mixHash(local_static_pub) Verification and processing: verify MAC1 mixHash(e_init_pub) # -> e (ck, k) = kdf2(ck, DH(s_resp_priv, e_init_pub)) # -> es s_init_pub = decryptAndHash(encrypted_static) # -> s (ck, k) = kdf2(ck, DH(s_resp_priv, s_init_pub)) # -> ss timestamp = decryptAndHash(encrypted_timestamp) validate timestamp within ±180 s ``` ### 5.4 Responder: CreateResp Continues immediately after ConsumeInit. ``` mixHash(e_resp_pub) # -> e (ck, k) = kdf2(ck, DH(e_resp_priv, e_init_pub)) # -> ee (ck, k) = kdf2(ck, DH(e_resp_priv, s_init_pub)) # -> se encrypted_nil = encryptAndHash(empty) mac1 = MAC1(initiator_static_pub, resp_body_without_macs) mac2 = 0 (no cookie in response) ``` ### 5.5 Initiator: ConsumeResp ``` mixHash(e_resp_pub) # <- e (ck, k) = kdf2(ck, DH(e_init_priv, e_resp_pub)) # <- ee (ck, k) = kdf2(ck, DH(s_init_priv, e_resp_pub)) # <- se decryptAndHash(encrypted_nil) # proves responder identity ``` ### 5.6 Transport Key Derivation (SPLIT) After both sides complete the handshake: ``` (T_initiator_send, T_initiator_recv) = kdf2(ck, empty) ``` - Initiator sends with `T_initiator_send`, receives with `T_initiator_recv`. - Responder sends with `T_initiator_recv`, receives with `T_initiator_send`. --- ## 6. Cookie / DoS Mitigation When the server is under load, it may respond to HandshakeInit with a CookieReply instead of HandshakeResp. ### 6.1 Cookie derivation (server side) The server maintains a 32-byte `cookie_secret` rotated every 2 minutes. ``` cookie_key = MAC(key=cookie_secret, data="cookie--" || server_static_pub) cookie = MAC(key=cookie_key, data=client_addr_string)[0:16] ``` `client_addr_string` is the client's UDP address formatted as `"ip:port"`. ### 6.2 CookieReply construction ``` key = mac1_from_HandshakeInit zero-extended to 32 bytes nonce = random 24 bytes encrypted_cookie = XChaCha20-Poly1305-Seal(key, nonce, cookie, aad=empty) ``` ### 6.3 MAC2 retry On receiving a CookieReply, the initiator decrypts the cookie and retries HandshakeInit with: ``` MAC2 = MAC2(cookie, message_body_without_mac2)[0:16] ``` The server accepts a HandshakeInit with a valid MAC2 even under load. --- ## 7. Session Lifecycle ``` Client Server | | |------ HandshakeInit --------->| | | (may send CookieReply if under load) |<----- HandshakeResp ----------| | | | [session established] | | | |<===== Data / Fragments ======>| |<===== Keepalives ============>| | | |------ Disconnect ------------>| (or timeout) ``` ### 7.1 Session indices Each side independently picks a random 32-bit `session_index`. The sender places the **recipient's** index in every outgoing data packet's `receiver_index` field so the recipient can route it to the correct session. ### 7.2 Send counter The sender maintains a monotonically increasing 64-bit counter starting at 0. The counter is used both as the AEAD nonce and for the receiver's replay window. When the counter reaches `2^64 - 1` it wraps to 0 and the session is closed, forcing a new handshake. ### 7.3 Timeouts and keepalives | Parameter | Default | Description | |---|---|---| | `KeepaliveInterval` | 10 s | Send keepalive if data idle for this long | | `SessionTimeout` | 180 s | Close session if no packet received for this long | | `RekeyAfterTime` | 180 s | Initiate new handshake after this session age | | `RekeyAfterMessages` | 2^60 | Initiate new handshake after this many packets sent | Each side enforces its own timeout independently. Keepalive receipt does **not** reset the data-idle timer; a peer that only sends keepalives will still receive them in return. --- ## 8. Replay Protection Incoming data, keepalive, disconnect, and fragment packets are all subject to replay protection using a 4096-entry sliding window implemented as a `[64]uint64` bitmap (64 words × 64 bits). ``` Window covers: [head - 4095, head] ``` - If `counter > head`: accepted (new high-water mark). - If `counter < head - 4095`: rejected (too old). - Otherwise: check bitmask. If bit already set: rejected (duplicate). The window is updated (`head` advanced, bitmap updated) only after AEAD decryption succeeds. `head` is stored atomically for a lock-free fast path on the common case of strictly in-order delivery. **Why 4096 entries?** On Linux, `sendFragments` allocates all N fragment counters in a tight loop *before* the `sendmmsg` batch is written to the socket. A concurrent keepalive send can steal a counter mid-loop and arrive at the peer before the fragment batch, causing the peer's `head` to advance past the early fragment counters. With the original 64-entry window, a difference ≥ 64 triggered spurious replay rejections and permanent event loss. A 4096-entry window safely accommodates the maximum in-flight fragment count with ample headroom. --- ## 9. Fragmentation Frames whose serialised plaintext exceeds `maxFragPayload` bytes are split into multiple DataFragment packets. ### 9.1 Default MTU parameters | Parameter | Value | Derivation | |---|---|---| | Default MaxPacketSize | 1232 bytes | IPv6 min path MTU (1280) − IPv6 header (40) − UDP header (8) | | Default maxFragPayload | 1192 bytes | 1232 − DataHeader(16) − FragmentHeader(8) − AEADTag(16) | | Maximum fragments per frame | 65 535 | `frag_count` is uint16 | | Maximum frame size at default MTU | ≈ 78 MB | 65 535 × 1192 bytes | ### 9.2 Reassembly The receiver maintains a map of partial frames keyed by `frame_id`. When all `frag_count` fragments for a `frame_id` arrive, the payloads are concatenated in `frag_index` order and the result is decoded as a Frame. Incomplete fragment sets are garbage-collected after `2 × KeepaliveInterval` of inactivity. At most `MaxIncompleteFrames` (default 64, configurable per `ServerConfig`/`DialConfig`) partial frames are buffered per session; excess fragments are silently dropped. Duplicate fragments (same `frame_id` + `frag_index`) are ignored. --- ## 10. Frame Wire Format A **Frame** is the plaintext inside a Data or reassembled DataFragment packet. It uses a custom encoding that is a strict subset of Protocol Buffers wire format, allowing standard proto decoders to parse it as an extension of the proto schema. ``` Byte 0: channel_id (uint8, raw — not proto-encoded) Bytes 1..N: events (field-1 LEN records, one per event) [optional] Seq (field-2 varint) — omitted when 0 (unreliable) [optional] AckSeq (field-3 varint) — omitted when 0 [optional] AckBitmap (field-4 I64 LE) — omitted when 0 [optional] WindowSize (field-5 varint) — omitted when 0 ``` Each event body is encoded as a Protocol Buffers–style LEN field: ``` varint(0x0A) # field=1, wire type=2 (LEN) → event body follows varint(len(event_body)) # byte length of event body event_body[0] # event type (uint8, 0–254 app-defined; 255 internal) event_body[1:] # opaque payload bytes (may be empty) ``` The reliability fields are appended after all events using these proto tags: ``` varint(0x10) # field=2, wire type=0 (varint) → Seq (uint32) varint(0x18) # field=3, wire type=0 (varint) → AckSeq (uint32) varint(0x21) # field=4, wire type=1 (I64 LE) → AckBitmap (uint64) varint(0x28) # field=5, wire type=0 (varint) → WindowSize (uint32) ``` All four reliability fields are zero-omitted, preserving backward compatibility with peers that implement only unreliable delivery. A **standalone ACK** frame carries no events (`Events` is empty) and has `Seq == 0`; it carries only `AckSeq`, `AckBitmap`, and `WindowSize`. Multiple events may be packed into a single Frame (coalescing). ### 10.1 Channel IDs | Value | Usage | |---|---| | 0 | Default channel | | 1–254 | Application-defined channels | | 255 | Internal (`channelCloseType`) | Event type 255 (`channelCloseType`) on any channel signals that the remote peer has closed that channel. The receiver evicts the channel and signals any blocked `Recv` callers with an error. --- ## 11. Reliable Delivery Reliable delivery is opt-in per channel via `Channel.SetReliable(ReliableCfg)`. Channels that do not call `SetReliable` have zero overhead; all reliability fields in their frames are zero and ignored on receipt. ### 11.1 Sequence numbers Sequence numbers are `uint32`, starting at **1** (0 is reserved to mean "unreliable"). The sender assigns a monotonically increasing sequence number to each outgoing frame in `preSend`. Sequence numbers reset to 1 whenever the underlying session reconnects (persistent connections). ### 11.2 Cumulative ACK `AckSeq` carries the **next expected** sequence number (TCP-style). A value of `AckSeq = N` means the receiver has received all frames with `seq ∈ [1, N-1]` in order and is waiting for `seq == N`. The sender frees all pending frames with `seq < AckSeq`. ### 11.3 Selective ACK (SACK) `AckBitmap` is a 64-bit SACK bitmap. Bit `i` (LSB = bit 0) is set when the frame with sequence number `AckSeq + i + 1` has been received out of order. The sender uses this to free selectively acknowledged frames without waiting for gap-filling retransmits. The receiver maintains an out-of-order (OOO) buffer of at most `reliableOOOWindow = 64` slots. Frames arriving more than 64 positions ahead of the expected sequence are dropped. ### 11.4 Flow control (window) `WindowSize` reports how many additional frames the sender of the ACK can accept (receive-buffer headroom). The remote sender blocks when `numPending >= peerWindow`. `WindowSize` is updated dynamically as the receiver's channel buffer drains. The initial window equals the configured `ReliableCfg.WindowSize` (default 256). ### 11.5 ACK timing ACKs are **piggybacked** on outgoing data frames whenever possible: before a data frame is sent, any pending ACK state is consumed and written into the frame's `AckSeq`/`AckBitmap`/`WindowSize` fields at no extra cost. When no data is ready to send, a **standalone ACK** is dispatched after at most `ACKDelay` (default 20 ms) by a `time.AfterFunc` timer. ### 11.6 Retransmit A single retransmit timer (`time.AfterFunc`) is armed per channel after any frame is sent. On firing it retransmits the oldest unACKed frame and reschedules itself with **exponential backoff** (RTO doubles each retry, capped at `maxRTO = 30 s`). After `MaxRetries` (default 10) consecutive retransmit attempts the channel is closed. The retransmit timer is reset to `BaseRTO` (default 200 ms) whenever an ACK frees at least one frame. ### 11.7 Reconnect behaviour When a persistent `Conn` reconnects, `reset()` is called on every channel that has a `reliableState`. This purges all pending frames, resets `nextSeq` to 1, restores `peerWindow` to the configured maximum, and broadcasts on the flow-control condition variable to unblock any goroutines waiting in `preSend`. The receiver-side `expectSeq` is also reset to 1 and the OOO buffer is cleared. In-flight frames that had not been ACKed before the reconnect are discarded; the application layer is responsible for any end-to-end retransmission policy across reconnects. --- ## 12. Numeric Limits | Constant | Value | |---|---| | `sizeHandshakeInit` | 148 bytes | | `sizeHandshakeResp` | 92 bytes | | `sizeCookieReply` | 64 bytes | | `sizeDataHeader` | 16 bytes | | `sizeFragmentHeader` | 8 bytes | | `sizeAEADTag` | 16 bytes | | `sizeKeepalive` / `sizeDisconnect` | 32 bytes | | `defaultMaxPacketSize` | 1232 bytes | | `defaultMaxFragPayload` | 1192 bytes | | `MaxIncompleteFrames` (default) | 64 (configurable) | | `windowWords` | 64 | | `windowSize` (replay) | 4096 (= 64 words × 64 bits) | | `maxTimestampSkew` | 180 s | | `cookieRotation` | 2 min | | `rekeyAfterTime` | 180 s | | `rekeyAfterMessages` | 2^60 | | Maximum fragments per frame | 65 535 | | Maximum channels | 256 (IDs 0–255) | | `defaultReliableWindow` | 256 frames | | `defaultBaseRTO` | 200 ms | | `defaultMaxRetries` | 10 | | `defaultACKDelay` | 20 ms | | `maxRTO` | 30 s | | `reliableOOOWindow` | 64 slots | --- ## 13. Implementation Notes ### Batch sends (Linux) On Linux, outgoing fragments are sent in a single `sendmmsg(2)` syscall via `ipv4.PacketConn.WriteBatch` (IPv4 sockets) or `ipv6.PacketConn.WriteBatch` (IPv6 sockets). On other platforms, fragments are sent in a loop of individual `sendmsg` calls. ### Batch receives Incoming UDP datagrams are read in batches of up to 64 (server) or 16 (client) messages per `recvmmsg(2)` syscall via `ipv4.PacketConn.ReadBatch`. ### Buffer pools Send and receive paths use `sync.Pool`-backed byte slices to reduce GC pressure. AEAD operations write ciphertext and plaintext in-place into pool buffers; no additional allocation occurs on the hot path when buffers have sufficient capacity. ### Socket buffers Both client and server request 4 MiB `SO_RCVBUF` / `SO_SNDBUF`. On Linux, this may be silently clamped by `net.core.rmem_max` / `wmem_max` (default ≈ 208 KiB). To guarantee the full 4 MiB, either raise the sysctl: ```bash sysctl -w net.core.rmem_max=4194304 sysctl -w net.core.wmem_max=4194304 ``` or use `SO_RCVBUFFORCE` (requires `CAP_NET_ADMIN`). In Docker, pass `--sysctl net.core.rmem_max=4194304`. ### Rate limiting When `SendRateLimitBPS` is non-zero in `ServerConfig` or `DialConfig`, a token-bucket limiter is installed on the session's send path. The burst capacity is 2× the per-second rate, allowing short bursts to proceed at full wire speed. The limiter is checked in `session.send()` and `session.sendFragments()` before each write; it sleeps on a timer when tokens are exhausted and aborts early if the session closes. ### Pipeline sizing (`ProbeUDPRecvBufSize`) Benchmark and high-throughput pipeline code should call `wiresocket.ProbeUDPRecvBufSize(requested int) int` to determine the actual achievable receive-buffer size before computing `inflightCap`. On Linux without `CAP_NET_ADMIN`, `SO_RCVBUF` is clamped by `net.core.rmem_max`; the probe function creates a temporary loopback socket, applies `SO_RCVBUFFORCE` (falling back to `SO_RCVBUF`), reads back the result with `getsockopt`, and divides by 2 to undo the kernel's automatic doubling. On other platforms it returns `requested` unchanged. Using the probed value prevents the in-flight frame count from exceeding what the kernel buffer can hold, avoiding packet loss under burst sends.
nextwave's avatar
nextwave 2 weeks ago
First round of reliability tuning in. Throughput is still holding strong. Next round is testing the protocol out on increasingly shaky environments. image
nextwave's avatar
nextwave 2 weeks ago
Some say we've moved from the Information Age to the Intelligence Age. We're actually in the Discernment Age.
nextwave's avatar
nextwave 2 weeks ago
Here's a benchmark for the final set of optimizations on commodity hardware. Very reasonable performance. image
nextwave's avatar
nextwave 3 weeks ago
Hopefully this will shed some light as to why I designed the protocol with channels. Channel multiplexing lets logically separate concerns share a single encrypted session without the overhead of establishing multiple connections. The main use cases: Prioritization and QoS - High-priority control messages (e.g. stop/pause commands) on a dedicated channel so they aren't queued behind large data payloads - Real-time telemetry on one channel, bulk file transfer on another — each can have independent flow control Topic / stream isolation - Pub/sub: each topic maps to a channel; subscribers receive only what they're interested in without receiver-side filtering overhead - Sensor feeds: temperature on ch 1, GPS on ch 2, video frames on ch 3 — consumers subscribe selectively Request/response multiplexing - Interleave multiple concurrent RPC calls over one session — each call gets its own channel ID to match replies to requests, similar to HTTP/2 streams Backpressure isolation - A slow consumer on channel 3 doesn't block or drop events on channel 1 — each channel has its own buffer - Wiresocket's per-channel events buffer means a full channel drops or blocks independently Versioned or feature-gated streams - Send on channel 0 for v1 clients, channel 1 for v2 clients — a server can maintain both protocols simultaneously without separate sockets Lifecycle decoupling - A control plane (handshake, session management) and data plane (event stream) on separate channels — the control channel can signal teardown without racing with in-flight data Security boundaries - Different trust levels or tenants sharing a session can be isolated by channel — the application enforces which channels a principal may read/write without separate TLS termination In wiresocket specifically, the design is intentionally minimal: 256 channels (0–255), channel 255 reserved for internal close signals. The application layer owns the semantics of each channel ID, keeping the protocol unopinionated about how multiplexing is used.
nextwave's avatar
nextwave 3 weeks ago
The early reference implementation in Golang will saturate multi-gigabit connections on commodity hardware. The payload is the size of individual events. image
nextwave's avatar
nextwave 3 weeks ago
# Wiresocket Protocol Specification Version: 1 (`noisePrologue = "wiresocket v1"`) --- ## 1. Overview Wiresocket is a WireGuard-inspired, encrypted UDP event-stream protocol. It provides a bidirectional, multi-channel event stream between a client and server over a single UDP socket. **Key properties:** - **Transport**: UDP (connectionless) - **Encryption**: Noise IK over X25519 / ChaCha20-Poly1305 / BLAKE2s - **Authentication**: Both static keys (server always; client optionally) - **Multiplexing**: up to 256 independent channels per session (channel IDs 0–255) - **Fragmentation**: frames exceeding one UDP datagram are split across up to 65 535 fragments - **DoS mitigation**: WireGuard-style cookies (XChaCha20-Poly1305) - **Replay protection**: 64-bit sliding window All multi-byte integers are **little-endian** unless stated otherwise. --- ## 2. Cryptographic Primitives | Primitive | Algorithm | Notes | |---|---|---| | DH | X25519 (RFC 7748) | Low-order-point rejection enforced | | AEAD (handshake) | ChaCha20-Poly1305 (RFC 8439) | | | AEAD (transport) | ChaCha20-Poly1305 | One AEAD instance per direction, cached per session | | AEAD (cookie reply) | XChaCha20-Poly1305 | 24-byte random nonce | | Hash | BLAKE2s-256 | | | MAC | Keyed BLAKE2s-256 | `BLAKE2s(key=K, data=D)` | | KDF | HKDF-BLAKE2s | See §2.1 | | Keypairs | X25519 | RFC 7748 clamping applied | ### 2.1 KDF ``` prk = MAC(key=ck, data=input) T[1] = MAC(key=prk, data=0x01) T[i] = MAC(key=prk, data=T[i-1] || byte(i)) ``` `kdf2(ck, input)` returns `(T[1], T[2])`. `kdf3(ck, input)` returns `(T[1], T[2], T[3])`. ### 2.2 Initial chaining key and hash ``` initialCK = "Noise_IK_25519_ChaChaPoly_BLAKE2s" (zero-padded to 32 bytes) initialH = HASH(initialCK || "wiresocket v1") ``` Both are computed once at process startup. ### 2.3 AEAD nonce Transport AEAD nonces are 12 bytes: ``` nonce[0:4] = 0x00 0x00 0x00 0x00 nonce[4:12] = counter (uint64 LE) ``` ### 2.4 MAC1 derivation ``` mac1_key = HASH("mac1----" || receiver_static_pub) MAC1 = MAC(mac1_key, message_body)[0:16] ``` `message_body` is everything in the serialised message before the MAC fields. ### 2.5 MAC2 derivation ``` MAC2 = MAC(key=cookie[0:16] zero-extended to 32 bytes, data=message_body)[0:16] ``` --- ## 3. Packet Types The first byte of every UDP datagram is a **type tag**: | Value | Name | Size (bytes) | |---|---|---| | 1 | HandshakeInit | 148 | | 2 | HandshakeResp | 92 | | 3 | CookieReply | 64 | | 4 | Data | 16 + ciphertext | | 5 | Disconnect | 32 (16 header + 16 AEAD tag) | | 6 | Keepalive | 32 (16 header + 16 AEAD tag) | | 7 | DataFragment | 16 + ciphertext | --- ## 4. Wire Formats ### 4.1 HandshakeInit (type 1, 148 bytes) Sent by the initiator to begin a session. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x01 1 3 reserved (zero) 4 4 sender_index uint32 LE — initiator's session token 8 32 ephemeral initiator ephemeral public key (X25519) 40 48 encrypted_static AEAD(initiator static pub key, 32 plain + 16 tag) 88 28 encrypted_timestamp AEAD(TAI64N timestamp, 12 plain + 16 tag) 116 16 mac1 MAC1 keyed by responder's static pub 132 16 mac2 MAC2 keyed by cookie (zeros if no cookie) ``` `sender_index` is a random 32-bit token chosen by the initiator. It is used as `receiver_index` in all subsequent packets the server sends to this session. `encrypted_timestamp` carries a 12-byte TAI64N timestamp (`uint64 BE seconds` || `uint32 BE nanoseconds`). The TAI epoch offset applied is `0x4000000000000000`. The responder rejects timestamps more than ±180 seconds from its own clock. ### 4.2 HandshakeResp (type 2, 92 bytes) Sent by the responder in reply to a valid HandshakeInit. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x02 1 3 reserved (zero) 4 4 sender_index uint32 LE — responder's session token 8 4 receiver_index uint32 LE — echoes initiator's sender_index 12 32 ephemeral responder ephemeral public key (X25519) 44 16 encrypted_nil AEAD(empty plaintext) — 0 plain + 16 tag 60 16 mac1 MAC1 keyed by initiator's static pub 76 16 mac2 MAC2 keyed by cookie (zeros if no cookie) ``` ### 4.3 CookieReply (type 3, 64 bytes) Sent by the server instead of HandshakeResp when under load. The cookie must be used in MAC2 of the retried HandshakeInit. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x03 1 3 reserved (zero) 4 4 receiver_index uint32 LE — echoes initiator's sender_index 8 24 nonce random 24-byte XChaCha20 nonce 32 32 encrypted_cookie XChaCha20-Poly1305(cookie, 16 plain + 16 tag) ``` The XChaCha20-Poly1305 key is the MAC1 from the HandshakeInit zero-extended to 32 bytes. Only the genuine initiator (who knows its own MAC1) can decrypt the cookie. ### 4.4 Data (type 4) Carries one encrypted Frame. Total size: `16 + len(plaintext) + 16`. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x04 1 3 reserved (zero) 4 4 receiver_index uint32 LE — recipient's session token 8 8 counter uint64 LE — monotonic send counter 16 N+16 ciphertext ChaCha20-Poly1305(frame_bytes, N plain + 16 tag) ``` AAD is empty. The nonce is constructed from `counter` (see §2.3). ### 4.5 Disconnect (type 5, 32 bytes) Authenticated graceful shutdown notification. Same header layout as Data with an AEAD over empty plaintext. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x05 1 3 reserved (zero) 4 4 receiver_index uint32 LE 8 8 counter uint64 LE 16 16 AEAD tag ChaCha20-Poly1305(empty) — 0 plain + 16 tag ``` ### 4.6 Keepalive (type 6, 32 bytes) Liveness probe. Same layout as Disconnect with type 6. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x06 1 3 reserved (zero) 4 4 receiver_index uint32 LE 8 8 counter uint64 LE 16 16 AEAD tag ChaCha20-Poly1305(empty) — 0 plain + 16 tag ``` A keepalive is sent whenever no data has been exchanged for `KeepaliveInterval` (default 10 s). The counter participates in replay protection. ### 4.7 DataFragment (type 7) Carries one encrypted fragment of a large Frame. Total size: `16 + 8 + len(frag_data) + 16`. ``` Offset Size Field ------ ---- ----- 0 1 type = 0x07 1 3 reserved (zero) 4 4 receiver_index uint32 LE 8 8 counter uint64 LE 16 N+24 ciphertext ChaCha20-Poly1305(frag_plain, (8+M) plain + 16 tag) ``` The **plaintext** inside the ciphertext has its own sub-header: ``` Plain offset Size Field ------------ ---- ----- 0 4 frame_id uint32 LE — unique per frame within a session 4 2 frag_index uint16 LE — zero-based index of this fragment 6 2 frag_count uint16 LE — total number of fragments (1–65535) 8 M frag_data raw fragment bytes ``` `frame_id` is a monotonically increasing counter scoped to the sending session. All fragments of the same frame share the same `frame_id`. --- ## 5. Noise IK Handshake The handshake follows the **Noise IK** pattern (`Noise_IK_25519_ChaChaPoly_BLAKE2s`): ``` Pre-messages: -> s (responder's static public key known to initiator out-of-band) Messages: -> e, es, s, ss (HandshakeInit) <- e, ee, se (HandshakeResp) ``` ### 5.1 Symmetric State Both sides maintain a symmetric state `(ck, h, k, nonce)`: - `ck` — chaining key, starts as `initialCK` - `h` — transcript hash, starts as `initialH` - `k` — current AEAD key (empty until first `mixKey`) - `nonce` — per-key counter, reset to 0 on each `mixKey` **`mixHash(data)`**: `h = HASH(h || data)` **`mixKey(dh_out)`**: `(ck, k) = kdf2(ck, dh_out)` ; `nonce = 0` **`encryptAndHash(plain)`**: `c = AEAD_Seal(k, nonce++, h, plain)` ; `mixHash(c)` ; return `c` **`decryptAndHash(cipher)`**: `p = AEAD_Open(k, nonce++, h, cipher)` ; `mixHash(cipher)` ; return `p` ### 5.2 Initiator: CreateInit Starting state: `ck = initialCK`, `h = HASH(initialCK || "wiresocket v1")`. ``` Pre-message: mixHash(responder_static_pub) Message construction: mixHash(e_pub) # -> e (ck, k) = kdf2(ck, DH(e_priv, s_resp_pub)) # -> es encrypted_static = encryptAndHash(s_init_pub) # -> s (ck, k) = kdf2(ck, DH(s_init_priv, s_resp_pub)) # -> ss encrypted_timestamp = encryptAndHash(TAI64N()) mac1 = MAC1(responder_static_pub, msg_body_without_macs) mac2 = MAC2(cookie, msg_body_without_mac2) # zero if no cookie ``` ### 5.3 Responder: ConsumeInit ``` Pre-message: mixHash(local_static_pub) Verification and processing: verify MAC1 mixHash(e_init_pub) # -> e (ck, k) = kdf2(ck, DH(s_resp_priv, e_init_pub)) # -> es s_init_pub = decryptAndHash(encrypted_static) # -> s (ck, k) = kdf2(ck, DH(s_resp_priv, s_init_pub)) # -> ss timestamp = decryptAndHash(encrypted_timestamp) validate timestamp within ±180 s ``` ### 5.4 Responder: CreateResp Continues immediately after ConsumeInit. ``` mixHash(e_resp_pub) # -> e (ck, k) = kdf2(ck, DH(e_resp_priv, e_init_pub)) # -> ee (ck, k) = kdf2(ck, DH(e_resp_priv, s_init_pub)) # -> se encrypted_nil = encryptAndHash(empty) mac1 = MAC1(initiator_static_pub, resp_body_without_macs) mac2 = 0 (no cookie in response) ``` ### 5.5 Initiator: ConsumeResp ``` mixHash(e_resp_pub) # <- e (ck, k) = kdf2(ck, DH(e_init_priv, e_resp_pub)) # <- ee (ck, k) = kdf2(ck, DH(s_init_priv, e_resp_pub)) # <- se decryptAndHash(encrypted_nil) # proves responder identity ``` ### 5.6 Transport Key Derivation (SPLIT) After both sides complete the handshake: ``` (T_initiator_send, T_initiator_recv) = kdf2(ck, empty) ``` - Initiator sends with `T_initiator_send`, receives with `T_initiator_recv`. - Responder sends with `T_initiator_recv`, receives with `T_initiator_send`. --- ## 6. Cookie / DoS Mitigation When the server is under load, it may respond to HandshakeInit with a CookieReply instead of HandshakeResp. ### 6.1 Cookie derivation (server side) The server maintains a 32-byte `cookie_secret` rotated every 2 minutes. ``` cookie_key = MAC(key=cookie_secret, data="cookie--" || server_static_pub) cookie = MAC(key=cookie_key, data=client_addr_string)[0:16] ``` `client_addr_string` is the client's UDP address formatted as `"ip:port"`. ### 6.2 CookieReply construction ``` key = mac1_from_HandshakeInit zero-extended to 32 bytes nonce = random 24 bytes encrypted_cookie = XChaCha20-Poly1305-Seal(key, nonce, cookie, aad=empty) ``` ### 6.3 MAC2 retry On receiving a CookieReply, the initiator decrypts the cookie and retries HandshakeInit with: ``` MAC2 = MAC2(cookie, message_body_without_mac2)[0:16] ``` The server accepts a HandshakeInit with a valid MAC2 even under load. --- ## 7. Session Lifecycle ``` Client Server | | |------ HandshakeInit --------->| | | (may send CookieReply if under load) |<----- HandshakeResp ----------| | | | [session established] | | | |<===== Data / Fragments ======>| |<===== Keepalives ============>| | | |------ Disconnect ------------>| (or timeout) ``` ### 7.1 Session indices Each side independently picks a random 32-bit `session_index`. The sender places the **recipient's** index in every outgoing data packet's `receiver_index` field so the recipient can route it to the correct session. ### 7.2 Send counter The sender maintains a monotonically increasing 64-bit counter starting at 0. The counter is used both as the AEAD nonce and for the receiver's replay window. When the counter reaches `2^64 - 1` it wraps to 0 and the session is closed, forcing a new handshake. ### 7.3 Timeouts and keepalives | Parameter | Default | Description | |---|---|---| | `KeepaliveInterval` | 10 s | Send keepalive if data idle for this long | | `SessionTimeout` | 180 s | Close session if no packet received for this long | | `RekeyAfterTime` | 180 s | Initiate new handshake after this session age | | `RekeyAfterMessages` | 2^60 | Initiate new handshake after this many packets sent | Each side enforces its own timeout independently. Keepalive receipt does **not** reset the data-idle timer; a peer that only sends keepalives will still receive them in return. --- ## 8. Replay Protection Incoming data, keepalive, disconnect, and fragment packets are all subject to replay protection using a 64-bit sliding window of size 64. ``` Window covers: [head - 63, head] ``` - If `counter > head`: accepted (new high-water mark). - If `counter < head - 63`: rejected (too old). - Otherwise: check bitmask. If bit already set: rejected (duplicate). The window is updated (`head` advanced, bitmap updated) only after AEAD decryption succeeds. `head` is stored atomically for a lock-free fast path on the common case of strictly in-order delivery. --- ## 9. Fragmentation Frames whose serialised plaintext exceeds `maxFragPayload` bytes are split into multiple DataFragment packets. ### 9.1 Default MTU parameters | Parameter | Value | Derivation | |---|---|---| | Default MaxPacketSize | 1232 bytes | IPv6 min path MTU (1280) − IPv6 header (40) − UDP header (8) | | Default maxFragPayload | 1192 bytes | 1232 − DataHeader(16) − FragmentHeader(8) − AEADTag(16) | | Maximum fragments per frame | 65 535 | `frag_count` is uint16 | | Maximum frame size at default MTU | ≈ 78 MB | 65 535 × 1192 bytes | ### 9.2 Reassembly The receiver maintains a map of partial frames keyed by `frame_id`. When all `frag_count` fragments for a `frame_id` arrive, the payloads are concatenated in `frag_index` order and the result is decoded as a Frame. Incomplete fragment sets are garbage-collected after `2 × KeepaliveInterval` of inactivity. At most `MaxIncompleteFrames` (default 64) partial frames are buffered per session; excess fragments are silently dropped. Duplicate fragments (same `frame_id` + `frag_index`) are ignored. --- ## 10. Frame Wire Format A **Frame** is the plaintext inside a Data or reassembled DataFragment packet. ``` Byte 0: channel_id (uint8) Byte 1..N: events (sequence of length-prefixed event bodies) ``` Each event body is encoded as a Protocol Buffers–style LEN field (field 1, wire type 2): ``` varint(0x0A) # field=1, wire type=LEN (proto tag) varint(len(event_body)) # byte length of event body event_body[0] # event type (uint8, 0–254 app-defined; 255 internal) event_body[1:] # opaque payload bytes (may be empty) ``` Multiple events may be packed into a single Frame (coalescing). ### 10.1 Channel IDs | Value | Usage | |---|---| | 0 | Default channel | | 1–254 | Application-defined channels | | 255 | Internal (`channelCloseType`) | Event type 255 on channel ID 255 signals that the remote peer has closed that channel. The receiver evicts the channel and signals any blocked `Recv` callers with an error. --- ## 11. Numeric Limits | Constant | Value | |---|---| | `sizeHandshakeInit` | 148 bytes | | `sizeHandshakeResp` | 92 bytes | | `sizeCookieReply` | 64 bytes | | `sizeDataHeader` | 16 bytes | | `sizeFragmentHeader` | 8 bytes | | `sizeAEADTag` | 16 bytes | | `sizeKeepalive` / `sizeDisconnect` | 32 bytes | | `defaultMaxPacketSize` | 1232 bytes | | `defaultMaxFragPayload` | 1192 bytes | | `maxReassemblyBufs` (default) | 64 | | `windowSize` (replay) | 64 | | `maxTimestampSkew` | 180 s | | `cookieRotation` | 2 min | | `rekeyAfterTime` | 180 s | | `rekeyAfterMessages` | 2^60 | | Maximum fragments per frame | 65 535 | | Maximum channels | 256 (IDs 0–255) | --- ## 12. Implementation Notes ### Batch sends (Linux) On Linux, outgoing fragments are sent in a single `sendmmsg(2)` syscall via `ipv4.PacketConn.WriteBatch` (IPv4 sockets) or `ipv6.PacketConn.WriteBatch` (IPv6 sockets). On other platforms, fragments are sent in a loop of individual `sendmsg` calls. ### Batch receives Incoming UDP datagrams are read in batches of up to 64 (server) or 16 (client) messages per `recvmmsg(2)` syscall via `ipv4.PacketConn.ReadBatch`. ### Buffer pools Send and receive paths use `sync.Pool`-backed byte slices to reduce GC pressure. AEAD operations write ciphertext and plaintext in-place into pool buffers; no additional allocation occurs on the hot path when buffers have sufficient capacity. ### Socket buffers Both client and server request 4 MiB `SO_RCVBUF` / `SO_SNDBUF`. On Linux, this may be silently clamped by `net.core.rmem_max` / `wmem_max` (default ≈ 208 KiB). To guarantee the full 4 MiB, either raise the sysctl: ```bash sysctl -w net.core.rmem_max=4194304 sysctl -w net.core.wmem_max=4194304 ``` or use `SO_RCVBUFFORCE` (requires `CAP_NET_ADMIN`). In Docker, pass `--sysctl net.core.rmem_max=4194304`.
nextwave's avatar
nextwave 3 weeks ago
Part of me is thinking that Claude Code is the result of someone finally figuring out how to combine dev and gambling.
nextwave's avatar
nextwave 3 weeks ago
“The mind is its own place, and in itself can make a heaven of hell, a hell of heaven.” — John Milton image