Protocol · September 2026

The port42 WebSocket subprotocol

What travels between a Port42 machine, or a browser, and a Port42 relay: the relay handshake, the end-to-end Noise session inside it, and the framing.

What this is and isn't
  • The definition behind the WebSocket subprotocol name "port42".
  • It covers the connection to a relay only. The full Port42 protocol will be published separately.
  • Port42 1.0.5 and later, and the browser guest, send the name in the handshake.
  • Sharing between machines is new and not independently audited.

Plain text version

The port42 WebSocket Subprotocol, Version 1

Subprotocol Identifier:  port42
Subprotocol Common Name:  The Port42 Protocol
Change Controller:  Gordon Mattey, Port42 <gordon@port42.ai>
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": <host>, "nonce": <32 hex chars>}

       "relay" is the host name the client connected to.

   2.  Client to relay:

         {"t": "hello", "role": "host" | "guest",
          "key": <peer id>, "sig": <base64>}

       "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": <host peer id>}

   5.  Relay to host: {"t": "incoming", "sid": <32 hex chars>,
       "from": <guest peer id>}

   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": <id>, "method": <name>,
      "args": {...}}

   The host returns zero or more "stream" envelopes and then one
   "response" envelope for each call_id.  Each carries
   {"payload": {"content": <JSON text>, ...}}.  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
   <https://port42.ai/llms.txt>.

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, <https://noiseprotocol.org/noise.html>.

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