|
| 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" |
0 commit comments