73 lines
7.6 KiB
Markdown
73 lines
7.6 KiB
Markdown
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
|
|
|
|
# smolmail 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](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 demands, from HTTP and TLS to WebPKI certificates and 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, and 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@host` address, not a second kind of address, and the key in a `smol://` path plays no part in the dial. The port in an address is the server's TCP port and selects the path, and the WebSocket itself is on 443. Nothing advertises that a host offers this endpoint. A client learns by trying.
|
|
|
|
- A client MUST dial `/smol` for an address that names port 1961 explicitly, so that each upstream has one path.
|
|
- The client MUST offer the subprotocol `smolmail.1` and 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.
|
|
|
|
The endpoint is `wss://` because a browser refuses `ws://` from an `https://` 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 `Origin` by 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 an `Origin` restriction 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 no `Origin` header 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, `SEND` needs neither `RESOLVE` nor 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. This is the first-contact interception §4 already describes, and its 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. 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](https://code.randogoth.com/randogoth/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](LICENSES/CC-BY-SA-4.0.txt)).
|