UDP frame forwarding
UDP forwarding is one application composition of Layer Minus: it relays UDP-shaped application frames between two wallets. L0 itself only forwards OpenPGP (and, here, AES) ciphertext by wallet / key ID — see How to use Layer Minus. It is not a raw operating-system UDP socket to mailbox B and does not change the TCP/IP underlay.
Public site: https://gitbook.conet.network/l0/udp-forward.html
The CoNET-SI runtime and diagnostic harness implement the protocol described here. Client integration remains application-specific; this is not a promise that arbitrary UDP software works without an adapter.
What it is
The client and UDP server use AES-256-GCM for frame content. OpenPGP is used only to deliver the symmetric-key subscription and to protect commands addressed to mailbox B.
| Actor | Responsibility |
|---|---|
| Client wallet | Creates the AES key and sessionId; opens a receive listen; encrypts uplink frames |
| UDP server wallet | Has an AddressPGP user key and route; decrypts the subscription; encrypts downlink frames |
| Mailbox B | Hosts UDP session state and relays AES ciphertext; must not learn the AES key |
| Entries A/C/D | Accept client and server requests and forward them to B |
Each client or server request uses a healthy entry different from B. SI-to-SI forwarding remains HTTP on port 80. The selected entry sees its direct client's IP; B sees the forwarding entry rather than a direct client connection.
Encryption and route matrix
| Operation | Contains symmetric key? | Encrypt to | Transport target |
|---|---|---|---|
udp_subscribe |
Yes | UDP server user PGP | Entry A, then ordinary business routing to B |
Client udp_listen |
No | B route PGP | Entry C, long-lived SSE through B |
Server udp_server_listen |
No | B route PGP | An entry different from B, long-lived SSE |
udp_relay server → client |
No; AES payload only | B route PGP | Entry D |
udp_uplink client → server |
No; AES payload only | B route PGP | Entry D |
udp_unlisten |
No | B route PGP | An entry different from B |
The key rule is strict: udp_subscribe is a user-PGP business message, not a route command. If B can decrypt it, the client used the wrong key. SI rejects that case with encrypt_to_udp_server_user_pgp.
How it works
- The UDP server registers through the same AddressPGP flow used by Chat.
- The client creates a random 32-byte AES key and a session ID.
- The client signs and encrypts
udp_subscribeto the server's user PGP, then posts it through entry A. - The server receives and decrypts that subscription through its normal user-PGP Chat delivery path.
- The client opens
udp_listen, encrypted to B's route key, through entry C. - B verifies
isMyRoute(udpServerWallet), creates the session, and sendsudp_listen_attachedto the server. - The server sends AES ciphertext with
udp_relay; B writes a downlink frame to the client SSE. - The client sends AES ciphertext with
udp_uplink; B writes an uplink frame to a dedicated UDP-server SSE or, if available, the server's Chat listen. udp_unlistencloses the client session and emitsudp_listen_detached.
The subscription itself arrives on the server's Chat listen because it is user-PGP business ciphertext. The UDP frame sessions use separate UDP pools.
Canonical command fields
Commands are JSON strings inside the signed Layer Minus envelope:
signMessage = personal_sign(message)
literal = base64(UTF8(JSON.stringify({ message, signMessage })))
armored = OpenPGP_encrypt(literal, selected public key)
udp_subscribe:
{
"command": "udp_subscribe",
"walletAddress": "<client EOA>",
"udpServerWallet": "<server EOA>",
"sessionId": "<session identifier>",
"algorithm": "aes-256-gcm",
"Securitykey": "<base64 32-byte key>",
"timestamp": 1710000000
}
Client listen:
{
"command": "udp_listen",
"listenKind": "udp",
"walletAddress": "<client EOA>",
"udpServerWallet": "<server EOA>",
"sessionId": "<session identifier>",
"timestamp": 1710000000
}
The equivalent command: "mining" with listenKind: "udp" is also accepted. A server can use udp_server_listen or command: "mining" with listenKind: "udp_server".
Relay and uplink commands contain walletAddress, sessionId, payload, and timestamp. They must never contain Securitykey.
Frame format and runtime bounds
| Item | Current rule | ||||
|---|---|---|---|---|---|
| AES key | 32 bytes; standard base64 in the user-PGP subscription | ||||
| Payload | `base64(nonce[12] \ | \ | ciphertext \ | \ | tag[16])` |
| Maximum encoded payload | 12,000 characters | ||||
| Accepted timestamp skew | 600 seconds | ||||
| Session limits | 16 per client, 64 per server, 256 globally | ||||
| Idle timeout | 10 minutes | ||||
| Scheduling | Non-overlapping setTimeout sweep |
||||
| Pool isolation | UDP client/server pools remain separate from chat/mining liveness |
A receive-only SSE socket can remain writable after its request body has ended. SI therefore does not classify readableEnded alone as stale.
Guarantees and non-guarantees
If udp_subscribe is encrypted to the server's user key and route commands omit the key, mailbox B can relay frames without learning their plaintext or AES key. Wallet signatures bind commands to their declared actors, and B validates route and session ownership before forwarding.
The protocol does not provide raw UDP semantics, automatic retransmission, application-level ordering for both directions, congestion control, or proof that the destination application consumed a frame. SI's delivered: true means a frame was written to an active SSE path; it is not an application acknowledgement.
Mailbox B still sees sessionId, client and server wallets, frame timing and size, direction, and session duration. That is enough to classify some realtime or sensor-like use even when AES payloads stay sealed.
It also does not hide a client's IP from its entry, defeat a global timing observer, or implement padding buckets, constant-rate mode, a replay window beyond the 600-second timestamp skew, application ACKs, or automatic rekey. Those are listed as upgrades in security limits.
Implementation anchors
- Session and authorization logic:
src/CoNET-SI/src/util/udpForward.ts - AES-256-GCM framing:
src/CoNET-SI/src/util/udpForwardCrypto.ts - SI command dispatch:
src/CoNET-SI/src/util/localNodeCommand.ts - Entry-routed diagnostic:
npx tsx scripts/testConetDepinUdp.ts
Next
Relationship to Chat real-time voice
The real-time voice MVP uses the same zero-trust frame principles as this
UDP composition, but it is not raw UDP and it does not reuse the normal Chat
mailbox SSE. Each participant opens a separate random voice_listen session;
voice_uplink and voice_downlink commands write AES-GCM frames to the peer's
temporary voice SSE. Voice sessions have their own limits and lifecycle and
must not be inserted into udpClientPool, udpServerPool, or the normal Chat
offline store. The mailbox never decrypts either UDP or voice payloads.
- How to use Layer Minus places UDP among the other L0 combinations.
- Security limits grades UDP metadata and missing native-UDP properties.
- Zero-trust mailbox routing defines the shared A/B/C path.
- Wallet-addressed peer identity defines user and route keys.
- Node and client roles defines pool and plaintext boundaries.