Skip to content

Commit ea808cc

Browse files
protocol and client specs
1 parent 35d4065 commit ea808cc

File tree

7 files changed

+322
-0
lines changed

7 files changed

+322
-0
lines changed

spec/TOPICS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,10 @@
1212

1313
- **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.
1414

15+
- **SMP proxy protocol flow**: The PRXY/PFWD/RFWD proxy protocol involves Client.hs (proxySMPCommand with 10 error scenarios, forwardSMPTransmission with sessionSecret encryption), Protocol.hs (command types, version-dependent encoding), Transport.hs (proxiedSMPRelayVersion cap, proxyServer flag disabling block encryption). The double encryption (client-relay via PFWD + proxy-relay via RFWD), combined timeout (tcpConnect + tcpTimeout), nonce/reverseNonce pairing, and version downgrade logic are not visible from any single module.
16+
17+
- **Service certificate subscription model**: Service subscriptions (SUBS/NSUBS) and per-queue subscriptions (SUB/NSUB) coexist with complex state transitions. Client/Agent.hs manages dual active/pending subscription maps with session-aware cleanup. Protocol.hs defines useServiceAuth (only NEW/SUB/NSUB). Client.hs implements authTransmission with dual signing (entity key over cert hash + transmission, service key over transmission only). Transport.hs handles the service certificate handshake extension (v16+). The full subscription lifecycle — from DBService credentials through handshake to service subscription to disconnect/reconnect — spans all four modules.
18+
19+
- **Two agent layers**: Client/Agent.hs ("small agent") is used only in servers — SMP proxy and notification server — to manage client connections to other SMP servers. Agent.hs + Agent/Client.hs ("big agent") is used in client applications. Both manage SMP client connections with subscription tracking and reconnection, but the big agent adds the full messaging agent layer (connections, double ratchet, file transfer). When documenting Agent/Client.hs, Client/Agent.hs should be reviewed for shared patterns and differences.
20+
1521
- **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.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Simplex.Messaging.Client
2+
3+
> Generic protocol client: connection management, command sending/receiving, batching, proxy protocol, reconnection.
4+
5+
**Source**: [`Client.hs`](../../../../src/Simplex/Messaging/Client.hs)
6+
7+
**Protocol spec**: [`protocol/simplex-messaging.md`](../../../../protocol/simplex-messaging.md) — SimpleX Messaging Protocol.
8+
9+
## Overview
10+
11+
This module implements the client side of the `Protocol` typeclass — connecting to servers, sending commands, receiving responses, and managing connection lifecycle. It is generic over `Protocol v err msg`, instantiated for SMP as `SMPClient` (= `ProtocolClient SMPVersion ErrorType BrokerMsg`). The SMP proxy protocol (PRXY/PFWD/RFWD) is also implemented here.
12+
13+
## Four concurrent threads per connection
14+
15+
`getProtocolClient` launches four threads via `raceAny_`:
16+
- `send`: reads from `sndQ` (TBQueue) and writes to TLS
17+
- `receive`: reads from TLS and writes to `rcvQ` (TBQueue), updates `lastReceived` timestamp
18+
- `process`: reads from `rcvQ` and dispatches to response variables or `msgQ`
19+
- `monitor`: periodic ping loop (only when `smpPingInterval > 0`)
20+
21+
When any thread terminates (via `raceAny_`), the `disconnected` callback fires.
22+
23+
## clientCorrId — random correlation IDs
24+
25+
`clientCorrId` is a `TVar ChaChaDRG` used to generate random `CbNonce` values that serve as correlation IDs. The `CbNonce` is also used as the nonce for proxy encryption. When a nonce is explicitly passed (e.g., by `createSMPQueue`), it is used instead of generating a random one.
26+
27+
## nonBlockingWriteTBQueue — fork on full queue
28+
29+
If `tryWriteTBQueue` returns `False` (queue full), a new thread is forked to perform the blocking write. This prevents the caller from blocking when the send queue is full.
30+
31+
## getResponse — double-check after timeout
32+
33+
Regardless of whether a response arrived or the timeout fired, `getResponse` sets `pending` to `False` and then tries to read the response variable again. The source comment states: "Try to read response again in case it arrived after timeout expired but before `pending` was set to False above. See `processMsg`." This handles the race between the timeout and a response arriving.
34+
35+
`timeoutErrorCount` is incremented on each timeout and reset to 0 on each received response (in `getResponse`). The `receive` thread also resets it to 0 on every TLS read. The monitor uses this count to decide when to drop the connection.
36+
37+
## processMsg — empty corrId means server event
38+
39+
When `corrId` is empty (`B.null $ bs corrId`), the response is treated as a server-initiated event (`STEvent`). When non-empty, it is looked up in `sentCommands`. If the command was already expired (`wasPending` is `False`), the response is forwarded to `msgQ` as `STResponse` rather than being put into the `responseVar`.
40+
41+
Entity ID mismatch (response entity ID differs from request entity ID) is treated as `unexpectedResponse`.
42+
43+
## monitor — adaptive ping with connection drop
44+
45+
The ping loop sleeps for `smpPingInterval`, then checks how long since `lastReceived`. If significant time remains in the interval, it re-sleeps for just that remaining time (avoiding early pings). Pings are only sent when `sendPings` is `True` — this is set by `enablePings`, which is called by `subscribeSMPQueue`, `subscribeSMPQueues`, `subscribeSMPQueueNotifications`, `subscribeSMPQueuesNtfs`, and `subscribeService`, not on connection establishment.
46+
47+
The source code drops the client when `maxCnt` commands have timed out in sequence **and** at least `recoverWindow` (15 minutes) has passed since the last received response.
48+
49+
## Batch commands do not expire
50+
51+
The source comment states: "Currently there is coupling - batch commands do not expire, and individually sent commands do. This is to reflect the fact that we send subscriptions only as batches, and also because we do not track a separate timeout for the whole batch, so it is not obvious when should we expire it."
52+
53+
When using `sendBatch`, requests are written to `sndQ` with `Nothing` as the request parameter (vs `Just r` for individual sends), which means the send thread won't check the `pending` flag.
54+
55+
## chooseTransportHost
56+
57+
Selects onion or public host based on `hostMode` and `socksProxy` configuration:
58+
- `HMOnionViaSocks`: use onion only if SOCKS proxy is configured
59+
- `HMOnion`: always prefer onion
60+
- `HMPublic`: always prefer public
61+
62+
When `requiredHostMode` is `True`, the function returns `Left PCEIncompatibleHost` if no matching host exists. When `False`, it falls back to the first host in the list.
63+
64+
## SocksIsolateByAuth — SOCKS credential generation
65+
66+
When `SocksIsolateByAuth` is the SOCKS auth mode, `clientSocksCredentials` generates SOCKS credentials as `SocksCredentials sessionUsername ""` where `sessionUsername` is `B64.encode $ C.sha256Hash $ bshow userId <> ...`. The suffix varies by `sessionMode`:
67+
- `TSMUser`: `""`
68+
- `TSMSession`: `":" <> bshow proxySessTs`
69+
- `TSMServer`: `":" <> bshow proxySessTs <> "@" <> strEncode srv`
70+
- `TSMEntity`: `":" <> bshow proxySessTs <> "@" <> strEncode srv <> "/" <> entityId`
71+
72+
## useWebPort — preset domain suffix matching
73+
74+
`useWebPort` decides whether to use port 443 (HTTP) transport. `SWPPreset` mode matches when the server's first host is a domain that has any of `presetDomains` as a suffix (via `isSuffixOf`).
75+
76+
## connectSMPProxiedRelay — combined timeout
77+
78+
The timeout for the `PRXY` command is `netTimeoutInt tcpConnectTimeout nm + netTimeoutInt tcpTimeout nm` — both timeouts are transformed by `netTimeoutInt` (which selects background/interactive and applies exponential scaling) before being summed.
79+
80+
## ProxiedRelay — stored auth for reconnection
81+
82+
The source comment on `prBasicAuth` states: "auth is included here to allow reconnecting via the same proxy after NO_SESSION error."
83+
84+
## proxySMPCommand — 9 error scenarios
85+
86+
The source comment states: "there may be one successful scenario and 9 error scenarios" and documents all 10 (scenarios 0-9), mapping each combination of success/error at the client-proxy and proxy-relay boundaries to specific error types. Errors from the destination relay wrapped in `PRES` are thrown as `ExceptT` errors (transparent proxy). Errors from the proxy itself are returned as `Left ProxyClientError`.
87+
88+
## forwardSMPTransmission — proxy-side forwarding
89+
90+
Used by the proxy server to forward `RFWD` to the destination relay. Uses `cbEncryptNoPad`/`cbDecryptNoPad` (no padding) with the session secret from the proxy-relay connection. Response nonce is `reverseNonce` of the request nonce.
91+
92+
## action — weak thread reference
93+
94+
`action` stores a `Weak ThreadId` (via `mkWeakThreadId`) to the main client thread. `closeProtocolClient` dereferences and kills it.
95+
96+
## netTimeoutInt — exponential backoff for interactive retries
97+
98+
`netTimeoutInt` applies `(3/2)^n` scaling to `interactiveTimeout` for interactive request modes:
99+
- n=0: `interactiveTimeout`
100+
- n=1: `interactiveTimeout * 3/2`
101+
- n=2: `interactiveTimeout * 9/4`
102+
- n=3+: `interactiveTimeout * 27/8`
103+
104+
Background mode always uses `backgroundTimeout` regardless of retry count.
105+
106+
## authTransmission — dual auth with service signature
107+
108+
When a command uses service auth (`useServiceAuth` returns `True`) and a service certificate is present, the entity key signs over the concatenation of `serviceCertHash <> transmission` (not just the transmission). The source comment states: "entity key must sign over both transmission and service certificate hash, to prevent any service substitution via MITM inside TLS." The service key only signs the transmission itself.
109+
110+
For X25519 keys, `cbAuthenticate` produces a `TAAuthenticator`. For Ed25519/Ed448 keys, `C.sign'` produces a `TASignature`.
111+
112+
## subscribeSMPQueue / getSMPMessage — request mode comments
113+
114+
Several commands have explicit request mode comments:
115+
- `subscribeSMPQueue`: "This command is always sent in background request mode"
116+
- `getSMPMessage`: "This command is always sent in interactive request mode, as NSE has limited time"
117+
- `ackSMPMessage`: "This command is always sent in background request mode"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Simplex.Messaging.Client.Agent
2+
3+
> SMP client connections with subscription management, reconnection, and service certificate support.
4+
5+
**Source**: [`Client/Agent.hs`](../../../../../src/Simplex/Messaging/Client/Agent.hs)
6+
7+
## Overview
8+
9+
`SMPClientAgent` manages `SMPClient` connections via `smpClients :: TMap SMPServer SMPClientVar` (one per SMP server), tracks active and pending subscriptions, and handles automatic reconnection. It is parameterized by `Party` (`p`) and uses the `ServiceParty` constraint to support both `RecipientService` and `NotifierService` modes.
10+
11+
## Dual subscription model
12+
13+
Four TMap fields track subscriptions in two dimensions:
14+
15+
| | Active | Pending |
16+
|---|---|---|
17+
| **Service** | `activeServiceSubs :: TMap SMPServer (TVar (Maybe (ServiceSub, SessionId)))` | `pendingServiceSubs :: TMap SMPServer (TVar (Maybe ServiceSub))` |
18+
| **Queue** | `activeQueueSubs :: TMap SMPServer (TMap QueueId (SessionId, C.APrivateAuthKey))` | `pendingQueueSubs :: TMap SMPServer (TMap QueueId C.APrivateAuthKey)` |
19+
20+
The source comment states: "Only one service subscription can exist per server with this agent. With correctly functioning SMP server, queue and service subscriptions can't be active at the same time." And: "Pending service subscriptions can co-exist with pending queue subscriptions on the same SMP server during subscriptions being transitioned from per-queue to service."
21+
22+
Active subscriptions store the `SessionId` of the connection that established them. On disconnect, only subscriptions matching the disconnected session's `SessionId` are moved to pending.
23+
24+
## persistErrorInterval — delayed error cleanup
25+
26+
When `newSMPClient` (which calls `connectClient`) fails, the error is stored with an expiry timestamp (`addUTCTime ei`) rather than being removed immediately. `waitForSMPClient` checks if the timestamp has expired before retrying. When `persistErrorInterval` is 0, the error is removed from the map immediately.
27+
28+
## removeClientAndSubs — subscription vars looked up outside STM
29+
30+
The source comment states: "Looking up subscription vars outside of STM transaction to reduce re-evaluation. It is possible because these vars are never removed, they are only added."
31+
32+
Within the STM transaction, only subscriptions matching the disconnected session's `SessionId` are moved to pending. The source comment on `updateServiceSub` states: "We don't change active subscription in case session ID is different from disconnected client." And: "We don't reset pending subscription to Nothing here to avoid any race conditions with subsequent client sessions that might have set pending already."
33+
34+
## updateActiveServiceSub
35+
36+
When the service ID and session ID match the existing active subscription, `updateActiveServiceSub` adds the queue count (`n + n'`) and XOR-merges the IdsHash (`idsHash <> idsHash'`) rather than replacing. When they don't match, the subscription is replaced entirely.
37+
38+
## CAServiceUnavailable event
39+
40+
The source comment states: "CAServiceUnavailable is used when service ID in pending subscription is different from the current service in connection. This will require resubscribing to all queues associated with this service ID individually, creating new associations. It may happen if, for example, SMP server deletes service information (e.g. via downgrade and upgrade) and assigns different service ID to the service certificate."
41+
42+
## serviceAvailable check
43+
44+
`smpSubscribeService` checks both that `serviceId` matches and that `partyServiceRole` matches the connection's `serviceRole`. If either doesn't match, `CAServiceUnavailable` is notified.
45+
46+
## groupSub — subscription response classification
47+
48+
When processing subscription responses, each queue is classified:
49+
- If the response includes a `serviceId` matching the connection's service ID: counted as a service-subscribed queue (added to `sQs`)
50+
- Otherwise: counted as a queue-only subscription (added to `qOks` with `SessionId` and key)
51+
- Queues not found in `pending` map are skipped (accumulator unchanged)
52+
53+
## Reconnect worker cleanup race
54+
55+
The source comment on `cleanup` states: "Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar. Not waiting may result in terminated worker remaining in the map."
56+
57+
## DBService abstraction
58+
59+
`DBService` provides `getCredentials :: SMPServer -> IO (Either SMPClientError ServiceCredentials)` and `updateServiceId :: SMPServer -> Maybe ServiceId -> IO (Either SMPClientError ())`. When `dbService` is `Nothing`, connections are made without service credentials.
60+
61+
## isOwnServer — domain suffix matching
62+
63+
`isOwnServer` checks if the server's first host exactly matches any `ownServerDomains` entry, or if any entry is a suffix preceded by `.`. This is used to set the `OwnServer` flag on client connections.
64+
65+
## smpSessions — proxy session lookup
66+
67+
`smpSessions` maps `SessionId` (from `thParams`) to `(OwnServer, SMPClient)`. `lookupSMPServerClient` performs a lookup into this map.

0 commit comments

Comments
 (0)