7.6 KiB
Smol Mail over WebSocket -- annex
This annex is not part of the protocol. It adds no version, no section, no address form and no label, and a server that ignores it is fully conformant. It records one convention for carrying the transport of §4 through a WebSocket, so that browsers, which cannot open TCP sockets, and clients on networks that pass only port 443 can reach a mailbox, and so that independent implementations of that carriage agree. The keywords below bind only an implementation that claims to follow this annex. Section references are to SPEC.md.
Why an annex and not a transport
Reticulum (§13) replaced the transport and gave the protocol a mailbox that works without the internet. A WebSocket replaces nothing: it is a pipe around §4, and everything it needs -- HTTP, TLS, WebPKI certificates, Origin policy -- is what §4 was written to do without. Keeping that in a separate process keeps it out of the mailbox, whose pre-authentication surface stays length u32 || type u8.
Carriage
- The WebSocket carries the exact byte stream of §4: the Noise_NX handshake, the 2-byte Noise length prefixes and the application frames, unchanged in both directions.
- Bytes travel in binary messages. Message boundaries carry no meaning and are not aligned to Noise messages; a receiver MUST treat the messages as one continuous stream.
- A sender MUST NOT emit a WebSocket message larger than 64 KiB, and a receiver MAY close on one that is; a full Noise message with its prefix is 65537 bytes, one over, so a full one does not fit.
- A text message is an error, and the receiver MUST close the connection.
- WebSocket close codes carry no meaning. Every status is a §12 byte inside the Noise channel. A failed upgrade, a refused origin and a dropped socket are local errors, and a client MUST NOT report them as a status.
Endpoint
alice@example.org → wss://example.org/smol
alice@example.org:1962 → wss://example.org/smol/1962
- Addresses are those of §3, unchanged. This is a second way to dial a
user@hostaddress, not a second kind of address. The key in asmol://path plays no part in the dial. - The port in an address is the server's TCP port and selects the path. The WebSocket itself is on 443.
- A client MUST dial
/smolfor an address that names port 1961 explicitly, so that each upstream has one path. - The client MUST offer the subprotocol
smolmail.1and the endpoint MUST select it. A client MUST abort when the response selects anything else or nothing: RFC 6455 lets a handshake succeed with no subprotocol, and the peer is then not known to speak this one. - Nothing advertises that a host offers this endpoint. A client learns by trying.
- The endpoint is
wss://because a browser refusesws://from anhttps://page and because TLS on 443 is what restrictive networks pass, not because TLS protects anything: §4 is plain TCP and the Noise channel supplies the confidentiality on either.ws://MAY be used where neither reason applies, such as loopback and onion services.
Trust
The TLS certificate is a transport formality that browsers demand, and it is not part of the trust model. The pin is the Noise static key, exactly as in §4, and a client MUST apply every rule of §4 and §8 as if it had dialled TCP. A valid certificate says nothing about who holds the mailbox, and a client MUST NOT present it as if it did.
Because the endpoint is a pipe to the same server, the static key, the handshake hash and the server_static that AUTH and REGISTER sign over are the ones a TCP client sees. A pin made over one route holds over the other.
The sidecar
The endpoint is a process beside the server, behind a TLS-terminating reverse proxy.
- It MUST NOT relay to an upstream the operator did not configure. The path selects among configured upstreams and does nothing else; a path that names none is refused, and a host is never taken from the request.
- It holds no keys and parses nothing past the WebSocket framing.
- It admits any
Originby default, because a browser client hosted elsewhere has to be able to deliver here. An operator MAY restrict origins at the price of refusing those senders, but anOriginrestriction is not access control: only browsers send the header truthfully, it stops a page the operator did not approve and nothing more, and a connection with noOriginheader is admitted. - It SHOULD NOT listen where the TCP port would not. A sidecar on loopback or a private network is reachable from every page the local browser opens, which the TCP port never was.
Limits and privacy
The server sees every connection arrive from the sidecar's address, so the per-IP limits of §10 see one peer; the sidecar or the proxy SHOULD enforce them in its place, reading the client address from a proxy header only when the proxy is one the operator configured, and the server's own limits have to be raised to carry the sidecar's whole traffic. The per-accept-token SEND limit is unaffected. §9 holds with one change: the connecting IP address is learned by the proxy and the sidecar rather than by the server. All three belong to the same operator, so the operator learns what it did before. A network observer sees TLS to port 443 and the host name instead of a connection to port 1961.
Clients
- A browser client SHOULD dial the endpoint of the host in the address, and MAY fall back to a relay when the host offers none.
- A native client MAY dial the endpoint when TCP is unreachable.
- A browser client SHOULD steer users to
smol://addresses for contacts: with the recipient's key in hand,SENDneeds neitherRESOLVEnor a pinned server (§4). This does not cover the client's own server, whose key still has to come from a trusted channel. - Reverse proxies close idle connections, commonly after a minute. A client SHOULD open a connection per batch of work and close it when done, as §13.10 advises for links.
Relays
Most servers are TCP only, so a browser client reaches most hosts through a relay: a process run for the client rather than for a mailbox, which takes the host and port from the request and pipes the same byte stream to them over TCP. It is the opposite role to a sidecar, and the rule against unconfigured upstreams does not apply to it. The carriage is the one above; the request form is not specified here, because a relay serves the pages of its own origin and the two need agree only with each other.
A relay sees ciphertext only when the host is pinned, and a relay that tampers then produces a failed handshake or a pin mismatch. Against an unpinned host a relay can complete the handshake itself and present its own key, with no failure to notice: the first-contact interception §4 already describes, whose rules are the defence -- the session is unverified, MUST be shown as such, and MUST NOT carry REGISTER, FETCH or DELETE. A relay does learn the client's IP address and every host the client contacts, and the server learns the relay's address in place of the client's, so a client SHOULD use only a relay run by a party it would trust with that list; for a browser client that is the origin serving its code, which it already trusts with the master. Bounding a relay as a proxy is its operator's hardening and outside this annex.
Status
Only the byte format is implemented, by the gsmol bridge acting as a relay. The endpoint, the subprotocol name and the sidecar are not implemented anywhere yet.
License
© 2026 randogoth. Licensed under the Creative Commons Attribution-ShareAlike 4.0 International license (CC BY-SA 4.0).