From 35d4065f325373ca883b9388d826b344a14b8cd9 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:32:02 +0000 Subject: [PATCH] specs for transport --- spec/TOPICS.md | 6 + .../modules/Simplex/FileTransfer/Transport.md | 23 ++++ .../Messaging/Notifications/Transport.md | 36 ++++++ spec/modules/Simplex/Messaging/Transport.md | 114 ++++++++++++++++++ .../Simplex/Messaging/Transport/Buffer.md | 17 +++ .../Simplex/Messaging/Transport/Client.md | 23 ++++ .../Messaging/Transport/Credentials.md | 13 ++ .../Simplex/Messaging/Transport/HTTP2.md | 13 ++ .../Messaging/Transport/HTTP2/Client.md | 19 +++ .../Simplex/Messaging/Transport/HTTP2/File.md | 7 ++ .../Messaging/Transport/HTTP2/Server.md | 15 +++ .../Simplex/Messaging/Transport/KeepAlive.md | 11 ++ .../Simplex/Messaging/Transport/Server.md | 33 +++++ .../Simplex/Messaging/Transport/Shared.md | 28 +++++ .../Simplex/Messaging/Transport/WebSockets.md | 15 +++ src/Simplex/FileTransfer/Transport.hs | 2 + src/Simplex/Messaging/Transport.hs | 6 + .../Messaging/Transport/Credentials.hs | 1 + .../Messaging/Transport/HTTP2/Client.hs | 1 + src/Simplex/Messaging/Transport/Server.hs | 2 + src/Simplex/Messaging/Transport/Shared.hs | 1 + src/Simplex/Messaging/Transport/WebSockets.hs | 1 + 22 files changed, 387 insertions(+) create mode 100644 spec/modules/Simplex/FileTransfer/Transport.md create mode 100644 spec/modules/Simplex/Messaging/Notifications/Transport.md create mode 100644 spec/modules/Simplex/Messaging/Transport.md create mode 100644 spec/modules/Simplex/Messaging/Transport/Buffer.md create mode 100644 spec/modules/Simplex/Messaging/Transport/Client.md create mode 100644 spec/modules/Simplex/Messaging/Transport/Credentials.md create mode 100644 spec/modules/Simplex/Messaging/Transport/HTTP2.md create mode 100644 spec/modules/Simplex/Messaging/Transport/HTTP2/Client.md create mode 100644 spec/modules/Simplex/Messaging/Transport/HTTP2/File.md create mode 100644 spec/modules/Simplex/Messaging/Transport/HTTP2/Server.md create mode 100644 spec/modules/Simplex/Messaging/Transport/KeepAlive.md create mode 100644 spec/modules/Simplex/Messaging/Transport/Server.md create mode 100644 spec/modules/Simplex/Messaging/Transport/Shared.md create mode 100644 spec/modules/Simplex/Messaging/Transport/WebSockets.md diff --git a/spec/TOPICS.md b/spec/TOPICS.md index a8eafc1a1..d7a627254 100644 --- a/spec/TOPICS.md +++ b/spec/TOPICS.md @@ -7,3 +7,9 @@ - **Padding schemes**: Three different padding formats across the codebase — Crypto.hs uses 2-byte Word16 length prefix (max ~65KB), Crypto/Lazy.hs uses 8-byte Int64 prefix (file-sized), and both use '#' fill character. Ratchet header padding uses fixed sizes (88 or 2310 bytes). All use `pad`/`unPad` but with incompatible formats. The relationship between padding, encryption, and message size limits spans Crypto, Lazy, Ratchet, and the protocol layer. - **NaCl construction variants**: crypto_box, secret_box, and KEM hybrid secret all use the same XSalsa20+Poly1305 core (Crypto.hs `xSalsa20`), but with different key sources (DH, symmetric, SHA3_256(DH||KEM)). The lazy streaming variant (Lazy.hs) adds prepend-tag vs tail-tag placement. File.hs wraps lazy streaming with handle-based I/O. Full picture requires reading Crypto.hs, Lazy.hs, File.hs, and SNTRUP761.hs together. + +- **Transport encryption layering**: Three encryption layers overlap — TLS (Transport.hs), optional block encryption via sbcHkdf chains (Transport.hs tPutBlock/tGetBlock), and SMP protocol-level encryption. Block encryption is disabled for proxy connections (already encrypted), and absent for NTF protocol. The interaction of these layers with proxy version downgrade logic spans Transport.hs, Client.hs, and the SMP proxy module. + +- **Certificate chain trust model**: ChainCertificates (Shared.hs) defines 0–4 cert chain semantics, used by both Client.hs (validateCertificateChain) and Server.hs (validateClientCertificate, SNI credential switching). The 4-length case skipping index 2 (operator cert) and the FQHN-disabled x509validate are decisions that span the entire transport security model. + +- **Handshake protocol family**: SMP (Transport.hs), NTF (Notifications/Transport.hs), and XFTP (FileTransfer/Transport.hs) all have handshake protocols with the same structure (version negotiation + session binding + key exchange) but different feature sets. NTF is a strict subset. XFTP doesn't use the TLS handshake at all (HTTP2 layer). The shared types (THandle, THandleParams, THandleAuth) mean changes to the handshake infrastructure affect all three protocols. diff --git a/spec/modules/Simplex/FileTransfer/Transport.md b/spec/modules/Simplex/FileTransfer/Transport.md new file mode 100644 index 000000000..6bad5455c --- /dev/null +++ b/spec/modules/Simplex/FileTransfer/Transport.md @@ -0,0 +1,23 @@ +# Simplex.FileTransfer.Transport + +> XFTP protocol types, version negotiation, and encrypted file streaming with integrity verification. + +**Source**: [`FileTransfer/Transport.hs`](../../../../src/Simplex/FileTransfer/Transport.hs) + +## xftpClientHandshakeStub — XFTP doesn't use TLS handshake + +`xftpClientHandshakeStub` always fails with `throwE TEVersion`. The source comment states: "XFTP protocol does not use this handshake method." The XFTP handshake is performed at the HTTP/2 layer — `XFTPServerHandshake` and `XFTPClientHandshake` are sent as HTTP/2 request/response bodies (see `FileTransfer/Client.hs` and `FileTransfer/Server.hs`). + +## receiveSbFile — constant-time auth tag verification + +`receiveSbFile` validates the authentication tag using `BA.constEq` (constant-time byte comparison). The auth tag is collected from the stream after all file data — if the file data ends mid-chunk, the remaining bytes of that chunk are used first, and a follow-up read provides the rest of the tag if needed. + +## receiveFile_ — two-phase integrity verification + +File reception has two verification phases: +1. **During receive**: either size checking (plaintext via `hReceiveFile`) or auth tag validation (encrypted via `receiveSbFile`) +2. **After receive**: `LC.sha256Hash` of the entire received file is compared to `chunkDigest` + +## sendEncFile — auth tag appended after all chunks + +`sendEncFile` streams encrypted chunks via `LC.sbEncryptChunk`, then sends `LC.sbAuth sbState` (the authentication tag) as a final frame when the remaining size reaches zero. diff --git a/spec/modules/Simplex/Messaging/Notifications/Transport.md b/spec/modules/Simplex/Messaging/Notifications/Transport.md new file mode 100644 index 000000000..dd4564738 --- /dev/null +++ b/spec/modules/Simplex/Messaging/Notifications/Transport.md @@ -0,0 +1,36 @@ +# Simplex.Messaging.Notifications.Transport + +> Notification Router Protocol transport: manages push notification subscriptions between client and NTF Router. + +**Source**: [`Notifications/Transport.hs`](../../../../../src/Simplex/Messaging/Notifications/Transport.hs) + +**Protocol spec**: [`protocol/push-notifications.md`](../../../../../protocol/push-notifications.md) — SimpleX Notification Router protocol. + +## Overview + +This module implements the transport layer for the **Notification Router Protocol**. Per the protocol spec: "To manage notification subscriptions to SMP routers, SimpleX Notification Router provides an RPC protocol with a similar design to SimpleX Messaging Protocol router." + +The protocol spec diagram shows three separate protocols in the notification flow: +1. **Notification Router Protocol** (this module): client ↔ SimpleX Notification Router — subscription management +2. **SMP protocol**: SMP Router → SimpleX Notifications Subscriber — notification signals +3. **Push provider** (e.g., APN): SimpleX Push Router → device — per the spec: "the notifications are e2e encrypted between SimpleX Notification Router and the user's device" + +## Differences from SMP transport + +The NTF protocol reuses SMP's transport infrastructure but with reduced parameters: + +| Property | SMP | NTF | +|----------|-----|-----| +| Block size | 16384 | 512 | +| Block encryption | Yes (v11+) | No (`encryptBlock = Nothing`) | +| Service certificates | Yes (v16+) | No (`serviceAuth = False`) | +| Version range | 6–19 | 1–3 | +| Handshake messages | 2–3 | 2 | + +## Same ALPN/legacy fallback pattern as SMP + +`ntfServerHandshake` uses the same pattern as `smpServerHandshake`: if ALPN is not negotiated (`getSessionALPN` returns `Nothing`), the server offers only `legacyServerNTFVRange` (v1 only). + +## NTF handshake uses SMP shared types + +The handshake reuses SMP's `THandle`, `THandleParams`, `THandleAuth` types. The `encodeAuthEncryptCmds` and `authEncryptCmdsP` helper functions are defined locally in this module (with NTF-specific version thresholds). NTF never sets `sessSecret` / `sessSecret'`, `peerClientService`, or `clientService` — these are always `Nothing`. diff --git a/spec/modules/Simplex/Messaging/Transport.md b/spec/modules/Simplex/Messaging/Transport.md new file mode 100644 index 000000000..f88188792 --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport.md @@ -0,0 +1,114 @@ +# Simplex.Messaging.Transport + +> SMP transport layer: TLS connection management, SMP handshake protocol, block encryption, version negotiation. + +**Source**: [`Transport.hs`](../../../../src/Simplex/Messaging/Transport.hs) + +**Protocol spec**: [`protocol/simplex-messaging.md` — Transport connection](../../../../protocol/simplex-messaging.md#transport-connection-with-the-smp-router) — SMP encrypted transport, handshake syntax, certificate chain requirements. + +## Overview + +This is the core transport module. It defines: +- The `Transport` typeclass abstracting over TLS and WebSocket connections +- The SMP handshake protocol (server and client sides) +- Optional block encryption using HKDF-derived symmetric key chains (v11+) +- Version negotiation with backward-compatible extensions + +Per the protocol spec: "Each transport block has a fixed size of 16384 bytes for traffic uniformity." The `sessionIdentifier` field uses tls-unique channel binding (RFC 5929) — "it should be included in authorized part of all SMP transmissions sent in this transport connection." + +## SMP version 13 is missing + +The version history jumps from 12 (`blockedEntitySMPVersion`) to 14 (`proxyServerHandshakeSMPVersion`). Version 13 was skipped. + +## proxiedSMPRelayVersion — anti-fingerprinting cap + +`proxiedSMPRelayVersion = 18`, one below `currentClientSMPRelayVersion = 19`. The code comment states: "SMP proxy sets it to lower than its current version to prevent client version fingerprinting by the destination relays when clients upgrade at different times." + +In practice (Server.hs), the SMP proxy uses `proxiedSMPRelayVRange` to cap the destination relay's version range in the `PKEY` response sent to the client, so the client sees a capped version range rather than the relay's actual range. + +## withTlsUnique — different API calls yield same value + +`withTlsUnique` extracts the tls-unique channel binding (RFC 5929) using a type-level dispatch: +- **Server** (`STServer`): `T.getPeerFinished` — the peer's (client's) Finished message +- **Client** (`STClient`): `T.getFinished` — own (client's) Finished message + +Both calls yield the client's Finished message. If the result is `Nothing`, the connection is closed immediately (`closeTLS cxt >> ioe_EOF`). + +## defaultSupportedParams vs defaultSupportedParamsHTTPS + +Two TLS parameter sets: + +- **`defaultSupportedParams`**: ChaCha20-Poly1305 ciphers only, Ed448/Ed25519 signatures only, X448/X25519 groups. Per the protocol spec: "TLS_CHACHA20_POLY1305_SHA256 cipher suite, ed25519 EdDSA algorithms for signatures, x25519 ECDHE groups for key exchange." +- **`defaultSupportedParamsHTTPS`**: extends `defaultSupportedParams` with `ciphersuite_strong`, additional groups, and additional hash/signature combinations. The source comment says: "A selection of extra parameters to accomodate browser chains." + +In the SMP server (Server.hs), when HTTP credentials are configured, `defaultSupportedParamsHTTPS` is used for all connections on that port (not selected per-connection). When no HTTP credentials are configured, `defaultSupportedParams` is used. + +## SMP handshake flow + +Per the [protocol spec](../../../../protocol/simplex-messaging.md#transport-handshake), the handshake is a two-message exchange (three if service certs are used): + +1. **Server → Client**: `paddedRouterHello` containing `smpVersionRange`, `sessionIdentifier` (tls-unique), and `routerCertKey` (certificate chain + X25519 key signed by the server's certificate) +2. **Client → Server**: `paddedClientHello` containing agreed `smpVersion`, `keyHash` (router identity — CA certificate fingerprint), optional `clientKey`, `proxyRouter` flag, and optional `clientService` +3. **Server → Client** (service only): `paddedRouterHandshakeResponse` containing assigned `serviceId` or `handshakeError` + +The client verifies `sessionIdentifier` matches its own tls-unique (`when (sessionId /= sessId) $ throwE TEBadSession`). The server verifies `keyHash` matches its CA fingerprint (`when (keyHash /= kh) $ throwE $ TEHandshake IDENTITY`). + +Per the protocol spec: "For TLS transport client should assert that sessionIdentifier is equal to tls-unique channel binding defined in RFC 5929." + +### legacyServerSMPRelayVRange when no ALPN + +If ALPN is not negotiated (`getSessionALPN c` returns `Nothing`), the server offers `legacyServerSMPRelayVRange` (v6 only) instead of the full version range. Per the protocol spec: "If the client does not confirm this protocol name, the router would fall back to v6 of SMP protocol." The spec notes: "This is added to allow support of older clients without breaking backward compatibility and to extend or modify handshake syntax." + +### Service certificate handshake extension + +When `clientService` is present in the client handshake, the server performs additional verification: +- The TLS client certificate chain must exactly match the certificate chain in the handshake message (`getPeerCertChain c == cc`) +- The signed X25519 public key is verified against the leaf certificate's key (`getCertVerifyKey leafCert` then `C.verifyX509`) +- On success, the server sends `SMPServerHandshakeResponse` with a `serviceId` +- On failure, the server sends `SMPServerHandshakeError` before raising the error + +Per the protocol spec (v16+): "`clientService` provides long-term service client certificate for high-volume services using SMP router (chat relays, notification routers, high traffic bots). The router responds with a third handshake message containing the assigned service ID." + +The client only includes service credentials when `v >= serviceCertsSMPVersion && certificateSent c` (the TLS client certificate was actually sent). + +## tPutBlock / tGetBlock — optional block encryption + +When `encryptBlock` is set, transport blocks are encrypted before being sent over TLS: + +- **Send**: `atomically $ stateTVar sndKey C.sbcHkdf` advances the chain key and returns `(SbKey, CbNonce)`; the block is encrypted with `C.sbEncrypt` +- **Receive**: same pattern with `rcvKey` and `C.sbDecrypt` + +The chain keys are initialized from `C.sbcInit sessionId secret` where `sessionId` is the tls-unique value and `secret` is the session DH shared secret. + +The code comment on `proxyServer` flag states: "This property, if True, disables additional transport encrytion inside TLS. (Proxy server connection already has additional encryption, so this layer is not needed there)." The protocol spec confirms: "`proxyRouter` flag (v14+) disables additional transport encryption inside TLS for proxy connections, since proxy router connection already has additional encryption." + +The protocol spec version history (v11) describes this as "additional encryption of transport blocks with forward secrecy." + +## smpTHandleClient — chain key swap + +`smpTHandleClient` applies `swap` to the chain key pair before creating `TSbChainKeys`. The code comment states: "swap is needed to use client's sndKey as server's rcvKey and vice versa." + +## Proxy version downgrade logic + +When the proxy connects to a destination relay older than v14 (`proxyServerHandshakeSMPVersion`), the client-side handshake caps the version range: + +``` +if proxyServer && maxVersion smpVersionRange < proxyServerHandshakeSMPVersion + then vRange {maxVersion = max (minVersion vRange) deletedEventSMPVersion} +``` + +The code comment explains: "Transport encryption between proxy and destination breaks clients with version 10 or earlier, because of a larger message size (see maxMessageLength)." The cap at `deletedEventSMPVersion` (v10) ensures transport encryption (v11+) is not negotiated with older relays. + +The comment also notes: "Prior to version v6.3 the version between proxy and destination was capped at 8, by mistake, which also disables transport encryption and the latest features." + +## forceCertChain + +`forceCertChain` forces evaluation of the certificate chain and signed key via `length (show cc) `seq` show signedKey `seq` cert`. Introduced in commit 9e7e0d10 ("smp-server: conserve resources"), sub-bullet "transport: force auth params, remove async wrapper" — part of a commit that adds strictness annotations throughout (`bang more thunks`, `strict`). + +## smpTHandle — version 0 bootstrap + +`smpTHandle` creates a `THandle` with version 0, no auth, and no block encryption. This handle is used for the handshake exchange itself (`sendHandshake`/`getHandshake`). After the handshake completes, `smpTHandle_` creates the real handle with the negotiated version, auth, and encryption parameters. + +## getHandshake — forward-compatible parsing + +The code comment states: "ignores tail bytes to allow future extensions." The protocol spec confirms: "`ignoredPart` in handshake allows to add additional parameters in handshake without changing protocol version — the client and routers must ignore any extra bytes within the original block length." diff --git a/spec/modules/Simplex/Messaging/Transport/Buffer.md b/spec/modules/Simplex/Messaging/Transport/Buffer.md new file mode 100644 index 000000000..6b1edf9fd --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/Buffer.md @@ -0,0 +1,17 @@ +# Simplex.Messaging.Transport.Buffer + +> Buffered TLS reading with TMVar-based concurrency lock. + +**Source**: [`Transport/Buffer.hs`](../../../../../src/Simplex/Messaging/Transport/Buffer.hs) + +## TBuffer — concurrent read safety via getLock + +`TBuffer` uses a `TMVar ()` as a mutex (`getLock`). `getBuffered` acquires the lock via `withBufferLock`, then loops and accumulates bytes until the requested count is reached. + +## getBuffered — first chunk has no timeout + +`getBuffered` reads the first chunk via `getChunk` (no timeout), but applies `withTimedErr t_` (the transport timeout) to subsequent chunks. + +## getLnBuffered — test only + +The source comment states: "This function is only used in test and needs to be improved before it can be used in production, it will never complete if TLS connection is closed before there is newline." diff --git a/spec/modules/Simplex/Messaging/Transport/Client.md b/spec/modules/Simplex/Messaging/Transport/Client.md new file mode 100644 index 000000000..bdc1cdb82 --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/Client.md @@ -0,0 +1,23 @@ +# Simplex.Messaging.Transport.Client + +> TLS client connection setup: TCP/SOCKS5 connection, TLS handshake, certificate validation, host types. + +**Source**: [`Transport/Client.hs`](../../../../../src/Simplex/Messaging/Transport/Client.hs) + +## ConnectionHandle — three-stage cleanup + +`ConnectionHandle` has three constructors: `CHSocket` (raw socket), `CHContext` (TLS context), `CHTransport` (transport connection). An `IORef` holds the current handle, updated by `set` on each successful transition. The `E.bracket` cleanup function tears down the connection at whatever stage it reached. + +## SocksIsolateByAuth + +`SocksIsolateByAuth` is the default SOCKS authentication mode. When active, [Simplex.Messaging.Client](../Client.md) generates SOCKS credentials (`SocksCredentials sessionUsername ""`) where `sessionUsername` is `B64.encode $ C.sha256Hash $ bshow userId <> ...` with additional components based on `sessionMode` (`TSMUser`, `TSMSession`, `TSMServer`, `TSMEntity`). + +The three modes defined here: `SocksAuthUsername` (explicit credentials), `SocksAuthNull` (no auth, `@` prefix), `SocksIsolateByAuth` (empty string — credentials generated by the caller). + +## validateCertificateChain + +Validation checks the SHA-256 fingerprint of the identity certificate (extracted via `chainIdCaCerts` — see [Shared.md](./Shared.md#chainidcacerts--certificate-chain-semantics)) against the key hash. If the fingerprint doesn't match, the chain is rejected with `UnknownCA`. If the fingerprint matches, standard X.509 validation is performed using the CA certificate as trust anchor. + +## No TLS timeout for client connections + +The code comment states: "No TLS timeout to avoid failing connections via SOCKS." `transportTimeout` is set to `Nothing` for all client connections via `clientTransportConfig`. diff --git a/spec/modules/Simplex/Messaging/Transport/Credentials.md b/spec/modules/Simplex/Messaging/Transport/Credentials.md new file mode 100644 index 000000000..9aefa53ae --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/Credentials.md @@ -0,0 +1,13 @@ +# Simplex.Messaging.Transport.Credentials + +> Certificate generation for transport layer: Ed25519 key pairs, X.509 signing, TLS credential extraction. + +**Source**: [`Transport/Credentials.hs`](../../../../../src/Simplex/Messaging/Transport/Credentials.hs) + +## genCredentials — nanosecond stripping + +`genCredentials` zeroes out nanoseconds from the current time before creating the certificate validity period: `todNSec = 0`. The source comment explains: "remove nanoseconds from time - certificate encoding/decoding removes them." + +## tlsCredentials — root fingerprint from last credential + +`tlsCredentials` extracts the SHA-256 fingerprint from `L.last credentials` (the root/CA certificate), and the private key from `L.head credentials` (the leaf). The returned `KeyHash` wraps this root fingerprint. diff --git a/spec/modules/Simplex/Messaging/Transport/HTTP2.md b/spec/modules/Simplex/Messaging/Transport/HTTP2.md new file mode 100644 index 000000000..ad4b5de57 --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/HTTP2.md @@ -0,0 +1,13 @@ +# Simplex.Messaging.Transport.HTTP2 + +> Bridges TLS transport to HTTP/2 configuration, buffer management, and body reading. + +**Source**: [`Transport/HTTP2.hs`](../../../../../src/Simplex/Messaging/Transport/HTTP2.hs) + +## allocHTTP2Config — manual buffer allocation + +`allocHTTP2Config` uses `mallocBytes` to allocate a write buffer (`Ptr Word8`) for the `http2` package's `Config`. The config bridges TLS to HTTP/2 by passing `cPut c` and `cGet c` from the `Transport` typeclass into the HTTP/2 config's `confSendAll` and `confReadN`. + +## http2TLSParams + +`http2TLSParams` uses `ciphersuite_strong_det` (from `Network.TLS.Extra`), distinct from the `ciphersuite_strong` used in `defaultSupportedParamsHTTPS`. This is the default `suportedTLSParams` in the HTTP/2 client configuration. diff --git a/spec/modules/Simplex/Messaging/Transport/HTTP2/Client.md b/spec/modules/Simplex/Messaging/Transport/HTTP2/Client.md new file mode 100644 index 000000000..0ff0e0af8 --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/HTTP2/Client.md @@ -0,0 +1,19 @@ +# Simplex.Messaging.Transport.HTTP2.Client + +> Thread-safe HTTP/2 client with request queuing, connection lifecycle, and timeout management. + +**Source**: [`Transport/HTTP2/Client.hs`](../../../../../../src/Simplex/Messaging/Transport/HTTP2/Client.hs) + +## sendRequest vs sendRequestDirect — thread safety + +`sendRequest` is thread-safe: it puts the request on a `TBQueue` and waits for the response via a `TMVar`. A single background thread (`process`) dequeues and sends requests sequentially through the HTTP/2 session. + +`sendRequestDirect` bypasses the queue and calls `sendReq` directly. The source comment warns: "this function should not be used until HTTP2 is thread safe, use sendRequest." + +## attachHTTP2Client — runs on both client and server TLS + +The source comment states: "HTTP2 client can be run on both client and server TLS connections." `attachHTTP2Client` takes a `TLS p` where `p` can be `TClient` or `TServer`, allowing an HTTP/2 client session to run on an existing server-side TLS connection. + +## Connection timeout and async lifecycle + +`getVerifiedHTTP2ClientWith` starts the HTTP/2 session in an `async` and waits up to `connTimeout` for the session to establish (signal via `TMVar`). If the timeout fires, the async is cancelled. If the session establishes successfully, the `action` field holds the async handle — `closeHTTP2Client` cancels it with `uninterruptibleCancel`. diff --git a/spec/modules/Simplex/Messaging/Transport/HTTP2/File.md b/spec/modules/Simplex/Messaging/Transport/HTTP2/File.md new file mode 100644 index 000000000..8d03d8239 --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/HTTP2/File.md @@ -0,0 +1,7 @@ +# Simplex.Messaging.Transport.HTTP2.File + +> File transfer over HTTP/2: chunked send/receive with size tracking. + +**Source**: [`Transport/HTTP2/File.hs`](../../../../../../src/Simplex/Messaging/Transport/HTTP2/File.hs) + +No non-obvious behavior. See source. diff --git a/spec/modules/Simplex/Messaging/Transport/HTTP2/Server.md b/spec/modules/Simplex/Messaging/Transport/HTTP2/Server.md new file mode 100644 index 000000000..931f89bca --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/HTTP2/Server.md @@ -0,0 +1,15 @@ +# Simplex.Messaging.Transport.HTTP2.Server + +> HTTP/2 server with inactive client expiration. The single-queue server is for testing only. + +**Source**: [`Transport/HTTP2/Server.hs`](../../../../../../src/Simplex/Messaging/Transport/HTTP2/Server.hs) + +## Inactive client expiration + +`runHTTP2ServerWith_` tracks last activity per client via a `TVar SystemTime`. A background thread (`expireInactiveClient`) periodically checks whether the client has been inactive beyond the `ExpirationConfig` threshold. If so, it calls `closeConnection tls`. + +The activity timestamp is updated on every HTTP/2 request (before dispatching to the handler). + +## getHTTP2Server — testing only + +The source comment states: "This server is for testing only, it processes all requests in a single queue." `getHTTP2Server` puts all requests on a single `TBQueue`. `runHTTP2Server` dispatches requests directly via `H.run` without queueing. diff --git a/spec/modules/Simplex/Messaging/Transport/KeepAlive.md b/spec/modules/Simplex/Messaging/Transport/KeepAlive.md new file mode 100644 index 000000000..1f943bafa --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/KeepAlive.md @@ -0,0 +1,11 @@ +# Simplex.Messaging.Transport.KeepAlive + +> Platform-specific TCP keepalive configuration via CApiFFI. + +**Source**: [`Transport/KeepAlive.hs`](../../../../../src/Simplex/Messaging/Transport/KeepAlive.hs) + +## Platform-specific TCP_KEEPIDLE + +macOS uses `TCP_KEEPALIVE` instead of `TCP_KEEPIDLE`. The CPP conditional imports the correct constant at compile time via `foreign import capi`. Windows uses hardcoded numeric values — the source comment states: "The values are copied from windows::Win32::Networking::WinSock." + +Defaults: idle=30s, interval=15s, count=4. diff --git a/spec/modules/Simplex/Messaging/Transport/Server.md b/spec/modules/Simplex/Messaging/Transport/Server.md new file mode 100644 index 000000000..181951dcd --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/Server.md @@ -0,0 +1,33 @@ +# Simplex.Messaging.Transport.Server + +> TLS server: socket lifecycle, client acceptance, SNI credential switching, socket leak detection. + +**Source**: [`Transport/Server.hs`](../../../../../src/Simplex/Messaging/Transport/Server.hs) + +## safeAccept — errno-based retry + +`safeAccept` retries `accept()` on specific errno values. The code comment references the POSIX man page: "man accept says: For reliable operation the application should detect the network errors defined for the protocol after accept() and treat them like EAGAIN by retrying." The retry set: `eCONNABORTED, eAGAIN, eNETDOWN, ePROTO, eNOPROTOOPT, eHOSTDOWN, eNONET, eHOSTUNREACH, eOPNOTSUPP, eNETUNREACH`. Any other error is logged and re-thrown. + +## SocketState — leak detection + +`SocketState = (TVar Int, TVar Int, TVar (IntMap (Weak ThreadId)))` tracks: accepted count, gracefully-closed count, and active client threads. `getSocketStats` computes `socketsLeaked = socketsAccepted - socketsClosed - socketsActive`. + +## closeServer — weak thread references + +`closeServer` kills active client threads via `Weak ThreadId`. The code: `readTVarIO clients >>= mapM_ (deRefWeak >=> mapM_ killThread)`. `deRefWeak` returns `Nothing` if the thread has already been garbage collected, so the shutdown does not fail on already-dead threads. + +## SNI credential switching + +`supportedTLSServerParams` selects TLS credentials based on SNI: +- **No SNI**: uses `credential` (the primary server credential) +- **SNI present**: uses `sniCredential` (when configured) + +The `sniCredUsed` TVar records whether SNI triggered credential switching. In the SMP server (Server.hs), when `sniUsed` is `True`, the connection is dispatched to the HTTP handler instead of the SMP handler. + +## startTCPServer — address resolution + +`startTCPServer` resolves the listen address and selects `AF_INET6` first, falling back to `AF_INET`: `select as = fromJust $ family AF_INET6 <|> family AF_INET`. + +## Client certificate validation for services + +`paramsAskClientCert` enables TLS client certificate requests. In `validateClientCertificate`, an empty chain (`CCEmpty`) returns no error — client certificates are optional, as noted by the code comment: "client certificates are only used for services." diff --git a/spec/modules/Simplex/Messaging/Transport/Shared.md b/spec/modules/Simplex/Messaging/Transport/Shared.md new file mode 100644 index 000000000..8248c068b --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/Shared.md @@ -0,0 +1,28 @@ +# Simplex.Messaging.Transport.Shared + +> Certificate chain parsing and X.509 validation utilities shared between client and server. + +**Source**: [`Transport/Shared.hs`](../../../../../src/Simplex/Messaging/Transport/Shared.hs) + +**Protocol spec**: [`protocol/simplex-messaging.md` — Router certificate](../../../../protocol/simplex-messaging.md#router-certificate) — certificate chain lengths and semantics. + +## chainIdCaCerts — certificate chain semantics + +`chainIdCaCerts` classifies TLS certificate chains (which are ordered leaf-first) by length: + +| Length | Constructor | Code comment | +|--------|------------|--------------| +| 0 | `CCEmpty` | (no chain) | +| 1 | `CCSelf cert` | (self-signed) | +| 2 | `CCValid {leafCert, idCert=cert, caCert=cert}` | "current long-term online/offline certificates chain" | +| 3 | `CCValid {leafCert, idCert, caCert}` | "with additional operator certificate (preset in the client)" | +| 4 | `CCValid {leafCert, idCert, _, caCert}` | "with network certificate" | +| 5+ | `CCLong` | (rejected) | + +The protocol spec defines supported chain lengths of 2, 3, and 4 certificates (see [Router certificate](../../../../protocol/simplex-messaging.md#router-certificate)). In all `CCValid` cases, `idCert` is the certificate whose fingerprint is compared against the server address key hash, and `caCert` is used as the X.509 trust anchor. + +In the 4-cert case, index 2 is skipped (`_`) — it is present in the chain but not used as either the identity or the trust anchor. + +## x509validate — FQHN check disabled + +`x509validate` sets `checkFQHN = False`. The protocol spec identifies servers by certificate fingerprint (key hash in the server address), not by domain name. The validation uses a fresh `ValidationCache` (`ValidationCacheUnknown` for all lookups, no-op store) — each connection validates independently. diff --git a/spec/modules/Simplex/Messaging/Transport/WebSockets.md b/spec/modules/Simplex/Messaging/Transport/WebSockets.md new file mode 100644 index 000000000..7f1b3e673 --- /dev/null +++ b/spec/modules/Simplex/Messaging/Transport/WebSockets.md @@ -0,0 +1,15 @@ +# Simplex.Messaging.Transport.WebSockets + +> WebSocket transport implementation over TLS, with strict message framing. + +**Source**: [`Transport/WebSockets.hs`](../../../../../src/Simplex/Messaging/Transport/WebSockets.hs) + +## cGet — strict size check (unlike TLS) + +`cGet` throws `TEBadBlock` if the received WebSocket message length doesn't equal `n`. This differs from the TLS `cGet` which uses `getBuffered` to accumulate partial reads. + +## WebSocket options + +- `connectionCompressionOptions = NoCompression` +- `connectionFramePayloadSizeLimit = SizeLimit $ fromIntegral smpBlockSize` (16384) +- `connectionMessageDataSizeLimit = SizeLimit 65536` diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index d55b25148..11b504a4c 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -103,6 +103,7 @@ currentXFTPVersion = VersionXFTP 3 supportedFileServerVRange :: VersionRangeXFTP supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion +-- spec: spec/modules/Simplex/FileTransfer/Transport.md#xftpclienthandshakestub--xftp-doesnt-use-tls-handshake -- XFTP protocol does not use this handshake method xftpClientHandshakeStub :: c 'TClient -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> Bool -> Maybe (ServiceCredentials, C.KeyPairEd25519) -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient) xftpClientHandshakeStub _c _ks _keyHash _xftpVRange _proxyServer _serviceKeys = throwE TEVersion @@ -190,6 +191,7 @@ receiveEncFile getBody = receiveFile_ . receive data ReceiveFileError = RFESize | RFECrypto +-- spec: spec/modules/Simplex/FileTransfer/Transport.md#receivesbfile--constant-time-auth-tag-verification receiveSbFile :: (Int -> IO ByteString) -> Handle -> LC.SbState -> Word32 -> IO (Either ReceiveFileError ()) receiveSbFile getBody h = receive where diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index f1eb1a8bd..fde483177 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -240,6 +240,7 @@ currentServerSMPRelayVersion = VersionSMP 19 -- Max SMP protocol version to be used in e2e encrypted -- connection between client and server, as defined by SMP proxy. +-- spec: spec/modules/Simplex/Messaging/Transport.md#proxiedsmprelayversion--anti-fingerprinting-cap -- SMP proxy sets it to lower than its current version -- to prevent client version fingerprinting by the -- destination relays when clients upgrade at different times. @@ -376,6 +377,7 @@ getTLS cfg tlsCertSent tlsPeerCert cxt = withTlsUnique @TLS @p cxt newTLS tlsALPN <- T.getNegotiatedProtocol cxt pure TLS {tlsContext = cxt, tlsALPN, tlsTransportConfig = cfg, tlsCertSent, tlsPeerCert, tlsUniq, tlsBuffer} +-- spec: spec/modules/Simplex/Messaging/Transport.md#withtlsunique--different-api-calls-yield-same-value withTlsUnique :: forall c p. TransportPeerI p => T.Context -> (ByteString -> IO (c p)) -> IO (c p) withTlsUnique cxt f = cxtFinished cxt @@ -722,6 +724,7 @@ instance Encoding TransportError where TENoServerAuth -> "NO_AUTH" TEHandshake e -> "HANDSHAKE " <> bshow e +-- spec: spec/modules/Simplex/Messaging/Transport.md#tputblock--tgetblock--optional-block-encryption -- | Pad and send block to SMP transport. tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) tPutBlock THandle {connection = c, params = THandleParams {blockSize, encryptBlock}} block = do @@ -797,6 +800,7 @@ smpClientHandshake :: forall c. Transport c => c 'TClient -> Maybe C.KeyPairX255 smpClientHandshake c ks_ keyHash@(C.KeyHash kh) vRange proxyServer serviceKeys_ = do SMPServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th when (sessionId /= sessId) $ throwE TEBadSession + -- spec: spec/modules/Simplex/Messaging/Transport.md#proxy-version-downgrade-logic -- Below logic downgrades version range in case the "client" is SMP proxy server and it is -- connected to the destination server of the version 11 or older. -- It disables transport encryption between SMP proxy and destination relay. @@ -857,6 +861,7 @@ smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRang smpTHandleClient th v vr pk_ ck_ proxyServer clientService = do let thAuth = clientTHParams <$!> ck_ be <- blockEncryption th v proxyServer thAuth + -- spec: spec/modules/Simplex/Messaging/Transport.md#smpthandleclient--chain-key-swap -- swap is needed to use client's sndKey as server's rcvKey and vice versa pure $ smpTHandle_ th v vr thAuth $ uncurry TSbChainKeys . swap <$> be where @@ -893,6 +898,7 @@ smpTHandle_ th@THandle {params} v vr thAuth encryptBlock = } in (th :: THandleSMP c p) {params = params'} +-- spec: spec/modules/Simplex/Messaging/Transport.md#forcecertchain--space-leak-prevention forceCertChain :: CertChainPubKey -> CertChainPubKey forceCertChain cert@(CertChainPubKey (X.CertificateChain cc) signedKey) = length (show cc) `seq` show signedKey `seq` cert {-# INLINE forceCertChain #-} diff --git a/src/Simplex/Messaging/Transport/Credentials.hs b/src/Simplex/Messaging/Transport/Credentials.hs index 8e3efe795..26bcadf7a 100644 --- a/src/Simplex/Messaging/Transport/Credentials.hs +++ b/src/Simplex/Messaging/Transport/Credentials.hs @@ -40,6 +40,7 @@ privateToTls (C.APrivateSignKey _ k) = case k of type Credentials = (C.ASignatureKeyPair, X509.SignedCertificate) +-- spec: spec/modules/Simplex/Messaging/Transport/Credentials.md#gencredentials--nanosecond-stripping genCredentials :: TVar ChaChaDRG -> Maybe Credentials -> (Hours, Hours) -> Text -> IO Credentials genCredentials g parent (before, after) subjectName = do subjectKeys <- atomically $ C.generateSignatureKeyPair C.SEd25519 g diff --git a/src/Simplex/Messaging/Transport/HTTP2/Client.hs b/src/Simplex/Messaging/Transport/HTTP2/Client.hs index ca0714225..d3402130b 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/Client.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/Client.hs @@ -193,6 +193,7 @@ sendRequest HTTP2Client {client_ = HClient {config, reqQ}} req reqTimeout_ = do let reqTimeout = http2RequestTimeout config reqTimeout_ maybe (Left HCResponseTimeout) Right <$> (reqTimeout `timeout` atomically (takeTMVar resp)) +-- spec: spec/modules/Simplex/Messaging/Transport/HTTP2/Client.md#sendrequest-vs-sendrequestdirect--thread-safety -- | this function should not be used until HTTP2 is thread safe, use sendRequest sendRequestDirect :: HTTP2Client -> Request -> Maybe Int -> IO (Either HTTP2ClientError HTTP2Response) sendRequestDirect HTTP2Client {client_ = HClient {config, disconnected}, sendReq} req reqTimeout_ = do diff --git a/src/Simplex/Messaging/Transport/Server.hs b/src/Simplex/Messaging/Transport/Server.hs index cdfc300b7..63e471855 100644 --- a/src/Simplex/Messaging/Transport/Server.hs +++ b/src/Simplex/Messaging/Transport/Server.hs @@ -183,6 +183,7 @@ runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket serve tId <- mkWeakThreadId =<< server conn `forkFinally` closeConn atomically $ unlessM (readTVar closed) $ modifyTVar' clients $ IM.insert cId tId +-- spec: spec/modules/Simplex/Messaging/Transport/Server.md#safeaccept--errno-based-retry -- | Recover from errors in `accept` whenever it is safe. -- Some errors are safe to ignore, while blindly restaring `accept` may trigger a busy loop. -- @@ -224,6 +225,7 @@ getSocketStats (accepted, closed, active) = do let socketsLeaked = socketsAccepted - socketsClosed - socketsActive pure SocketStats {socketsAccepted, socketsClosed, socketsActive, socketsLeaked} +-- spec: spec/modules/Simplex/Messaging/Transport/Server.md#closeserver--weak-thread-references closeServer :: TMVar Bool -> TVar (IntMap (Weak ThreadId)) -> Socket -> IO () closeServer started clients sock = do close sock diff --git a/src/Simplex/Messaging/Transport/Shared.hs b/src/Simplex/Messaging/Transport/Shared.hs index 204ef3f5e..e7f450f49 100644 --- a/src/Simplex/Messaging/Transport/Shared.hs +++ b/src/Simplex/Messaging/Transport/Shared.hs @@ -24,6 +24,7 @@ data ChainCertificates | CCValid {leafCert :: X.SignedCertificate, idCert :: X.SignedCertificate, caCert :: X.SignedCertificate} | CCLong +-- spec: spec/modules/Simplex/Messaging/Transport/Shared.md#chainidcacerts--certificate-chain-semantics chainIdCaCerts :: X.CertificateChain -> ChainCertificates chainIdCaCerts (X.CertificateChain chain) = case chain of [] -> CCEmpty diff --git a/src/Simplex/Messaging/Transport/WebSockets.hs b/src/Simplex/Messaging/Transport/WebSockets.hs index 3ab213dcd..38ac6627d 100644 --- a/src/Simplex/Messaging/Transport/WebSockets.hs +++ b/src/Simplex/Messaging/Transport/WebSockets.hs @@ -69,6 +69,7 @@ instance Transport WS where closeConnection = S.close . wsStream {-# INLINE closeConnection #-} + -- spec: spec/modules/Simplex/Messaging/Transport/WebSockets.md#cget--strict-size-check-unlike-tls cGet :: WS p -> Int -> IO ByteString cGet c n = do s <- receiveData (wsConnection c)