2026-02-26 13:29:13 CET

npub145…kgem6 on Nostr: # Wiresocket Protocol Specification > **DRAFT** — This document reflects ...

# 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.