The port42 WebSocket Subprotocol, Version 1 Subprotocol Identifier: port42 Subprotocol Common Name: The Port42 Protocol Change Controller: Gordon Mattey, Port42 Date: 30 September 2026 1. Scope "port42" names the protocol carried on a WebSocket [RFC6455] connection between a Port42 instance, or a browser guest, and a Port42 relay. Two Port42 instances reach each other across networks through a relay. The relay pairs the two ends by public key and forwards data it cannot read, because the two ends run an end-to-end Noise session through it. A client offers "port42" in the Sec-WebSocket-Protocol header field. A relay SHOULD accept connections that offer "port42" and connections that offer no subprotocol. Port42 1.0.5 and later send the header field, and so does the browser guest, which falls back to no subprotocol for a relay that does not echo it. The key words "MUST", "MUST NOT", "SHOULD" and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals. 2. Peer Identifiers A peer identifier is the base32 encoding [RFC4648] of a 32-byte Ed25519 public key [RFC8032], using the lowercase alphabet "abcdefghijklmnopqrstuvwxyz234567" and no padding. It is always 52 characters. A receiver MUST accept an uppercase peer identifier and MUST lowercase it before comparison. 3. Relay Protocol A relay serves a WebSocket at the path "/v1" over TLS [RFC8446], and GET /health. Control messages are JSON in text frames, each with a member "t" naming its type. Session data travels in binary frames. Every connection begins the same way: 1. Relay to client: {"t": "challenge", "relay": , "nonce": <32 hex chars>} "relay" is the host name the client connected to. 2. Client to relay: {"t": "hello", "role": "host" | "guest", "key": , "sig": } "sig" is the standard base64 encoding of an Ed25519 signature, by the key named in "key", over the ASCII string "port42-relay-v1|" relay "|" nonce "|" role 3. Relay to client: {"t": "ok"}, or {"t": "error", "code": "bad_hello", "message": ...} and the connection closes. Binding the relay name into the signature stops a hello signed for one relay being replayed at another. Because a host proves its key, a guest that asks for a key reaches that key's holder or nobody. A host keeps its connection open. A guest then asks for a session: 4. Guest to relay: {"t": "open", "to": } 5. Relay to host: {"t": "incoming", "sid": <32 hex chars>, "from": } 6. Host to relay: {"t": "accept", "sid"} or {"t": "refuse", "sid", "code"} 7. Relay to guest: {"t": "opened", "sid"}, or {"t": "error", "code"} with one of host_offline, refused, rate_limited, limit. 8. Data: binary frames. On the host's connection each frame begins with the 16 raw bytes of the session identifier. The guest's connection is one session and its frames carry no prefix. 9. End: {"t": "close", "sid"} from either side, or the socket closing. "from" is advisory. The host learns the guest's identity from the Noise handshake. A relay pings every client every 20 seconds, and clients that can send pings do the same, so that proxies in front of a relay keep quiet connections open. A connection whose ping goes unanswered is closed. The default limits in version 1 are a data frame of at most 64 KiB plus 64 bytes, session identifier included; 32 open sessions per host; 4 open sessions per guest key; 30 "open" requests a minute per client address and 10 a minute per target host; 15 seconds for a host to accept; a session idle for 5 minutes is closed. A refusal names the limit it hit. A relay holds only in-memory state, namely which host keys are connected and which sockets each session pairs. It stores nothing and logs only counts. A host MAY register on several relays. Each relay is one process with its own host name, so a session never spans relay processes. 4. Noise IK Session Once "opened", the two ends run a Noise [NOISE] handshake with the pattern Noise_IK_25519_ChaChaPoly_SHA256 and the prologue "port42-noise-v1". The guest is the initiator. Static keys are derived from the Ed25519 keys. An instance's X25519 private key [RFC7748] is the first 32 bytes of SHA-512 of its Ed25519 seed, as Ed25519 itself derives its scalar. A peer's X25519 public key is its Ed25519 public key converted from Edwards to Montgomery form. The initiator knows the responder's static key in advance from the peer identifier it dialed. The initiator's first handshake message carries, as its payload, the initiator's 32-byte Ed25519 public key. The responder MUST convert that key to Montgomery form and MUST refuse the session if the result differs from the initiator static key the handshake authenticated. The responder then treats the peer identifier of that Ed25519 key as authenticated. The responder's handshake message carries an empty payload. A relay that is compromised can drop, delay, or reorder sessions. It cannot read or alter their content. An altered frame fails authentication at the receiver and ends the session. 5. Session Framing A Noise transport message holds at most 65,535 bytes, including its 16-byte authentication tag. A logical message is split into chunks, each carried as the plaintext of one transport message. Each chunk is a one-byte header followed by part of the message. The header is 0x01 when more chunks follow and 0x00 on the last chunk. An empty message is one chunk. A receiver MUST refuse a message that grows past 8 MiB. 6. Inside a Session Each logical message is one JSON [RFC8259] envelope. Envelope members use snake case. The guest sends calls: {"type": "call", "call_id": , "method": , "args": {...}} The host returns zero or more "stream" envelopes and then one "response" envelope for each call_id. Each carries {"payload": {"content": , ...}}. An error is {"type": "error", "error", "code", "call_id"}. A guest sends only "call" envelopes; any other type is refused with "unknown_method". A remote call carries no credential. The host identifies the caller by the peer identifier that the Noise handshake authenticated. The methods a call may name, with their arguments, are listed in the Port42 method reference . 7. Security Status Sharing between Port42 instances is new and has not been independently audited. 8. References [NOISE] Perrin, T., "The Noise Protocol Framework", Revision 34, 11 July 2018, . [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. [RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, October 2006. [RFC6455] Fette, I. and A. Melnikov, "The WebSocket Protocol", RFC 6455, December 2011. [RFC7748] Langley, A., Hamburg, M., and S. Turner, "Elliptic Curves for Security", RFC 7748, January 2016. [RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, January 2017. [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, May 2017. [RFC8259] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, December 2017. [RFC8446] Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, August 2018.