From 875ddd80d6d58f9308b6d18e745c47a983c1ae3c Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Fri, 12 Apr 2024 20:04:29 +0300 Subject: [PATCH 01/13] SMP proxy: protocol (#954) * WIP: proxy-related types * test plan * buildable with stubs * add auth test * update protocol * fix * update rfc * update protocol/types * disable test --------- Co-authored-by: Evgeny Poberezkin --- notes-flow.txt | 23 +++++++ rfcs/2023-09-12-second-relays.md | 87 +++++++++++++------------ simplexmq.cabal | 1 + src/Simplex/Messaging/Crypto.hs | 2 + src/Simplex/Messaging/Protocol.hs | 80 ++++++++++++++++++++++- src/Simplex/Messaging/Server.hs | 23 +++++++ src/Simplex/Messaging/Server/Env/STM.hs | 45 ++++++++++--- src/Simplex/Messaging/Server/Main.hs | 3 +- src/Simplex/Messaging/Transport.hs | 4 ++ tests/SMPClient.hs | 12 +++- tests/SMPProxyTests.hs | 67 +++++++++++++++++++ tests/ServerTests.hs | 2 - tests/Test.hs | 2 + 13 files changed, 295 insertions(+), 56 deletions(-) create mode 100644 notes-flow.txt create mode 100644 tests/SMPProxyTests.hs diff --git a/notes-flow.txt b/notes-flow.txt new file mode 100644 index 000000000..93f984509 --- /dev/null +++ b/notes-flow.txt @@ -0,0 +1,23 @@ +common: + corrId - random BS, used as CbNonce + entityId - p2r tlsUniq + +# setup +s->p: "proxy", uri, auth? + # unless connected + p->r: "p_handshake" + p<-r: "r_key", tls-signed dh pub +s<-r: "r_key", tls-signed dh pub # reply entityId contains tlsUniq + +# working +s ; generate random dh priv, make shared secret +s->p: s2r("forward", random dh pub, SEND command blob) + p->r: p2r("forward", random dh pub, s2r("forward", ...))) + r->c@ "msg", ... + p<-r: p2r("r_res", s2r("ok" / "error", error)) +s<-p@ s2r("ok" / "error", error) + +# expired + p<-r@ p2r("error", "key expired") +s<-p@ "error", "key expired" +s ; reconnect \ No newline at end of file diff --git a/rfcs/2023-09-12-second-relays.md b/rfcs/2023-09-12-second-relays.md index 3721ee721..a47eb7bde 100644 --- a/rfcs/2023-09-12-second-relays.md +++ b/rfcs/2023-09-12-second-relays.md @@ -2,9 +2,9 @@ ## Problem -SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an audequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency. +SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an adequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency. -The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realise that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product". +The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realize that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product". Similarly, XFTP protocol relays are chosen by senders, and they can be used to detect file recipients' IP addresses. @@ -43,7 +43,7 @@ Overall, this is not a viable or even appropriate option for the current stage. 3. SMP / XFTP proxy. -Introduce SMP and XFTP protocol extenstions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users. +Introduce SMP and XFTP protocol extensions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users. Pros: - no dependency on and lower latency than via Tor @@ -68,7 +68,7 @@ Below considers this design. 2. SMP proxy should not be able to observe queue addresses and their count on the destination relays. This requirement is not needed for XFTP proxies, as each file chunk is downloaded only once, so there is no need to hide its address. -3. There must be no identifiers and cyphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality). +3. There must be no identifiers and ciphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality). 4. Traffic between the client and destination relays must be e2e encrypted, with MITM-by-proxy mitigated, relying on the relay identity (certificate fingerprint), ideally without any additional fingerprint in relay address. @@ -97,11 +97,11 @@ This would also reduce the difference in how the traffic looks to the observer - The flow of the messages will be: -1. Client requests proxy to create session with the relay by sending `server` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic. +1. Client requests proxy to create session with the relay by sending `PRXY` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic. -2. Proxy connects to SMP relay, negotiating a shared secret in the handshake that will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). SMP relay also returns in handshake its temporary DH key to agree e2e encryption with the client (sender-relay encryption, to hide metadata sent to the destination relay from proxy). +2. Proxy connects to SMP relay, negotiating a shared secret via a handshake headers - it will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). DH key returned by SMP relay in handshake will also be used to encrypt client commands, combining it with random per-command keys (sender-relay encryption, to hide metadata sent to the destination relay from proxy). -3. Proxy replies with `server_id` command including relay session ID to identify it in further requests, relay DH key for e2e encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before). +3. Proxy replies to sender with `PKEY` message using "entityId" transmission field to indicate session ID for using in further requests, relay DH key for _s2r_ encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before). A possible attack here is that proxy can use this TLS session to replay commands received from the client. Possibly, it could be mitigated with a bloom filter per proxy/SMP relay connection that would reject the repeated DH keys (that need to be used for replay), and also with DH key expiration (this mitigation should allow some acceptable rate of false positives from the bloom filter). @@ -113,11 +113,11 @@ It is important that the same public key from destination relay is returned to a *Unrelated cosideration for SMP protocol privacy improvement*: instead of signing commands to the destination relay, the sender could have a ratchet per queue agreed with the destination relay that would simply use authenticated encryption with per-message symmetric key to encrypt the message on the way to relay, and this encryption would be used as a proof of sender. -4. Now the client sends `forward` to proxy, which it then forwards to SMP relay, applying additional encryption layer. +4. Now the client sends `PFWD` to proxy, which it then forwards to SMP relay as `RFWD`, applying _p2r_ encryption layer. -5. SMP relay sends `response` to proxy applying additional encryption layer, which it then forwards to the client removing the additional encryption layer. +5. SMP relay sends `RRES` to proxy applying _p2r_ encryption layer, which it then forwards to the client as `PRES`, removing the _p2r_ encryption layer. -Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session. +Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients' relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session. Sequence diagram for sending the message via SMP proxy: @@ -126,33 +126,33 @@ Sequence diagram for sending the message via SMP proxy: | sending | | SMP | | SMP | | receiving | | client | | proxy | | relay | | client | ------------- ------------- ------------- ------------- - | `server` | | | - | -------------------------> | create TLS session, get keys | | + | `PRXY` | | | + | -------------------------> | | | | | ------------------------------> | | - | `server_id` | (if doesn't exist) | | + | | SMP handshake | | + | | <------------------------------ | | + | `PKEY` | | | | <------------------------- | | | | | | | - | TLS(F:s2r(SEND(e2e(msg)))) | | | - | -------------------------> | TLS(F:p2r(s2r(SEND(e2e(msg))))) | | + | `PFWD` (s2r) | | | + | -------------------------> | | | + | | `RFWD` (p2r) | | | | ------------------------------> | | - | | | | - | | TLS(R:p2r(s2r(OK/ERR))) | | - | TLS(R:s2r(OK/ERR)) | <------------------------------ | | - | <------------------------- | | TLS(MSG(r2c(e2e(msg)))) | - | | | -----------------------> | - | | | | - | | | TLS(ACK) | + | | `RRES` (p2r) | | + | | <------------------------------ | | + | `PRES` (s2r) | | `MSG` | + | <------------------------- | | -----------------------> | + | | | `ACK` | | | | <----------------------- | | | | | | | | | - ``` -Below diagram shows the encrypttion layers for `forward` and `response` commands: +Below diagram shows the encrypttion layers for `PFWD`/`RFWD` commands and `RRES`/`PRES` responses: -- s2r (added) - encryption between client and SMP relay, with relay key returned in server_id command, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address. +- s2r (added) - encryption between client and SMP relay, with relay key returned in relay handshake, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address. - e2e (exists now) - end-to-end encryption per SMP queue, with double ratchet e2e encryption inside it. -- p2r (added) - additional encryption between proxy and SMP relay with key agreed in the handshake, to mitigate traffic correlation inside TLS. This key could also be signed by the same certificate, if we don't want to rely on TLS security. +- p2r (added) - additional encryption between proxy and SMP relay with the shared secret agreed in the handshake, to mitigate traffic correlation inside TLS. - r2c (exists now) additional encryption between SMP relay and client to prevent traffic correlation inside TLS. ``` @@ -167,27 +167,32 @@ Below diagram shows the encrypttion layers for `forward` and `response` commands ----------------- ----------------- -- TLS -- ----------------- ----------------- ``` -When proxy connects to SMP relay it would indicate in the handshake that it will use proxy protocol and the SMP relay would expect the same `forward` commands and reply with `response`s. +Question: should proxy declare its role in handshake? When proxy connects to SMP relay it would indicate in the handshake that it will act as a proxy and the SMP relay would expect the same `forward` commands and reply with `response`s. -Below syntax aims to fit in 16kb block using spare capacity in SMP protocol. +Common SMP transmission format (v4), for reference: ```abnf -proxy_block = padded(proxy_transmission, 16384) -proxy_transmission = corr_id relay_session_id proxy_command -corr_id = length *8 OCTET -proxy_command = server / server_id / forward / response / error -server = "S" address [relay_basic_auth] ; creates transport session between proxy and relay -server_id = "I" relay_session_id tls_session_id signed_relay_key ; - ; session_id is the TLS session ID between proxy and relay, it has to be included inside encrypted block to prevent replay attacks -forward = %s"F" random_dh_pub_key encrypted_block ; it's important that a new key is used for each command, to prevent any correlation by proxy or by destination relay -response = %s"R" encrypted_block; response received from the destination SMP relay -relay_session_id = length *8 OCTET -error = %s"E" error +paddedTransmission = +transmission = signature signed +signature = 0 ; empty signatures here +signed = sessionIdentifier corrId entityId (smpCommand / brokerMsg) ``` -The overhead is: 1+8 (corrId) + 1+8 (relay_session_id) + 1 (command) + 1+32 (random_dh_pub_key) + 2 (original length) + 16 (auth tag for e2e encryption) + 16 (auth tag for proxy to relay encryption) = 86 bytes. The reserve for sent messages in SMP is ~84 bytes, so it should about fit with some reduced bytes somewhere. +- `corrId` is fully random each time and used as a nonce for encrypted blocks. +- `entityId` carries tlsUniq from the current proxy-to-relay connection. +- `smpCommand` gets extended with `s2p_command / p2r_command`. +- `brokerMsg` gets extended with `r_key / r_response`. -Another possible design is to allow mixing sent messages and normal SMP commands in the same transport connection, but it can make fitting in the block a bit harder, additional overhead would be: 1 (transmission count) + 2 (transmission size) + 1 (empty signature) = 4 bytes. +```abnf +s2p_command = proxy / forward +p2r_command = p_handshake ; forward is +proxy = %s"PRXY" SP relayUri SP basicAuth +relayUri = length %s"smp://" serverIdentity "@" srvHost [":" port] +forward = %s"PFWD" SP dhPublic SP encryptedBlock +r_key = %s"PKEY" SP dhPublic +r_response = %s"RRES" SP encryptedBlock +dhPublic = length x509encoded +``` The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response). diff --git a/simplexmq.cabal b/simplexmq.cabal index b85281080..95a5578ca 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -651,6 +651,7 @@ test-suite simplexmq-test ServerTests SMPAgentClient SMPClient + SMPProxyTests Util XFTPAgent XFTPCLI diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index 28183a1fc..bffd7559f 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -756,6 +756,8 @@ data Signature (a :: Algorithm) where SignatureEd25519 :: Ed25519.Signature -> Signature Ed25519 SignatureEd448 :: Ed448.Signature -> Signature Ed448 +deriving instance Eq (Signature a) + deriving instance Show (Signature a) data ASignature diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 2c593fc6f..77b14e23c 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -188,7 +188,9 @@ import Data.String import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) +import qualified Data.X509 as X import GHC.TypeLits (ErrorMessage (..), TypeError, type (+)) +import qualified GHC.TypeLits as TE import Network.Socket (ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -359,6 +361,17 @@ data Command (p :: Party) where PING :: Command Sender -- SMP notification subscriber commands NSUB :: Command Notifier + PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- request a relay server connection by URI + -- Transmission to proxy: + -- - entity ID: ID of the session with relay returned in PKEY (response to PRXY) + -- - corrId: also used as a nonce to encrypt transmission to relay, corrId + 1 - from relay + -- - key (1st param in the command) is used to agree DH secret for this particular transmission and its response + -- Encrypted transmission should include session ID (tlsunique) from proxy-relay connection. + PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender -- use CorrId as CbNonce, client to proxy + -- Transmission forwarded to relay: + -- - entity ID: empty + -- - corrId: unique correlation ID between proxy and relay, also used as a nonce to encrypt forwarded transmission + RFWD :: EncFwdTransmission -> Command Sender -- use CorrId as CbNonce, proxy to relay deriving instance Show (Command p) @@ -384,6 +397,18 @@ instance Encoding SubscriptionMode where 'C' -> pure SMOnlyCreate _ -> fail "bad SubscriptionMode" +newtype EncTransmission = EncTransmission ByteString + deriving (Show) + +data FwdTransmission = FwdTransmission + { fwdCorrId :: ByteString, + fwdKey :: C.PublicKeyX25519, + fwdTransmission :: ByteString + } + +newtype EncFwdTransmission = EncFwdTransmission ByteString + deriving (Show) + data BrokerMsg where -- SMP broker messages (responses, client messages, notifications) IDS :: QueueIdsKeys -> BrokerMsg @@ -393,6 +418,10 @@ data BrokerMsg where MSG :: RcvMessage -> BrokerMsg NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg + -- Should include certificate chain + PKEY :: X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key + RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy + PRES :: EncResponse -> BrokerMsg -- proxy to client END :: BrokerMsg OK :: BrokerMsg ERR :: ErrorType -> BrokerMsg @@ -405,6 +434,17 @@ data RcvMessage = RcvMessage } deriving (Eq, Show) +newtype EncFwdResponse = EncFwdResponse ByteString + deriving (Eq, Show) + +data FwdResponse = FwdResponse + { fwdCorrId :: ByteString, + fwdResponse :: ByteString + } + +newtype EncResponse = EncResponse ByteString + deriving (Eq, Show) + -- | received message without server/recipient encryption data Message = Message @@ -567,6 +607,9 @@ data CommandTag (p :: Party) where DEL_ :: CommandTag Recipient SEND_ :: CommandTag Sender PING_ :: CommandTag Sender + PRXY_ :: CommandTag Sender + PFWD_ :: CommandTag Sender + RFWD_ :: CommandTag Sender NSUB_ :: CommandTag Notifier data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p) @@ -580,6 +623,9 @@ data BrokerMsgTag | MSG_ | NID_ | NMSG_ + | PKEY_ + | RRES_ + | PRES_ | END_ | OK_ | ERR_ @@ -607,6 +653,9 @@ instance PartyI p => Encoding (CommandTag p) where DEL_ -> "DEL" SEND_ -> "SEND" PING_ -> "PING" + PRXY_ -> "PRXY" + PFWD_ -> "PFWD" + RFWD_ -> "RFWD" NSUB_ -> "NSUB" smpP = messageTagP @@ -623,6 +672,9 @@ instance ProtocolMsgTag CmdTag where "DEL" -> Just $ CT SRecipient DEL_ "SEND" -> Just $ CT SSender SEND_ "PING" -> Just $ CT SSender PING_ + "PRXY" -> Just $ CT SSender PRXY_ + "PFWD" -> Just $ CT SSender PFWD_ + "RFWD" -> Just $ CT SSender RFWD_ "NSUB" -> Just $ CT SNotifier NSUB_ _ -> Nothing @@ -639,6 +691,9 @@ instance Encoding BrokerMsgTag where MSG_ -> "MSG" NID_ -> "NID" NMSG_ -> "NMSG" + PKEY_ -> "PKEY" + RRES_ -> "RRES" + PRES_ -> "PRES" END_ -> "END" OK_ -> "OK" ERR_ -> "ERR" @@ -651,6 +706,9 @@ instance ProtocolMsgTag BrokerMsgTag where "MSG" -> Just MSG_ "NID" -> Just NID_ "NMSG" -> Just NMSG_ + "PKEY" -> Just PKEY_ + "RRES" -> Just RRES_ + "PRES" -> Just PRES_ "END" -> Just END_ "OK" -> Just OK_ "ERR" -> Just ERR_ @@ -829,7 +887,7 @@ type family UserProtocol (p :: ProtocolType) :: Constraint where UserProtocol PSMP = () UserProtocol PXFTP = () UserProtocol a = - (Int ~ Bool, TypeError (Text "Servers for protocol " :<>: ShowType a :<>: Text " cannot be configured by the users")) + (Int ~ Bool, TypeError (TE.Text "Servers for protocol " :<>: ShowType a :<>: TE.Text " cannot be configured by the users")) userProtocol :: SProtocolType p -> Maybe (Dict (UserProtocol p)) userProtocol = \case @@ -1046,6 +1104,8 @@ data ErrorType NO_MSG | -- | sent message is too large (> maxMessageLength = 16088 bytes) LARGE_MSG + | -- | relay public key is expired + EXPIRED | -- | internal server error INTERNAL | -- | used internally, never returned by the server (to be removed) @@ -1135,6 +1195,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg) PING -> e PING_ NSUB -> e NSUB_ + PRXY host auth_ -> e (PRXY_, ' ', strEncode host, ' ', auth_) + PFWD {} -> error "TODO: e (PFWD_,,)" + RFWD {} -> error "TODO: e (RFWD_,,)" where e :: Encoding a => a -> ByteString e = smpEncode @@ -1158,6 +1221,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where PING | isNothing auth && B.null queueId -> Right cmd | otherwise -> Left $ CMD HAS_AUTH + PRXY {} + | isNothing auth && B.null queueId -> Right cmd + | otherwise -> Left $ CMD HAS_AUTH -- other client commands must have both signature and queue ID _ | isNothing auth || B.null queueId -> Left $ CMD NO_AUTH @@ -1189,6 +1255,10 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where Cmd SSender <$> case tag of SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP) PING_ -> pure PING + PFWD_ -> error "TODO: PFWD_" + RFWD_ -> error "TODO: RFWD_" + PRXY_ -> PRXY <$> (_smpP >>= either fail pure . strDecode) <*> _smpP + CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg @@ -1204,6 +1274,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where e (MSG_, ' ', msgId, Tail body) NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh) NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta) + PKEY cert key -> e (PKEY_, ' ', C.encodeCertChain cert, C.SignedObject key) + RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock) + PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock) END -> e END_ OK -> e OK_ ERR err -> e (ERR_, ' ', err) @@ -1221,6 +1294,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP) NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP + PKEY_ -> PKEY <$> (A.space *> C.certChainP) <*> (C.getSignedExact <$> smpP) + RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP) + PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP) END_ -> pure END OK_ -> pure OK ERR_ -> ERR <$> _smpP @@ -1270,6 +1346,7 @@ instance Encoding ErrorType where CMD err -> "CMD " <> smpEncode err AUTH -> "AUTH" QUOTA -> "QUOTA" + EXPIRED -> "EXPIRED" NO_MSG -> "NO_MSG" LARGE_MSG -> "LARGE_MSG" INTERNAL -> "INTERNAL" @@ -1282,6 +1359,7 @@ instance Encoding ErrorType where "CMD" -> CMD <$> _smpP "AUTH" -> pure AUTH "QUOTA" -> pure QUOTA + "EXPIRED" -> pure EXPIRED "NO_MSG" -> pure NO_MSG "LARGE_MSG" -> pure LARGE_MSG "INTERNAL" -> pure INTERNAL diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 4535c7bd5..456de5be2 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -524,6 +524,9 @@ verifyTransmission auth_ tAuth authorized queueId cmd = Cmd SSender PING -> pure $ VRVerified Nothing -- NSUB will not be accepted without authorization Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (Just q `verifiedWith`) (notifierKey <$> notifier q)) <$> get SNotifier + Cmd SSender PRXY {} -> pure $ VRVerified Nothing + Cmd SSender PFWD {} -> pure $ VRVerified Nothing + Cmd SSender RFWD {} -> pure $ VRVerified Nothing where verify = verifyCmdAuthorization auth_ tAuth authorized dummyVerify = verify (dummyAuthKey tAuth) `seq` VRFailed @@ -597,6 +600,17 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv case command of SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody PING -> pure (corrId, "", PONG) + PRXY relay auth -> + ifM + allowProxy + (setupProxy relay) + (pure (corrId, queueId, ERR AUTH)) + where + allowProxy = do + ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config + pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth + PFWD _dhPub _encBlock -> error "TODO: processCommand.PFWD" + RFWD _encBlock -> error "TODO: processCommand.RFWD" Cmd SNotifier NSUB -> subscribeNotifications Cmd SRecipient command -> case command of @@ -922,6 +936,15 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv Right q -> updateDeletedStats q $> ok Left e -> pure $ err e + setupProxy :: SMPServer -> M (Transmission BrokerMsg) + setupProxy todo'relay = undefined + -- do + -- let relaySessionId = "TODO: relaySessionId" + -- (dummyRelayDhPublic, _) <- atomically . C.generateKeyPair =<< asks random + -- (_, dummySignKey) <- atomically . C.generateKeyPair =<< asks random + -- let dummyRelayKeySignature = C.sign' dummySignKey $ smpEncode dummyRelayDhPublic + -- pure (corrId, relaySessionId, PKEY dummyRelayDhPublic dummyRelayKeySignature) + ok :: Transmission BrokerMsg ok = (corrId, queueId, OK) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index baadfc79b..74d7d96e3 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -15,12 +15,14 @@ import qualified Data.IntMap.Strict as IM import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Data.Text (Text) import Data.Time.Clock (getCurrentTime) import Data.Time.Clock.System (SystemTime) import Data.X509.Validation (Fingerprint (..)) import Network.Socket (ServiceName) import qualified Network.TLS as T import Numeric.Natural +import Simplex.Messaging.Agent.Env.SQLite (Worker) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Crypto (KeyHash (..)) import qualified Simplex.Messaging.Crypto as C @@ -33,7 +35,7 @@ import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, VersionSMP, VersionRangeSMP) +import Simplex.Messaging.Transport (ATransport, SessionId, VersionSMP, VersionRangeSMP) import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) @@ -79,7 +81,8 @@ data ServerConfig = ServerConfig -- | TCP transport config transportConfig :: TransportServerConfig, -- | run listener on control port - controlPort :: Maybe ServiceName + controlPort :: Maybe ServiceName, + allowSMPProxy :: Bool -- auth is the same with `newQueueBasicAuth` } defMsgExpirationDays :: Int64 @@ -110,8 +113,9 @@ data Env = Env tlsServerParams :: T.ServerParams, serverStats :: ServerStats, sockets :: SocketState, - clientSeq :: TVar Int, - clients :: TVar (IntMap Client) + clientSeq :: TVar ClientId, + clients :: TVar (IntMap Client), + proxyServer :: SMPProxyServer -- senders served on this proxy } data Server = Server @@ -122,8 +126,22 @@ data Server = Server savingLock :: Lock } +data SMPProxyServer = SMPProxyServer + { relaySessions :: TMap SessionId SMPProxiedRelay, + relayServers :: TMap Text SessionId -- speed up client lookups by server URI + } + +data SMPProxiedRelay = SMPProxiedRelay + { worker :: Worker, + proxyKey :: C.DhSecretX25519, + fwdQ :: TBQueue (ClientId, CorrId, C.PublicKeyX25519, ByteString) -- FWD args from multiple clients using this server + -- can be used for QUOTA retries until the session is gone + } + +type ClientId = Int + data Client = Client - { clientId :: Int, + { clientId :: ClientId, subscriptions :: TMap RecipientId (TVar Sub), ntfSubscriptions :: TMap NotifierId (), rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)), @@ -135,7 +153,8 @@ data Client = Client connected :: TVar Bool, createdAt :: SystemTime, rcvActiveAt :: TVar SystemTime, - sndActiveAt :: TVar SystemTime + sndActiveAt :: TVar SystemTime, + proxyClient_ :: TVar (Maybe C.DhSecretX25519) -- this client is actually an SMP proxy } data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub @@ -154,7 +173,7 @@ newServer = do savingLock <- createLock return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, savingLock} -newClient :: TVar Int -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client +newClient :: TVar ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client newClient nextClientId qSize thVersion sessionId createdAt = do clientId <- stateTVar nextClientId $ \next -> (next, next + 1) subscriptions <- TM.empty @@ -166,7 +185,8 @@ newClient nextClientId qSize thVersion sessionId createdAt = do connected <- newTVar True rcvActiveAt <- newTVar createdAt sndActiveAt <- newTVar createdAt - return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} + proxyClient_ <- newTVar Nothing + return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, proxyClient_} newSubscription :: SubscriptionThread -> STM Sub newSubscription subThread = do @@ -187,7 +207,8 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, sockets <- atomically newSocketState clientSeq <- newTVarIO 0 clients <- newTVarIO mempty - return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients} + proxyServer <- newSMPProxyServer + return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyServer} where restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode) restoreQueues QueueStore {queues, senders, notifiers} f = do @@ -203,3 +224,9 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, addNotifier q = case notifier q of Nothing -> id Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q) + +newSMPProxyServer :: MonadIO m => m SMPProxyServer +newSMPProxyServer = do + relayServers <- atomically TM.empty + relaySessions <- atomically TM.empty + pure SMPProxyServer {relayServers, relaySessions} diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index a7844cc95..d14bdac1f 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -213,7 +213,8 @@ smpServerCLI cfgPath logPath = defaultTransportServerConfig { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini }, - controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini + controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini, + allowSMPProxy = True -- TODO: "get from INI" } data CliCommand diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6898af15d..3d4916b92 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -40,6 +40,7 @@ module Simplex.Messaging.Transport basicAuthSMPVersion, subModeSMPVersion, authCmdsSMPVersion, + sendingProxySMPVersion, simplexMQVersion, smpBlockSize, TransportConfig (..), @@ -148,6 +149,9 @@ subModeSMPVersion = VersionSMP 6 authCmdsSMPVersion :: VersionSMP authCmdsSMPVersion = VersionSMP 7 +sendingProxySMPVersion :: VersionSMP +sendingProxySMPVersion = VersionSMP 8 + currentClientSMPRelayVersion :: VersionSMP currentClientSMPRelayVersion = VersionSMP 6 diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 3e5e9d2ce..b63668f7a 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -73,7 +73,11 @@ testSMPClient = testSMPClientVR supportedClientSMPRelayVRange testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a testSMPClientVR vr client = do Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost - runTransportClient defaultTransportClientConfig Nothing useHost testPort (Just testKeyHash) $ \h -> do + testSMPClient_ useHost testPort vr client + +testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a +testSMPClient_ host port vr client = do + runTransportClient defaultTransportClientConfig Nothing host port (Just testKeyHash) $ \h -> do g <- C.newRandom ks <- atomically $ C.generateKeyPair g runExceptT (smpClientHandshake h ks testKeyHash vr) >>= \case @@ -107,12 +111,16 @@ cfg = certificateFile = "tests/fixtures/server.crt", smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig, - controlPort = Nothing + controlPort = Nothing, + allowSMPProxy = False } cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} +proxyCfg :: ServerConfig +proxyCfg = cfg { allowSMPProxy = True } + withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs new file mode 100644 index 000000000..0a429fc57 --- /dev/null +++ b/tests/SMPProxyTests.hs @@ -0,0 +1,67 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module SMPProxyTests where + +import SMPAgentClient (testSMPServer, testSMPServer2) +import SMPClient +import ServerTests (sendRecv) +import Simplex.Messaging.Protocol +import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) +import Simplex.Messaging.Transport +import Simplex.Messaging.Version (mkVersionRange) +import Test.Hspec +import Debug.Trace + +smpProxyTests :: Spec +smpProxyTests = do + describe "server configuration" $ do + it "refuses proxy handshake unless enabled" testNoProxy + it "checks basic auth in proxy requests" testProxyAuth + xdescribe "proxy requests" $ do + xdescribe "bad relay URIs" $ do + it "host not resolved" todo + it "when SMP port blackholed" todo + it "no SMP service at host/port" todo + it "bad SMP fingerprint" todo + it "connects to relay" testProxyConnect + xit "connects to itself as a relay" todo + xit "batching proxy requests" todo + xdescribe "forwarding requests" $ do + it "sender-proxy-relay-recipient works" todo + it "similar timing for proxied and direct sends" todo + +proxyVRange :: VersionRangeSMP +proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion + +testNoProxy :: IO () +testNoProxy = do + withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do + testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS) -> do + (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) + reply `shouldBe` Right (ERR AUTH) + +testProxyAuth :: IO () +testProxyAuth = do + withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + (_, s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") + traceShowM s + reply `shouldBe` Right (ERR AUTH) + where + proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} + +testProxyConnect :: IO () +testProxyConnect = do + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 Nothing) + case reply of + Right PKEY {} -> pure () + _ -> fail $ "bad reply: " <> show reply + +todo :: IO () +todo = do + fail "TODO" diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 09cf975c1..3b5f2f5dc 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -930,8 +930,6 @@ instance Eq C.ASignature where Just Refl -> s == s' _ -> False -deriving instance Eq (C.Signature a) - syntaxTests :: ATransport -> Spec syntaxTests (ATransport t) = do it "unknown command" $ ("", "abcd", "1234", ('H', 'E', 'L', 'L', 'O')) >#> ("", "abcd", "1234", ERR $ CMD UNKNOWN) diff --git a/tests/Test.hs b/tests/Test.hs index aebceb22d..f9fb2a2c0 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -21,6 +21,7 @@ import GHC.IO.Exception (IOException (..)) import qualified GHC.IO.Exception as IOException import NtfServerTests (ntfServerTests) import RemoteControl (remoteControlTests) +import SMPProxyTests (smpProxyTests) import ServerTests import Simplex.Messaging.Transport (TLS, Transport (..)) import Simplex.Messaging.Transport.WebSockets (WS) @@ -59,6 +60,7 @@ main = do describe "SMP server via WebSockets" $ serverTests (transport @WS) describe "Notifications server" $ ntfServerTests (transport @TLS) describe "SMP client agent" $ agentTests (transport @TLS) + describe "SMP proxy" smpProxyTests describe "XFTP" $ do describe "XFTP server" xftpServerTests describe "XFTP file description" fileDescriptionTests From ad4b5b6b71cc2225cacdcdc75b799c2a6706d037 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 15 Apr 2024 13:47:48 +0100 Subject: [PATCH 02/13] parameterize transport handle with transport peer to include server certificate (#1100) * parameterize transport handle with transport peer to include server certificate * include server certificate into THandle * load server chain and sign key * fix key type * fix for 8.10 --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Co-authored-by: IC Rainbow --- src/Simplex/FileTransfer/Client.hs | 13 ++-- src/Simplex/FileTransfer/Protocol.hs | 12 ++-- src/Simplex/FileTransfer/Server.hs | 16 ++--- src/Simplex/FileTransfer/Transport.hs | 9 +-- src/Simplex/Messaging/Client.hs | 14 ++-- src/Simplex/Messaging/Notifications/Server.hs | 10 +-- .../Messaging/Notifications/Server/Env.hs | 8 +-- .../Messaging/Notifications/Transport.hs | 44 +++++++----- src/Simplex/Messaging/Protocol.hs | 23 +++--- src/Simplex/Messaging/Server.hs | 18 ++--- src/Simplex/Messaging/Transport.hs | 70 ++++++++++++------- tests/CoreTests/BatchingTests.hs | 21 ++++-- tests/NtfClient.hs | 8 +-- tests/NtfServerTests.hs | 6 +- tests/SMPClient.hs | 36 +++++----- tests/SMPProxyTests.hs | 9 +-- tests/ServerTests.hs | 35 +++++----- 17 files changed, 198 insertions(+), 154 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index fcb54aece..a223d492e 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -51,7 +51,7 @@ import Simplex.Messaging.Protocol RecipientId, SenderId, ) -import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), supportedParameters) +import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client @@ -64,7 +64,7 @@ import UnliftIO.Directory data XFTPClient = XFTPClient { http2Client :: HTTP2Client, transportSession :: TransportSession FileResponse, - thParams :: THandleParams XFTPVersion, + thParams :: THandleParams XFTPVersion 'TClient, config :: XFTPClientConfig } @@ -120,19 +120,21 @@ getXFTPClient g transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN atomically $ writeTVar clientVar $ Just c pure c -xftpClientHandshakeV1 :: TVar ChaChaDRG -> VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP -> ExceptT XFTPClientError IO THandleParamsXFTP +xftpClientHandshakeV1 :: TVar ChaChaDRG -> VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient) xftpClientHandshakeV1 g serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do - shs <- getServerHandshake + shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake (v, sk) <- processServerHandshake shs (k, pk) <- atomically $ C.generateKeyPair g sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash, authPubKey = k} - pure thParams0 {thAuth = Just THandleAuth {peerPubKey = sk, privKey = pk}, thVersion = v} + pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, clientPrivKey = pk}, thVersion = v} where + getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake getServerHandshake = do let helloReq = H.requestNoBody "POST" "/" [] HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing liftHS . smpDecode =<< liftHS (C.unPad shsBody) + processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION case xftpVersionRange `compatibleVersion` serverVRange of @@ -145,6 +147,7 @@ xftpClientHandshakeV1 g serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessi _ -> throwError "bad certificate" pubKey <- maybe (throwError "bad server key type") (`C.verifyX509` exact) serverKey C.x509ToPublic (pubKey, []) >>= C.pubKey + sendClientHandshake :: XFTPClientHandshake -> ExceptT XFTPClientError IO () sendClientHandshake chs = do chs' <- liftHS $ C.pad (smpEncode chs) xftpBlockSize let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs' diff --git a/src/Simplex/FileTransfer/Protocol.hs b/src/Simplex/FileTransfer/Protocol.hs index f970fdfcc..418e48482 100644 --- a/src/Simplex/FileTransfer/Protocol.hs +++ b/src/Simplex/FileTransfer/Protocol.hs @@ -39,8 +39,8 @@ import Simplex.Messaging.Protocol ProtocolErrorType (..), ProtocolMsgTag (..), ProtocolType (..), - RcvPublicDhKey, RcvPublicAuthKey, + RcvPublicDhKey, RecipientId, SenderId, SentRawTransmission, @@ -48,14 +48,14 @@ import Simplex.Messaging.Protocol SndPublicAuthKey, Transmission, TransmissionForAuth (..), - encodeTransmissionForAuth, encodeTransmission, + encodeTransmissionForAuth, messageTagP, tDecodeParseValidate, tEncodeBatch1, tParse, ) -import Simplex.Messaging.Transport (THandleParams (..), TransportError (..)) +import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Util ((<$?>)) xftpBlockSize :: Int @@ -325,12 +325,12 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of Just Refl -> Just c _ -> Nothing -xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString +xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg) xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) corrId tForAuth -xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> Transmission c -> Either TransportError ByteString +xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString xftpEncodeTransmission thParams (corrId, fId, msg) = do let t = encodeTransmission thParams (corrId, fId, msg) xftpEncodeBatch1 (Nothing, t) @@ -339,7 +339,7 @@ xftpEncodeTransmission thParams (corrId, fId, msg) = do xftpEncodeBatch1 :: SentRawTransmission -> Either TransportError ByteString xftpEncodeBatch1 t = first (const TELargeMsg) $ C.pad (tEncodeBatch1 t) xftpBlockSize -xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> ByteString -> Either XFTPErrorType (SignedTransmission e c) +xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> ByteString -> Either XFTPErrorType (SignedTransmission e c) xftpDecodeTransmission thParams t = do t' <- first (const BLOCK) $ C.unPad t case tParse thParams t' of diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index b426360ea..b3bee134f 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -56,7 +56,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.Stats import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..)) +import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..)) import Simplex.Messaging.Transport.Buffer (trimCR) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize) @@ -75,7 +75,7 @@ import qualified UnliftIO.Exception as E type M a = ReaderT XFTPEnv IO a data XFTPTransportRequest = XFTPTransportRequest - { thParams :: THandleParamsXFTP, + { thParams :: THandleParamsXFTP 'TServer, reqBody :: HTTP2Body, request :: H.Request, sendResponse :: H.Response -> IO () @@ -91,7 +91,7 @@ runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpSer data Handshake = HandshakeSent C.PrivateKeyX25519 - | HandshakeAccepted THandleAuth VersionXFTP + | HandshakeAccepted (THandleAuth 'TServer) VersionXFTP xftpServer :: XFTPServerConfig -> TMVar Bool -> M () xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do @@ -120,7 +120,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira Nothing -> pure () -- handshake response sent Just thParams -> processRequest req0 {thParams} -- proceed with new version (XXX: may as well switch the request handler here) _ -> liftIO . sendResponse $ H.responseNoBody N.ok200 [] -- shouldn't happen: means server picked handshake protocol it doesn't know about - xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion)) + xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion 'TServer)) xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do s <- atomically $ TM.lookup sessionId sessions r <- runExceptT $ case s of @@ -138,18 +138,18 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira shs <- encodeXftp hs liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing - processClientHandshake privKey = do + processClientHandshake pk = do unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE body <- liftHS $ C.unPad bodyHead XFTPClientHandshake {xftpVersion, keyHash, authPubKey} <- liftHS $ smpDecode body kh <- asks serverIdentity unless (keyHash == kh) $ throwError HANDSHAKE unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE - let auth = THandleAuth {peerPubKey = authPubKey, privKey} + let auth = THAuthServer {clientPeerPubKey = authPubKey, serverPrivKey = pk} atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions liftIO . sendResponse $ H.responseNoBody N.ok200 [] pure Nothing - sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion)) + sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer)) sendError err = do runExceptT (encodeXftp err) >>= \case Right bs -> liftIO . sendResponse $ H.responseBuilder N.ok200 [] bs @@ -326,7 +326,7 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea data VerificationResult = VRVerified XFTPRequest | VRFailed -verifyXFTPTransmission :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult +verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult verifyXFTPTransmission auth_ tAuth authorized fId cmd = case cmd of FileCmd SFSender (FNEW file rcps auth') -> pure $ XFTPReqNew file rcps auth' `verifyWith` sndKey file diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index d8bb72f6b..041069e98 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} @@ -51,7 +52,7 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol (CommandError) -import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..)) +import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.File import Simplex.Messaging.Util (bshow) import Simplex.Messaging.Version @@ -76,8 +77,8 @@ type VersionRangeXFTP = VersionRange XFTPVersion pattern VersionXFTP :: Word16 -> VersionXFTP pattern VersionXFTP v = Version v -type THandleXFTP c = THandle XFTPVersion c -type THandleParamsXFTP = THandleParams XFTPVersion +type THandleXFTP c p = THandle XFTPVersion c p +type THandleParamsXFTP p = THandleParams XFTPVersion p initialXFTPVersion :: VersionXFTP initialXFTPVersion = VersionXFTP 1 @@ -89,7 +90,7 @@ supportedFileServerVRange :: VersionRangeXFTP supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion -- XFTP protocol does not support handshake -xftpClientHandshakeStub :: c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c) +xftpClientHandshakeStub :: c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient) xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwError $ TEHandshake VERSION data XFTPServerHandshake = XFTPServerHandshake diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index e0591b14d..61ec96003 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -84,8 +84,8 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception import Control.Monad -import Control.Monad.IO.Class (liftIO) import Control.Monad.Except +import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J @@ -119,7 +119,7 @@ import System.Timeout (timeout) -- Use 'getSMPClient' to connect to an SMP server and create a client handle. data ProtocolClient v err msg = ProtocolClient { action :: Maybe (Async ()), - thParams :: THandleParams v, + thParams :: THandleParams v 'TClient, sessionTs :: UTCTime, client_ :: PClient v err msg } @@ -138,7 +138,7 @@ data PClient v err msg = PClient msgQ :: Maybe (TBQueue (ServerTransmission v msg)) } -smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe THandleAuth -> STM SMPClient +smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient smpClientStub g sessionId thVersion thAuth = do connected <- newTVar False clientCorrId <- C.newRandomDRG g @@ -387,10 +387,10 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize raceAny_ ([send c' th, process c', receive c' th] <> [ping c' | smpPingInterval > 0]) `finally` disconnected c' - send :: Transport c => ProtocolClient v err msg -> THandle v c -> IO () + send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= tPutLog h - receive :: Transport c => ProtocolClient v err msg -> THandle v c -> IO () + receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () receive ProtocolClient {client_ = PClient {rcvQ}} h = forever $ tGet h >>= atomically . writeTBQueue rcvQ ping :: ProtocolClient v err msg -> IO () @@ -733,13 +733,13 @@ mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCo TM.insert corrId r sentCommands pure r -authTransmission :: Maybe THandleAuth -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth) +authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth) authTransmission thAuth pKey_ (CorrId corrId) t = traverse authenticate pKey_ where authenticate :: C.APrivateAuthKey -> Either TransportError TransmissionAuth authenticate (C.APrivateAuthKey a pk) = case a of C.SX25519 -> case thAuth of - Just THandleAuth {peerPubKey} -> Right $ TAAuthenticator $ C.cbAuthenticate peerPubKey pk (C.cbNonce corrId) t + Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t Nothing -> Left TENoServerAuth C.SEd25519 -> sign pk C.SEd448 -> sign pk diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 55ab40718..55bdb07eb 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -47,7 +47,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server import Simplex.Messaging.Server.Stats import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..)) +import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..)) import Simplex.Messaging.Transport.Server (runTransportServer, tlsServerCredentials) import Simplex.Messaging.Util import System.Exit (exitFailure) @@ -339,7 +339,7 @@ updateTknStatus NtfTknData {ntfTknId, tknStatus} status = do old <- atomically $ stateTVar tknStatus (,status) when (old /= status) $ withNtfLog $ \sl -> logTokenStatus sl ntfTknId status -runNtfClientTransport :: Transport c => THandleNTF c -> M () +runNtfClientTransport :: Transport c => THandleNTF c 'TServer -> M () runNtfClientTransport th@THandle {params} = do qSize <- asks $ clientQSize . config ts <- liftIO getSystemTime @@ -356,7 +356,7 @@ runNtfClientTransport th@THandle {params} = do clientDisconnected :: NtfServerClient -> IO () clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connected False -receive :: Transport c => THandleNTF c -> NtfServerClient -> M () +receive :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> M () receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do ts <- liftIO $ tGet th forM_ ts $ \t@(_, _, (corrId, entId, cmdOrError)) -> do @@ -371,7 +371,7 @@ receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ where write q t = atomically $ writeTBQueue q t -send :: Transport c => THandleNTF c -> NtfServerClient -> IO () +send :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> IO () send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do t <- atomically $ readTBQueue sndQ void . liftIO $ tPut h [Right (Nothing, encodeTransmission params t)] @@ -382,7 +382,7 @@ send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do data VerificationResult = VRVerified NtfRequest | VRFailed -verifyNtfTransmission :: Maybe (THandleAuth, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult +verifyNtfTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult verifyNtfTransmission auth_ (tAuth, authorized, (corrId, entId, _)) cmd = do st <- asks store case cmd of diff --git a/src/Simplex/Messaging/Notifications/Server/Env.hs b/src/Simplex/Messaging/Notifications/Server/Env.hs index 4a93a8a34..5bcd72f3d 100644 --- a/src/Simplex/Messaging/Notifications/Server/Env.hs +++ b/src/Simplex/Messaging/Notifications/Server/Env.hs @@ -24,16 +24,16 @@ import Numeric.Natural import Simplex.Messaging.Client.Agent import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol -import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Server.Stats import Simplex.Messaging.Notifications.Server.Store import Simplex.Messaging.Notifications.Server.StoreLog +import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF) import Simplex.Messaging.Protocol (CorrId, SMPServer, Transmission) import Simplex.Messaging.Server.Expiration import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, THandleParams) +import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) @@ -161,13 +161,13 @@ data NtfRequest data NtfServerClient = NtfServerClient { rcvQ :: TBQueue NtfRequest, sndQ :: TBQueue (Transmission NtfResponse), - ntfThParams :: THandleParams NTFVersion, + ntfThParams :: THandleParams NTFVersion 'TServer, connected :: TVar Bool, rcvActiveAt :: TVar SystemTime, sndActiveAt :: TVar SystemTime } -newNtfServerClient :: Natural -> THandleParams NTFVersion -> SystemTime -> STM NtfServerClient +newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> STM NtfServerClient newNtfServerClient qSize ntfThParams ts = do rcvQ <- newTBQueue qSize sndQ <- newTBQueue qSize diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index bc68fab03..d4a4a4cbe 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -5,6 +5,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} module Simplex.Messaging.Notifications.Transport where @@ -18,9 +19,9 @@ import qualified Data.X509 as X import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Transport +import Simplex.Messaging.Util (liftEitherWith) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal -import Simplex.Messaging.Util (liftEitherWith) ntfBlockSize :: Int ntfBlockSize = 512 @@ -54,7 +55,7 @@ supportedClientNTFVRange = mkVersionRange initialNTFVersion currentClientNTFVers supportedServerNTFVRange :: VersionRangeNTF supportedServerNTFVRange = mkVersionRange initialNTFVersion currentServerNTFVersion -type THandleNTF c = THandle NTFVersion c +type THandleNTF c p = THandle NTFVersion c p data NtfServerHandshake = NtfServerHandshake { ntfVersionRange :: VersionRangeNTF, @@ -111,7 +112,7 @@ encodeNtfAuthPubKey v k | otherwise = "" -- | Notifcations server transport handshake. -ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c) +ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TServer) ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c let sk = C.signX509 serverSignKey $ C.publicToX509 k @@ -121,11 +122,11 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do | keyHash /= kh -> throwError $ TEHandshake IDENTITY | v `isCompatible` ntfVRange -> - pure $ ntfThHandle th v pk k' + pure $ ntfThHandleServer th v pk k' | otherwise -> throwError $ TEHandshake VERSION -- | Notifcations server client transport handshake. -ntfClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c) +ntfClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient) ntfClientHandshake c (k, pk) keyHash ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th @@ -133,23 +134,32 @@ ntfClientHandshake c (k, pk) keyHash ntfVRange = do then throwError TEBadSession else case ntfVersionRange `compatibleVersion` ntfVRange of Just (Compatible v) -> do - sk_ <- forM sk' $ \exact -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do + ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do serverKey <- getServerVerifyKey c - pubKey <- C.verifyX509 serverKey exact - C.x509ToPublic (pubKey, []) >>= C.pubKey + pubKey <- C.verifyX509 serverKey signedKey + (,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash, authPubKey = Just k} - pure $ ntfThHandle th v pk sk_ + pure $ ntfThHandleClient th v pk ck_ Nothing -> throwError $ TEHandshake VERSION -ntfThHandle :: forall c. THandleNTF c -> VersionNTF -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleNTF c -ntfThHandle th@THandle {params} v privKey k_ = - -- TODO drop SMP v6: make thAuth non-optional - let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_ - v3 = v >= authBatchCmdsNTFVersion - params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3} - in (th :: THandleNTF c) {params = params'} +ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleNTF c 'TServer +ntfThHandleServer th v pk k_ = + let thAuth = (\k -> THAuthServer {clientPeerPubKey = k, serverPrivKey = pk}) <$> k_ + in ntfThHandle_ th v thAuth -ntfTHandle :: Transport c => c -> THandleNTF c +ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient +ntfThHandleClient th v pk ck_ = + let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, clientPrivKey = pk}) <$> ck_ + in ntfThHandle_ th v thAuth + +ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> Maybe (THandleAuth p) -> THandleNTF c p +ntfThHandle_ th@THandle {params} v thAuth = + -- TODO drop SMP v6: make thAuth non-optional + let v3 = v >= authBatchCmdsNTFVersion + params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3} + in (th :: THandleNTF c p) {params = params'} + +ntfTHandle :: Transport c => c -> THandleNTF c p ntfTHandle c = THandle {connection = c, params} where params = THandleParams {sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = VersionNTF 0, thAuth = Nothing, implySessId = False, batch = False} diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 77b14e23c..b5a3e7770 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -1135,7 +1135,7 @@ data CommandError deriving (Eq, Read, Show) -- | SMP transmission parser. -transmissionP :: THandleParams v -> Parser RawTransmission +transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do authenticator <- smpP authorized <- A.takeByteString @@ -1149,10 +1149,10 @@ transmissionP THandleParams {sessionId, implySessId} = do command <- A.takeByteString pure RawTransmission {authenticator, authorized = authorized', sessId, corrId, entityId, command} -class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where +class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where type ProtoCommand msg = cmd | cmd -> msg type ProtoType msg = (sch :: ProtocolType) | sch -> msg - protocolClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c) + protocolClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c 'TClient) protocolPing :: ProtoCommand msg protocolError :: msg -> Maybe err @@ -1258,7 +1258,6 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where PFWD_ -> error "TODO: PFWD_" RFWD_ -> error "TODO: RFWD_" PRXY_ -> PRXY <$> (_smpP >>= either fail pure . strDecode) <*> _smpP - CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg @@ -1386,7 +1385,7 @@ instance Encoding CommandError where _ -> fail "bad command error type" -- | Send signed SMP transmission to TCP transport. -tPut :: Transport c => THandle v c -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] +tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (batch params) (blockSize params) where tPutBatch :: TransportBatch () -> IO [Either TransportError ()] @@ -1395,7 +1394,7 @@ tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (ba TBTransmissions s n _ -> replicate n <$> tPutLog th s TBTransmission s _ -> (: []) <$> tPutLog th s -tPutLog :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ()) +tPutLog :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) tPutLog th s = do r <- tPutBlock th s case r of @@ -1461,7 +1460,7 @@ tEncodeBatch1 t = lenEncode 1 `B.cons` tEncodeForBatch t -- tForAuth is lazy to avoid computing it when there is no key to sign data TransmissionForAuth = TransmissionForAuth {tForAuth :: ~ByteString, tToSend :: ByteString} -encodeTransmissionForAuth :: ProtocolEncoding v e c => THandleParams v -> Transmission c -> TransmissionForAuth +encodeTransmissionForAuth :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> TransmissionForAuth encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId} t = TransmissionForAuth {tForAuth, tToSend = if implySessId then t' else tForAuth} where @@ -1469,7 +1468,7 @@ encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId} t' = encodeTransmission_ v t {-# INLINE encodeTransmissionForAuth #-} -encodeTransmission :: ProtocolEncoding v e c => THandleParams v -> Transmission c -> ByteString +encodeTransmission :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> ByteString encodeTransmission THandleParams {thVersion = v, sessionId, implySessId} t = if implySessId then t' else smpEncode sessionId <> t' where @@ -1482,11 +1481,11 @@ encodeTransmission_ v (CorrId corrId, queueId, command) = {-# INLINE encodeTransmission_ #-} -- | Receive and parse transmission from the TCP transport (ignoring any trailing padding). -tGetParse :: Transport c => THandle v c -> IO (NonEmpty (Either TransportError RawTransmission)) +tGetParse :: Transport c => THandle v c p -> IO (NonEmpty (Either TransportError RawTransmission)) tGetParse th@THandle {params} = eitherList (tParse params) <$> tGetBlock th {-# INLINE tGetParse #-} -tParse :: THandleParams v -> ByteString -> NonEmpty (Either TransportError RawTransmission) +tParse :: THandleParams v p -> ByteString -> NonEmpty (Either TransportError RawTransmission) tParse thParams@THandleParams {batch} s | batch = eitherList (L.map (\(Large t) -> tParse1 t)) ts | otherwise = [tParse1 s] @@ -1498,10 +1497,10 @@ eitherList :: (a -> NonEmpty (Either e b)) -> Either e a -> NonEmpty (Either e b eitherList = either (\e -> [Left e]) -- | Receive client and server transmissions (determined by `cmd` type). -tGet :: forall v err cmd c. (ProtocolEncoding v err cmd, Transport c) => THandle v c -> IO (NonEmpty (SignedTransmission err cmd)) +tGet :: forall v err cmd c p. (ProtocolEncoding v err cmd, Transport c) => THandle v c p -> IO (NonEmpty (SignedTransmission err cmd)) tGet th@THandle {params} = L.map (tDecodeParseValidate params) <$> tGetParse th -tDecodeParseValidate :: forall v err cmd. ProtocolEncoding v err cmd => THandleParams v -> Either TransportError RawTransmission -> SignedTransmission err cmd +tDecodeParseValidate :: forall v p err cmd. ProtocolEncoding v err cmd => THandleParams v p -> Either TransportError RawTransmission -> SignedTransmission err cmd tDecodeParseValidate THandleParams {sessionId, thVersion = v, implySessId} = \case Right RawTransmission {authenticator, authorized, sessId, corrId, entityId, command} | implySessId || sessId == sessionId -> diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 456de5be2..2ff0d5e51 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -409,7 +409,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do logError "Unauthorized control port command" hPutStrLn h "AUTH" -runClientTransport :: Transport c => THandleSMP c -> M () +runClientTransport :: Transport c => THandleSMP c 'TServer -> M () runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = do q <- asks $ tbqSize . config ts <- liftIO getSystemTime @@ -457,7 +457,7 @@ cancelSub sub = Sub {subThread = SubThread t} -> liftIO $ deRefWeak t >>= mapM_ killThread _ -> return () -receive :: Transport c => THandleSMP c -> Client -> M () +receive :: Transport c => THandleSMP c 'TServer -> Client -> M () receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " receive" forever $ do @@ -478,7 +478,7 @@ receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActi VRFailed -> Left (corrId, queueId, ERR AUTH) write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty -send :: Transport c => THandleSMP c -> Client -> IO () +send :: Transport c => THandleSMP c 'TServer -> Client -> IO () send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " send" forever $ do @@ -493,7 +493,7 @@ send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do NMSG {} -> 0 _ -> 1 -disconnectTransport :: Transport c => THandle v c -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () +disconnectTransport :: Transport c => THandle v c 'TServer -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () disconnectTransport THandle {connection, params = THandleParams {sessionId}} rcvActiveAt sndActiveAt expCfg noSubscriptions = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " disconnectTransport" loop @@ -514,7 +514,7 @@ data VerificationResult = VRVerified (Maybe QueueRec) | VRFailed -- - the queue or party key do not exist. -- In all cases, the time of the verification should depend only on the provided authorization type, -- a dummy key is used to run verification in the last two cases, and failure is returned irrespective of the result. -verifyTransmission :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult +verifyTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult verifyTransmission auth_ tAuth authorized queueId cmd = case cmd of Cmd SRecipient (NEW k _ _ _) -> pure $ Nothing `verifiedWith` k @@ -539,7 +539,7 @@ verifyTransmission auth_ tAuth authorized queueId cmd = st <- asks queueStore atomically $ getQueue st party queueId -verifyCmdAuthorization :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool +verifyCmdAuthorization :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAuth where verify :: C.APublicAuthKey -> TransmissionAuth -> Bool @@ -551,12 +551,12 @@ verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAu C.SX25519 -> verifyCmdAuth auth_ k s authorized _ -> verifyCmdAuth auth_ dummyKeyX25519 s authorized `seq` False -verifyCmdAuth :: Maybe (THandleAuth, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool +verifyCmdAuth :: Maybe (THandleAuth 'TServer, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool verifyCmdAuth auth_ k authenticator authorized = case auth_ of - Just (THandleAuth {privKey}, nonce) -> C.cbVerify k privKey nonce authenticator authorized + Just (THAuthServer {serverPrivKey = pk}, nonce) -> C.cbVerify k pk nonce authenticator authorized Nothing -> False -dummyVerifyCmd :: Maybe (THandleAuth, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool +dummyVerifyCmd :: Maybe (THandleAuth 'TServer, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool dummyVerifyCmd auth_ authorized = \case TASignature (C.ASignature a s) -> C.verify' (dummySignKey a) s authorized TAAuthenticator s -> verifyCmdAuth auth_ dummyKeyX25519 s authorized diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 3d4916b92..565199c36 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -12,6 +12,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} -- | @@ -315,20 +316,20 @@ instance Transport TLS where -- * SMP transport -- | The handle for SMP encrypted transport connection over Transport. -data THandle v c = THandle +data THandle v c p = THandle { connection :: c, - params :: THandleParams v + params :: THandleParams v p } -type THandleSMP c = THandle SMPVersion c +type THandleSMP c p = THandle SMPVersion c p -data THandleParams v = THandleParams +data THandleParams v p = THandleParams { sessionId :: SessionId, blockSize :: Int, -- | agreed server protocol version thVersion :: Version v, -- | peer public key for command authorization and shared secrets for entity ID encryption - thAuth :: Maybe THandleAuth, + thAuth :: Maybe (THandleAuth p), -- | do NOT send session ID in transmission, but include it into signed message -- based on protocol version implySessId :: Bool, @@ -337,10 +338,18 @@ data THandleParams v = THandleParams batch :: Bool } -data THandleAuth = THandleAuth - { peerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key - privKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server) - } +data THandleAuth (p :: TransportPeer) where + THAuthClient :: + { serverPeerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key + serverCertKey :: (X.CertificateChain, X.SignedExact X.PubKey), -- the key here is clientPrivKey signed with server certificate + clientPrivKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server) + } -> + THandleAuth 'TClient + THAuthServer :: + { clientPeerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key + serverPrivKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server) + } -> + THandleAuth 'TServer -- | TLS-unique channel binding type SessionId = ByteString @@ -442,13 +451,13 @@ serializeTransportError = \case TEHandshake e -> "HANDSHAKE " <> bshow e -- | Pad and send block to SMP transport. -tPutBlock :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ()) +tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) tPutBlock THandle {connection = c, params = THandleParams {blockSize}} block = bimapM (const $ pure TELargeMsg) (cPut c) $ C.pad block blockSize -- | Receive block from SMP transport. -tGetBlock :: Transport c => THandle v c -> IO (Either TransportError ByteString) +tGetBlock :: Transport c => THandle v c p -> IO (Either TransportError ByteString) tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do msg <- cGet c blockSize if B.length msg == blockSize @@ -458,7 +467,7 @@ tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do -- | Server SMP transport handshake. -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c) +smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TServer) smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do let th@THandle {params = THandleParams {sessionId}} = smpTHandle c sk = C.signX509 serverSignKey $ C.publicToX509 k @@ -469,13 +478,13 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do | keyHash /= kh -> throwE $ TEHandshake IDENTITY | v `isCompatible` smpVRange -> - pure $ smpThHandle th v pk k' + pure $ smpThHandleServer th v pk k' | otherwise -> throwE $ TEHandshake VERSION -- | Client SMP transport handshake. -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -smpClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c) +smpClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TClient) smpClientHandshake c (k, pk) keyHash@(C.KeyHash kh) smpVRange = do let th@THandle {params = THandleParams {sessionId}} = smpTHandle c ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th @@ -483,33 +492,42 @@ smpClientHandshake c (k, pk) keyHash@(C.KeyHash kh) smpVRange = do then throwE TEBadSession else case smpVersionRange `compatibleVersion` smpVRange of Just (Compatible v) -> do - sk_ <- forM authPubKey $ \(X.CertificateChain cert, exact) -> + ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () _ -> throwError "bad certificate" serverKey <- getServerVerifyKey c pubKey <- C.verifyX509 serverKey exact - C.x509ToPublic (pubKey, []) >>= C.pubKey + (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = Just k} - pure $ smpThHandle th v pk sk_ + pure $ smpThHandleClient th v pk ck_ Nothing -> throwE $ TEHandshake VERSION -smpThHandle :: forall c. THandleSMP c -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c -smpThHandle th@THandle {params} v privKey k_ = - -- TODO drop SMP v6: make thAuth non-optional - let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_ - params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion} - in (th :: THandleSMP c) {params = params'} +smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer +smpThHandleServer th v pk k_ = + let thAuth = (\k -> THAuthServer {clientPeerPubKey = k, serverPrivKey = pk}) <$> k_ + in smpThHandle_ th v thAuth -sendHandshake :: (Transport c, Encoding smp) => THandle v c -> smp -> ExceptT TransportError IO () +smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient +smpThHandleClient th v pk ck_ = + let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, clientPrivKey = pk}) <$> ck_ + in smpThHandle_ th v thAuth + +smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> THandleSMP c p +smpThHandle_ th@THandle {params} v thAuth = + -- TODO drop SMP v6: make thAuth non-optional + let params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion} + in (th :: THandleSMP c p) {params = params'} + +sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO () sendHandshake th = ExceptT . tPutBlock th . smpEncode -- ignores tail bytes to allow future extensions -getHandshake :: (Transport c, Encoding smp) => THandle v c -> ExceptT TransportError IO smp +getHandshake :: (Transport c, Encoding smp) => THandle v c p -> ExceptT TransportError IO smp getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP =<<) <$> tGetBlock th -smpTHandle :: Transport c => c -> THandleSMP c +smpTHandle :: Transport c => c -> THandleSMP c p smpTHandle c = THandle {connection = c, params} where params = THandleParams {sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = VersionSMP 0, thAuth = Nothing, implySessId = False, batch = True} diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index 996d2fed1..c349096dc 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} module CoreTests.BatchingTests (batchingTests) where @@ -11,6 +13,9 @@ import Crypto.Random (ChaChaDRG) import qualified Data.ByteString as B import Data.ByteString.Char8 (ByteString) import qualified Data.List.NonEmpty as L +import qualified Data.X509 as X +import qualified Data.X509.CertificateStore as XS +import qualified Data.X509.File as XF import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol @@ -314,7 +319,7 @@ randomSEND_ a v sessId len = do TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, sId, Cmd SSender $ SEND noMsgFlags msg) pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) corrId tForAuth -testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion +testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient testTHandleParams v sessionId = THandleParams { sessionId, @@ -325,11 +330,17 @@ testTHandleParams v sessionId = batch = True } -testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe THandleAuth) -testTHandleAuth v g (C.APublicAuthKey a k) = case a of +testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe (THandleAuth 'TClient)) +testTHandleAuth v g (C.APublicAuthKey a serverPeerPubKey) = case a of C.SX25519 | v >= authCmdsSMPVersion -> do - (_, privKey) <- atomically $ C.generateKeyPair g - pure $ Just THandleAuth {peerPubKey = k, privKey} + (_, clientPrivKey) <- atomically $ C.generateKeyPair @'C.X25519 g + ca <- head <$> XS.readCertificates "tests/fixtures/ca.crt" + serverCert <- head <$> XS.readCertificates "tests/fixtures/server.crt" + serverKey <- head <$> XF.readKeyFile "tests/fixtures/server.key" + signKey <- either error pure $ C.x509ToPrivate (serverKey, []) >>= C.privKey @C.APrivateSignKey + (serverAuthPub, _) <- atomically $ C.generateKeyPair @'C.X25519 g + let serverCertKey = (X.CertificateChain [serverCert, ca], C.signX509 signKey $ C.toPubKey C.publicToX509 serverAuthPub) + pure $ Just THAuthClient {serverPeerPubKey, serverCertKey, clientPrivKey} _ -> pure Nothing randomSENDCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> Int -> IO (PCTransmission ErrorType BrokerMsg) diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index 26981628f..d47722944 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -70,7 +70,7 @@ testKeyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=" ntfTestStoreLogFile :: FilePath ntfTestStoreLogFile = "tests/tmp/ntf-server-store.log" -testNtfClient :: Transport c => (THandleNTF c -> IO a) -> IO a +testNtfClient :: Transport c => (THandleNTF c 'TClient -> IO a) -> IO a testNtfClient client = do Right host <- pure $ chooseTransportHost defaultNetworkConfig testHost runTransportClient defaultTransportClientConfig Nothing host ntfTestPort (Just testKeyHash) $ \h -> do @@ -139,7 +139,7 @@ withNtfServerOn t port' = withNtfServerThreadOn t port' . const withNtfServer :: ATransport -> IO a -> IO a withNtfServer t = withNtfServerOn t ntfTestPort -runNtfTest :: forall c a. Transport c => (THandleNTF c -> IO a) -> IO a +runNtfTest :: forall c a. Transport c => (THandleNTF c 'TClient -> IO a) -> IO a runNtfTest test = withNtfServer (transport @c) $ testNtfClient test ntfServerTest :: @@ -150,7 +150,7 @@ ntfServerTest :: IO (Maybe TransmissionAuth, ByteString, ByteString, NtfResponse) ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h where - tPut' :: THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () + tPut' :: THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp) [Right ()] <- tPut h [Right (sig, t')] @@ -159,7 +159,7 @@ ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h [(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h pure (Nothing, corrId, qId, cmd) -ntfTest :: Transport c => TProxy c -> (THandleNTF c -> IO ()) -> Expectation +ntfTest :: Transport c => TProxy c -> (THandleNTF c 'TClient -> IO ()) -> Expectation ntfTest _ test' = runNtfTest test' `shouldReturn` () data APNSMockRequest = APNSMockRequest diff --git a/tests/NtfServerTests.hs b/tests/NtfServerTests.hs index e7e2018c2..e5f096eef 100644 --- a/tests/NtfServerTests.hs +++ b/tests/NtfServerTests.hs @@ -6,8 +6,8 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module NtfServerTests where @@ -72,13 +72,13 @@ pattern RespNtf corrId queueId command <- (_, _, (corrId, queueId, Right command deriving instance Eq NtfResponse -sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) +sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) sendRecvNtf h@THandle {params} (sgn, corrId, qId, cmd) = do let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (sgn, tToSend) tGet1 h -signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) +signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) signSendRecvNtf h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (authorize tForAuth, tToSend) diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index b63668f7a..cbdf4319d 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -67,15 +67,15 @@ xit'' d t = do ci <- runIO $ lookupEnv "CI" (if ci == Just "true" then skip "skipped on CI" . it d else it d) t -testSMPClient :: Transport c => (THandleSMP c -> IO a) -> IO a +testSMPClient :: Transport c => (THandleSMP c 'TClient -> IO a) -> IO a testSMPClient = testSMPClientVR supportedClientSMPRelayVRange -testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a +testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a testSMPClientVR vr client = do Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost testSMPClient_ useHost testPort vr client -testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a +testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a testSMPClient_ host port vr client = do runTransportClient defaultTransportClientConfig Nothing host port (Just testKeyHash) $ \h -> do g <- C.newRandom @@ -119,7 +119,7 @@ cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} proxyCfg :: ServerConfig -proxyCfg = cfg { allowSMPProxy = True } +proxyCfg = cfg {allowSMPProxy = True} withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} @@ -158,16 +158,16 @@ withSmpServer t = withSmpServerOn t testPort withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const -runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c -> IO a) -> IO a +runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c 'TClient -> IO a) -> IO a runSmpTest test = withSmpServer (transport @c) $ testSMPClient test -runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a +runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a runSmpTestN = runSmpTestNCfg cfg supportedClientSMPRelayVRange -runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a +runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a runSmpTestNCfg srvCfg clntVR nClients test = withSmpServerConfigOn (transport @c) srvCfg testPort $ \_ -> run nClients [] where - run :: Int -> [THandleSMP c] -> IO a + run :: Int -> [THandleSMP c 'TClient] -> IO a run 0 hs = test hs run n hs = testSMPClientVR clntVR $ \h -> run (n - 1) (h : hs) @@ -179,7 +179,7 @@ smpServerTest :: IO (Maybe TransmissionAuth, ByteString, ByteString, BrokerMsg) smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h where - tPut' :: THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () + tPut' :: THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp) [Right ()] <- tPut h [Right (sig, t')] @@ -188,33 +188,33 @@ smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h [(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h pure (Nothing, corrId, qId, cmd) -smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> IO ()) -> Expectation +smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> IO ()) -> Expectation smpTest _ test' = runSmpTest test' `shouldReturn` () -smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO ()) -> Expectation +smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO ()) -> Expectation smpTestN n test' = runSmpTestN n test' `shouldReturn` () -smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest2 = smpTest2Cfg cfg supportedClientSMPRelayVRange -smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest2Cfg srvCfg clntVR _ test' = runSmpTestNCfg srvCfg clntVR 2 _test `shouldReturn` () where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2] = test' h1 h2 _test _ = error "expected 2 handles" -smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest3 _ test' = smpTestN 3 _test where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2, h3] = test' h1 h2 h3 _test _ = error "expected 3 handles" -smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest4 _ test' = smpTestN 4 _test where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2, h3, h4] = test' h1 h2 h3 h4 _test _ = error "expected 4 handles" diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 0a429fc57..6cdf1f590 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -5,6 +6,7 @@ module SMPProxyTests where +import Debug.Trace import SMPAgentClient (testSMPServer, testSMPServer2) import SMPClient import ServerTests (sendRecv) @@ -13,7 +15,6 @@ import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Version (mkVersionRange) import Test.Hspec -import Debug.Trace smpProxyTests :: Spec smpProxyTests = do @@ -39,14 +40,14 @@ proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion testNoProxy :: IO () testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do - testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS) -> do + testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) reply `shouldBe` Right (ERR AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do - testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") traceShowM s reply `shouldBe` Right (ERR AUTH) @@ -56,7 +57,7 @@ testProxyAuth = do testProxyConnect :: IO () testProxyConnect = do withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do - testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 Nothing) case reply of Right PKEY {} -> pure () diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 3b5f2f5dc..a7e8fee86 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -78,13 +78,13 @@ pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh) pattern Msg :: MsgId -> MsgBody -> BrokerMsg pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body} -sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) +sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) sendRecv h@THandle {params} (sgn, corrId, qId, cmd) = do let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (sgn, tToSend) tGet1 h -signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) +signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (authorize tForAuth, tToSend) @@ -93,17 +93,17 @@ signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do authorize t = case a of C.SEd25519 -> Just . TASignature . C.ASignature C.SEd25519 $ C.sign' pk t C.SEd448 -> Just . TASignature . C.ASignature C.SEd448 $ C.sign' pk t - C.SX25519 -> (\THandleAuth {peerPubKey} -> TAAuthenticator $ C.cbAuthenticate peerPubKey pk (C.cbNonce corrId) t) <$> thAuth params + C.SX25519 -> (\THAuthClient {serverPeerPubKey = k} -> TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t) <$> thAuth params #if !MIN_VERSION_base(4,18,0) _sx448 -> undefined -- ghc8107 fails to the branch excluded by types #endif -tPut1 :: Transport c => THandle v c -> SentRawTransmission -> IO (Either TransportError ()) +tPut1 :: Transport c => THandle v c 'TClient -> SentRawTransmission -> IO (Either TransportError ()) tPut1 h t = do [r] <- tPut h [Right t] pure r -tGet1 :: (ProtocolEncoding v err cmd, Transport c) => THandle v c -> IO (SignedTransmission err cmd) +tGet1 :: (ProtocolEncoding v err cmd, Transport c) => THandle v c 'TClient -> IO (SignedTransmission err cmd) tGet1 h = do [r] <- liftIO $ tGet h pure r @@ -555,12 +555,12 @@ testWithStoreLog at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 removeFile testStoreLogFile where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () logSize :: FilePath -> IO Int @@ -653,12 +653,12 @@ testRestoreMessages at@(ATransport t) = removeFile testStoreMsgsFile removeFile testServerStatsBackupFile where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () checkStats :: ServerStatsData -> [RecipientId] -> Int -> Int -> Expectation @@ -727,15 +727,15 @@ testRestoreExpireMessages at@(ATransport t) = Right ServerStatsData {_msgExpired} <- strDecode <$> B.readFile testServerStatsBackupFile _msgExpired `shouldBe` 2 where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () -createAndSecureQueue :: Transport c => THandleSMP c -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret) +createAndSecureQueue :: Transport c => THandleSMP c 'TClient -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret) createAndSecureQueue h sPub = do g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g @@ -759,8 +759,8 @@ testTiming (ATransport t) = timingTests :: [(C.AuthAlg, C.AuthAlg, Int)] timingTests = [ (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd25519, 200), -- correct key type - -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150), - -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200), + -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150), + -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200), (C.AuthAlg C.SEd448, C.AuthAlg C.SEd25519, 200), (C.AuthAlg C.SEd448, C.AuthAlg C.SEd448, 150), -- correct key type (C.AuthAlg C.SEd448, C.AuthAlg C.SX25519, 200), @@ -770,7 +770,7 @@ testTiming (ATransport t) = ] timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const similarTime t1 t2 = abs (t2 / t1 - 1) < 0.2 -- normally the difference between "no queue" and "wrong key" is less than 5% - testSameTiming :: forall c. Transport c => THandleSMP c -> THandleSMP c -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation + testSameTiming :: forall c. Transport c => THandleSMP c 'TClient -> THandleSMP c 'TClient -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation testSameTiming rh sh (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg, n) = do g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g @@ -791,10 +791,11 @@ testTiming (ATransport t) = runTimingTest sh badKey sId $ _SEND "hello" where - runTimingTest :: PartyI p => THandleSMP c -> C.APrivateAuthKey -> ByteString -> Command p -> IO () + runTimingTest :: PartyI p => THandleSMP c 'TClient -> C.APrivateAuthKey -> ByteString -> Command p -> IO () runTimingTest h badKey qId cmd = do threadDelay 100000 - _ <- timeRepeat n $ do -- "warm up" the server + _ <- timeRepeat n $ do + -- "warm up" the server Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd) return () threadDelay 100000 From a3b229f668bc39da24b206c3e7f2f0e567717498 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 18 Apr 2024 22:35:17 +0100 Subject: [PATCH 03/13] SMP proxy: low level client and server implementation (#1096) * SMP proxy: low level client and server implementation * SMP proxy: server implementation (#1098) * wip * PRXY command * progress * SMP Proxy: client-level implementation (#1101) * buildable * encode messages * update pkey * fix queue types * wrap SEND in proxy lookup * WIP proxy client * WIP * post-rebase fixes * encode something with something * cleanup * update * fix nonce/corrId in batchingTests * WIP: dig into createSMPProxySession * agent * test progress * pass the test * parameterize transport handle with transport peer to include server certificate (#1100) * parameterize transport handle with transport peer to include server certificate * include server certificate into THandle * load server chain and sign key * fix key type * fix for 8.10 --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Co-authored-by: IC Rainbow * cleanup * add 2-server test * remove subsumed test * checkCredentials for BrokerMsg * skip batching tests * remove userId param * remove agent changes --------- Co-authored-by: Evgeny Poberezkin --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> * remove unused type * icrease test timeout * reduce transport block * envelope sizes * don't fork unless have proxied commands to process --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Co-authored-by: IC Rainbow --- .github/workflows/build.yml | 2 +- src/Simplex/FileTransfer/Client.hs | 4 +- src/Simplex/FileTransfer/Protocol.hs | 3 +- src/Simplex/Messaging/Agent.hs | 2 +- src/Simplex/Messaging/Client.hs | 149 ++++++++++++-- src/Simplex/Messaging/Client/Agent.hs | 21 +- src/Simplex/Messaging/Crypto.hs | 4 + src/Simplex/Messaging/Protocol.hs | 185 ++++++++++++++---- src/Simplex/Messaging/Server.hs | 185 +++++++++++++----- src/Simplex/Messaging/Server/Env/STM.hs | 35 ++-- src/Simplex/Messaging/Server/Main.hs | 7 +- .../Messaging/Server/QueueStore/STM.hs | 2 +- src/Simplex/Messaging/Transport.hs | 6 + tests/CoreTests/BatchingTests.hs | 12 +- tests/CoreTests/ProtocolErrorTests.hs | 10 +- tests/SMPClient.hs | 11 +- tests/SMPProxyTests.hs | 117 ++++++++--- tests/Test.hs | 2 +- 18 files changed, 583 insertions(+), 174 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4c44cff9..4a61490ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: run: cabal build --enable-tests - name: Test - timeout-minutes: 30 + timeout-minutes: 40 shell: bash run: cabal test --test-show-details=direct diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index a223d492e..2606d1bb9 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -186,9 +186,11 @@ xftpClientError = \case sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body) sendXFTPCommand c@XFTPClient {thParams} pKey fId cmd chunkSpec_ = do + -- TODO random corrId + let corrIdUsedAsNonce = "" t <- liftEither . first PCETransportError $ - xftpEncodeAuthTransmission thParams pKey ("", fId, FileCmd (sFileParty @p) cmd) + xftpEncodeAuthTransmission thParams pKey (corrIdUsedAsNonce, fId, FileCmd (sFileParty @p) cmd) sendXFTPTransmission c t chunkSpec_ sendXFTPTransmission :: XFTPClient -> ByteString -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body) diff --git a/src/Simplex/FileTransfer/Protocol.hs b/src/Simplex/FileTransfer/Protocol.hs index 418e48482..c55b327f8 100644 --- a/src/Simplex/FileTransfer/Protocol.hs +++ b/src/Simplex/FileTransfer/Protocol.hs @@ -48,6 +48,7 @@ import Simplex.Messaging.Protocol SndPublicAuthKey, Transmission, TransmissionForAuth (..), + CorrId (..), encodeTransmission, encodeTransmissionForAuth, messageTagP, @@ -328,7 +329,7 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg) - xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) corrId tForAuth + xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) (C.cbNonce $ bs corrId) tForAuth xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString xftpEncodeTransmission thParams (corrId, fId, msg) = do diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index e0cfb2aca..2d9fedc65 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -788,7 +788,7 @@ compatibleContactUri (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = AgentConfig {smpClientVRange, smpAgentVRange} <- asks config pure $ (,) - <$> (qUri `compatibleVersion` smpClientVRange) + <$> (qUri `compatibleVersion` smpClientVRange) <*> (crAgentVRange `compatibleVersion` smpAgentVRange pqSup) versionPQSupport_ :: VersionSMPA -> Maybe CR.VersionE2E -> PQSupport diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 61ec96003..1c7196495 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -54,6 +54,9 @@ module Simplex.Messaging.Client suspendSMPQueue, deleteSMPQueue, deleteSMPQueues, + createSMPProxySession, + proxySMPMessage, + forwardSMPMessage, sendProtocolCommand, -- * Supporting types and client configuration @@ -69,6 +72,7 @@ module Simplex.Messaging.Client chooseTransportHost, proxyUsername, temporaryClientError, + smpProxyError, ServerTransmission, ClientCommand, @@ -98,9 +102,12 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) import Data.Time.Clock (UTCTime (..), getCurrentTime) +import qualified Data.X509 as X +import qualified Data.X509.Validation as XV import Network.Socket (ServiceName) import Numeric.Natural import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Protocol @@ -110,7 +117,7 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), runTransportClient) import Simplex.Messaging.Transport.KeepAlive import Simplex.Messaging.Transport.WebSockets (WS) -import Simplex.Messaging.Util (bshow, raceAny_, threadDelay') +import Simplex.Messaging.Util (bshow, liftEitherWith, raceAny_, threadDelay') import Simplex.Messaging.Version import System.Timeout (timeout) @@ -480,6 +487,19 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} +-- TODO keep error params +smpProxyError :: SMPClientError -> ErrorType +smpProxyError = \case + PCEProtocolError _ -> PROXY PROTOCOL + PCEResponseError _ -> PROXY RESPONSE + PCEUnexpectedResponse _ -> PROXY UNEXPECTED + PCEResponseTimeout -> PROXY TIMEOUT + PCENetworkError -> PROXY NETWORK + PCEIncompatibleHost -> PROXY BAD_HOST + PCETransportError _ -> PROXY TRANSPORT + PCECryptoError _ -> INTERNAL + PCEIOError _ -> INTERNAL + -- | Create a new SMP queue. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command @@ -630,6 +650,102 @@ deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO deleteSMPQueues = okSMPCommands DEL {-# INLINE deleteSMPQueues #-} +-- TODO picture + +-- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender +-- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg +createSMPProxySession :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO (SessionId, VersionSMP, C.PublicKeyX25519) +createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = + sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case + -- XXX: rfc says sessionId should be in the entityId of response + PKEY sId vr (chain, key) -> do + case supportedClientSMPRelayVRange `compatibleVersion` vr of + Nothing -> throwE PCEIncompatibleHost -- TODO different error + Just (Compatible v) -> liftEitherWith x509Error $ (sId,v,) <$> validateRelay chain key + r -> throwE . PCEUnexpectedResponse $ bshow r + where + x509Error :: String -> SMPClientError + x509Error _msg = PCEResponseError $ error "TODO: x509 error" -- TODO different error + validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 + validateRelay (X.CertificateChain cert) exact = do + serverKey <- case cert of + [leaf, ca] + | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> + C.x509ToPublic (X.certPubKey . X.signedObject $ X.getSigned leaf, []) >>= C.pubKey + _ -> throwError "bad certificate" + pubKey <- C.verifyX509 serverKey exact + C.x509ToPublic (pubKey, []) >>= C.pubKey + +-- consider how to process slow responses - is it handled somehow locally or delegated to the caller +-- this method is used in the client +-- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender +-- receives PRES :: EncResponse -> BrokerMsg -- proxy to client +proxySMPMessage :: + SMPClient -> + -- proxy session from PKEY + SessionId -> + VersionSMP -> + C.PublicKeyX25519 -> + -- message to deliver + Maybe SndPrivateAuthKey -> + SenderId -> + MsgFlags -> + MsgBody -> + ExceptT SMPClientError IO () +-- TODO use version +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} sessionId _v serverKey spKey sId flags msg = do + -- prepare params + let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams + serverThParams = proxyThParams {sessionId, thAuth = serverThAuth} + (cmdPubKey, cmdPrivKey) <- liftIO . atomically $ C.generateKeyPair @'C.X25519 g + let cmdSecret = C.dh' serverKey cmdPrivKey + nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g + -- encode + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg) + auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth + b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of + [] -> throwE $ PCETransportError TELargeMsg -- some other error. Internal? + TBError e _ : _ -> throwE $ PCETransportError e -- large message error? + TBTransmission s _ : _ -> pure s + TBTransmissions s _ _ : _ -> pure s + et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength + sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et)) >>= \case + -- TODO support PKEY + resend? + PRES (EncResponse er) -> do + t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er + case tParse proxyThParams t' of + t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of + (_auth, _signed, (_c, _e, r)) -> case r of -- TODO: verify + Left e -> throwE $ PCEResponseError e + Right OK -> pure () + Right (ERR e) -> throwE $ PCEProtocolError e + Right u -> throwE . PCEUnexpectedResponse $ bshow u -- possibly differentiate unexpected response from server/proxy + _ -> throwE $ PCETransportError TEBadBlock + r -> throwE . PCEUnexpectedResponse $ bshow r -- from proxy + +-- this method is used in the proxy +-- sends RFWD :: EncFwdTransmission -> Command Sender +-- receives RRES :: EncFwdResponse -> BrokerMsg +-- proxy should send PRES to the client with EncResponse +forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse +forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do + -- prepare params + sessSecret <- case thAuth thParams of + Nothing -> throwError $ PCEProtocolError INTERNAL -- different error - proxy didn't pass key? + Just THAuthClient {serverPeerPubKey, clientPrivKey} -> pure $ C.dh' serverPeerPubKey clientPrivKey + nonce <- liftIO . atomically $ C.randomCbNonce g + -- wrap + let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} + eft <- liftEitherWith PCECryptoError $ EncFwdTransmission <$> C.cbEncrypt sessSecret nonce (smpEncode fwdT) paddedForwardedMsgLength + -- send + sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case + RRES (EncFwdResponse efr) -> do + -- unwrap + r' <- liftEitherWith PCECryptoError $ C.cbDecrypt sessSecret (C.reverseNonce nonce) efr + FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' + pure fwdResponse + r -> throwE . PCEUnexpectedResponse $ bshow r + okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = sendSMPCommand c (Just pKey) qId cmd >>= \case @@ -693,8 +809,11 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do -- | Send Protocol command sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg -sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} pKey entId cmd = - ExceptT $ uncurry sendRecv =<< mkTransmission c (pKey, entId, cmd) +sendProtocolCommand c = sendProtocolCommand_ c Nothing + +sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg +sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ pKey entId cmd = + ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd) where -- two separate "atomically" needed to avoid blocking sendRecv :: Either TransportError SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg) @@ -713,33 +832,35 @@ getResponse :: ProtocolClient v err msg -> Request err msg -> IO (Response err m getResponse ProtocolClient {client_ = PClient {tcpTimeout, pingErrorCount}} Request {entityId, responseVar} = do response <- timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case + -- BTW: another registerDelay candidate. Also, crashes caller with BlockedIndef. Just r -> atomically (writeTVar pingErrorCount 0) $> r Nothing -> pure $ Left PCEResponseTimeout pure Response {entityId, response} -mkTransmission :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) -mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} (pKey_, entId, cmd) = do - corrId <- atomically getNextCorrId - let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, entId, cmd) - auth = authTransmission (thAuth thParams) pKey_ corrId tForAuth - r <- atomically $ mkRequest corrId +mkTransmission :: ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) +mkTransmission c = mkTransmission_ c Nothing + +mkTransmission_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> ClientCommand msg -> IO (PCTransmission err msg) +mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} nonce_ (pKey_, entId, cmd) = do + nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_ + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entId, cmd) + auth = authTransmission (thAuth thParams) pKey_ nonce tForAuth + r <- atomically $ mkRequest (CorrId corrId) pure ((,tToSend) <$> auth, r) where - getNextCorrId :: STM CorrId - getNextCorrId = CorrId <$> C.randomBytes 24 clientCorrId -- also used as nonce mkRequest :: CorrId -> STM (Request err msg) mkRequest corrId = do r <- Request entId <$> newEmptyTMVar TM.insert corrId r sentCommands pure r -authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth) -authTransmission thAuth pKey_ (CorrId corrId) t = traverse authenticate pKey_ +authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TransmissionAuth) +authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_ where authenticate :: C.APrivateAuthKey -> Either TransportError TransmissionAuth authenticate (C.APrivateAuthKey a pk) = case a of C.SX25519 -> case thAuth of - Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t + Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t Nothing -> Left TENoServerAuth C.SEd25519 -> sign pk C.SEd448 -> sign pk diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 4b925c6f6..50f12ac35 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -98,6 +98,7 @@ data SMPClientAgent = SMPClientAgent agentQ :: TBQueue SMPClientAgentEvent, randomDrg :: TVar ChaChaDRG, smpClients :: TMap SMPServer SMPClientVar, + smpSessions :: TMap SessionId SMPClient, srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), reconnections :: TVar [Async ()], @@ -135,6 +136,7 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg msgQ <- newTBQueue msgQSize agentQ <- newTBQueue agentQSize smpClients <- TM.empty + smpSessions <- TM.empty srvSubs <- TM.empty pendingSrvSubs <- TM.empty reconnections <- newTVar [] @@ -147,6 +149,7 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg agentQ, randomDrg, smpClients, + smpSessions, srvSubs, pendingSrvSubs, reconnections, @@ -155,7 +158,7 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg } getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient -getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, workerSeq} srv = +getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, workerSeq} srv = atomically getClientVar >>= either newSMPClient waitForSMPClient where getClientVar :: STM (Either SMPClientVar SMPClientVar) @@ -178,7 +181,9 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, wo tryE (connectClient v) >>= \r -> case r of Right smp -> do logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv - atomically $ putTMVar (sessionVar v) r + atomically $ do + putTMVar (sessionVar v) r + TM.insert (sessionId $ thParams smp) smp smpSessions successAction smp Left e -> do if e == PCENetworkError || e == PCEResponseTimeout @@ -200,13 +205,14 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, wo connectClient v = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) (clientDisconnected v) clientDisconnected :: SMPClientVar -> SMPClient -> IO () - clientDisconnected v _ = do - removeClientAndSubs v >>= (`forM_` serverDown) + clientDisconnected v smp = do + removeClientAndSubs v smp >>= (`forM_` serverDown) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv - removeClientAndSubs :: SMPClientVar -> IO (Maybe (Map SMPSub C.APrivateAuthKey)) - removeClientAndSubs v = atomically $ do + removeClientAndSubs :: SMPClientVar -> SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey)) + removeClientAndSubs v smp = atomically $ do removeSessVar v srv smpClients + TM.delete (sessionId $ thParams smp) smpSessions TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs where updateSubs sVar = do @@ -271,6 +277,9 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, wo notify :: SMPClientAgentEvent -> IO () notify evt = atomically $ writeTBQueue (agentQ ca) evt +lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe SMPClient) +lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions + closeSMPClientAgent :: SMPClientAgent -> IO () closeSMPClientAgent c = do closeSMPServerClients c diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index bffd7559f..7aefbd709 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -141,6 +141,7 @@ module Simplex.Messaging.Crypto sbEncrypt_, cbNonce, randomCbNonce, + reverseNonce, -- * NaCl crypto_secretbox SbKey (unSbKey), @@ -1292,6 +1293,9 @@ randomCbNonce = fmap CryptoBoxNonce . randomBytes 24 randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString randomBytes n gVar = stateTVar gVar $ randomBytesGenerate n +reverseNonce :: CbNonce -> CbNonce +reverseNonce (CryptoBoxNonce s) = CryptoBoxNonce (B.reverse s) + instance Encoding CbNonce where smpEncode = unCbNonce smpP = CryptoBoxNonce <$> A.take 24 diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index b5a3e7770..459f9a0df 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -43,6 +43,8 @@ module Simplex.Messaging.Protocol ( -- * SMP protocol parameters supportedSMPClientVRange, maxMessageLength, + paddedProxiedMsgLength, + paddedForwardedMsgLength, e2eEncConfirmationLength, e2eEncMessageLength, @@ -56,6 +58,7 @@ module Simplex.Messaging.Protocol SubscriptionMode (..), Party (..), Cmd (..), + DirectParty, BrokerMsg (..), SParty (..), PartyI (..), @@ -63,6 +66,7 @@ module Simplex.Messaging.Protocol ProtocolErrorType (..), ErrorType (..), CommandError (..), + ProxyError (..), Transmission, TransmissionAuth (..), SignedTransmission, @@ -121,6 +125,12 @@ module Simplex.Messaging.Protocol EncNMsgMeta, SMPMsgMeta (..), NMsgMeta (..), + EncFwdResponse (..), + EncFwdTransmission (..), + EncResponse (..), + EncTransmission (..), + FwdResponse (..), + FwdTransmission (..), MsgFlags (..), initialSMPClientVersion, userProtocol, @@ -191,6 +201,7 @@ import Data.Word (Word16) import qualified Data.X509 as X import GHC.TypeLits (ErrorMessage (..), TypeError, type (+)) import qualified GHC.TypeLits as TE +import qualified GHC.TypeLits as Type import Network.Socket (ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -233,6 +244,20 @@ supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClie maxMessageLength :: Int maxMessageLength = 16088 +-- without signature works with min 16151 (fails with 16150) +-- with Ed448: 16265 (fails with 16264) +-- with Ed25519: 16215 (fails with 16214) +-- with X25519: 16232 (fails with 16231) +paddedProxiedMsgLength :: Int +paddedProxiedMsgLength = 16232 + +-- without signature works with min 16239 (fails with 16238) +-- with Ed448: 16353 (fails with 16352) +-- with Ed25519: 16303 (fails with 16302) +-- with X25519: 16320 (fails with 16319) +paddedForwardedMsgLength :: Int +paddedForwardedMsgLength = 16320 + type MaxMessageLen = 16088 -- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used) @@ -246,7 +271,7 @@ e2eEncMessageLength :: Int e2eEncMessageLength = 16032 -- | SMP protocol clients -data Party = Recipient | Sender | Notifier +data Party = Recipient | Sender | Notifier | ProxiedClient deriving (Show) -- | Singleton types for SMP protocol clients @@ -254,11 +279,13 @@ data SParty :: Party -> Type where SRecipient :: SParty Recipient SSender :: SParty Sender SNotifier :: SParty Notifier + SProxiedClient :: SParty ProxiedClient instance TestEquality SParty where testEquality SRecipient SRecipient = Just Refl testEquality SSender SSender = Just Refl testEquality SNotifier SNotifier = Just Refl + testEquality SProxiedClient SProxiedClient = Just Refl testEquality _ _ = Nothing deriving instance Show (SParty p) @@ -271,6 +298,15 @@ instance PartyI Sender where sParty = SSender instance PartyI Notifier where sParty = SNotifier +instance PartyI ProxiedClient where sParty = SProxiedClient + +type family DirectParty (p :: Party) :: Constraint where + DirectParty Recipient = () + DirectParty Sender = () + DirectParty Notifier = () + DirectParty p = + (Int ~ Bool, TypeError (Type.Text "Party " :<>: ShowType p :<>: Type.Text " is not direct")) + -- | Type for client command of any participant. data Cmd = forall p. PartyI p => Cmd (SParty p) (Command p) @@ -361,13 +397,13 @@ data Command (p :: Party) where PING :: Command Sender -- SMP notification subscriber commands NSUB :: Command Notifier - PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- request a relay server connection by URI + PRXY :: SMPServer -> Maybe BasicAuth -> Command ProxiedClient -- request a relay server connection by URI -- Transmission to proxy: -- - entity ID: ID of the session with relay returned in PKEY (response to PRXY) -- - corrId: also used as a nonce to encrypt transmission to relay, corrId + 1 - from relay -- - key (1st param in the command) is used to agree DH secret for this particular transmission and its response -- Encrypted transmission should include session ID (tlsunique) from proxy-relay connection. - PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender -- use CorrId as CbNonce, client to proxy + PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command ProxiedClient -- use CorrId as CbNonce, client to proxy -- Transmission forwarded to relay: -- - entity ID: empty -- - corrId: unique correlation ID between proxy and relay, also used as a nonce to encrypt forwarded transmission @@ -401,11 +437,18 @@ newtype EncTransmission = EncTransmission ByteString deriving (Show) data FwdTransmission = FwdTransmission - { fwdCorrId :: ByteString, + { fwdCorrId :: CorrId, fwdKey :: C.PublicKeyX25519, - fwdTransmission :: ByteString + fwdTransmission :: EncTransmission } +instance Encoding FwdTransmission where + smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} = + smpEncode (corrId, fwdKey, Tail t) + smpP = do + (corrId, fwdKey, Tail t) <- smpP + pure FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} + newtype EncFwdTransmission = EncFwdTransmission ByteString deriving (Show) @@ -419,7 +462,7 @@ data BrokerMsg where NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg -- Should include certificate chain - PKEY :: X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key + PKEY :: SessionId -> VersionRangeSMP -> (X.CertificateChain, X.SignedExact X.PubKey) -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy PRES :: EncResponse -> BrokerMsg -- proxy to client END :: BrokerMsg @@ -438,10 +481,17 @@ newtype EncFwdResponse = EncFwdResponse ByteString deriving (Eq, Show) data FwdResponse = FwdResponse - { fwdCorrId :: ByteString, - fwdResponse :: ByteString + { fwdCorrId :: CorrId, + fwdResponse :: EncResponse } +instance Encoding FwdResponse where + smpEncode FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} = + smpEncode (corrId, Tail t) + smpP = do + (corrId, Tail t) <- smpP + pure FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} + newtype EncResponse = EncResponse ByteString deriving (Eq, Show) @@ -607,8 +657,8 @@ data CommandTag (p :: Party) where DEL_ :: CommandTag Recipient SEND_ :: CommandTag Sender PING_ :: CommandTag Sender - PRXY_ :: CommandTag Sender - PFWD_ :: CommandTag Sender + PRXY_ :: CommandTag ProxiedClient + PFWD_ :: CommandTag ProxiedClient RFWD_ :: CommandTag Sender NSUB_ :: CommandTag Notifier @@ -672,8 +722,8 @@ instance ProtocolMsgTag CmdTag where "DEL" -> Just $ CT SRecipient DEL_ "SEND" -> Just $ CT SSender SEND_ "PING" -> Just $ CT SSender PING_ - "PRXY" -> Just $ CT SSender PRXY_ - "PFWD" -> Just $ CT SSender PFWD_ + "PRXY" -> Just $ CT SProxiedClient PRXY_ + "PFWD" -> Just $ CT SProxiedClient PFWD_ "RFWD" -> Just $ CT SSender RFWD_ "NSUB" -> Just $ CT SNotifier NSUB_ _ -> Nothing @@ -1096,6 +1146,8 @@ data ErrorType SESSION | -- | SMP command is unknown or has invalid syntax CMD {cmdErr :: CommandError} + | -- | error from proxied relay + PROXY {proxyErr :: ProxyError} | -- | command authorization error - bad signature or non-existing SMP queue AUTH | -- | SMP queue capacity is exceeded on the server @@ -1115,8 +1167,12 @@ data ErrorType instance StrEncoding ErrorType where strEncode = \case CMD e -> "CMD " <> bshow e + PROXY e -> "PROXY " <> bshow e e -> bshow e - strP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1 + strP = + "CMD " *> (CMD <$> parseRead1) + <|> "PROXY " *> (PROXY <$> parseRead1) + <|> parseRead1 -- | SMP command error type. data CommandError @@ -1134,6 +1190,22 @@ data CommandError NO_ENTITY deriving (Eq, Read, Show) +-- TODO keep error params +data ProxyError + = -- | Correctly parsed SMP server ERR response. + -- This error is forwarded to the agent client as `ERR SMP err`. + PROTOCOL -- {protocolErr :: String} + | -- | Invalid server response that failed to parse. + -- Forwarded to the agent client as `ERR BROKER RESPONSE`. + RESPONSE -- {responseErr :: String} + | UNEXPECTED + | TIMEOUT + | NETWORK + | BAD_HOST + | NO_SESSION + | TRANSPORT -- {transportErr :: TransportError} + deriving (Eq, Read, Show) + -- | SMP transmission parser. transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do @@ -1195,9 +1267,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg) PING -> e PING_ NSUB -> e NSUB_ - PRXY host auth_ -> e (PRXY_, ' ', strEncode host, ' ', auth_) - PFWD {} -> error "TODO: e (PFWD_,,)" - RFWD {} -> error "TODO: e (RFWD_,,)" + PRXY host auth_ -> e (PRXY_, ' ', host, auth_) + PFWD pubKey (EncTransmission s) -> e (PFWD_, ' ', pubKey, Tail s) + RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s) where e :: Encoding a => a -> ByteString e = smpEncode @@ -1207,27 +1279,33 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg {-# INLINE fromProtocolError #-} - checkCredentials (auth, _, queueId, _) cmd = case cmd of + checkCredentials (auth, _, entId, _) cmd = case cmd of -- NEW must have signature but NOT queue ID NEW {} | isNothing auth -> Left $ CMD NO_AUTH - | not (B.null queueId) -> Left $ CMD HAS_AUTH + | not (B.null entId) -> Left $ CMD HAS_AUTH | otherwise -> Right cmd -- SEND must have queue ID, signature is not always required SEND {} - | B.null queueId -> Left $ CMD NO_ENTITY + | B.null entId -> Left $ CMD NO_ENTITY | otherwise -> Right cmd - -- PING must not have queue ID or signature - PING - | isNothing auth && B.null queueId -> Right cmd - | otherwise -> Left $ CMD HAS_AUTH - PRXY {} - | isNothing auth && B.null queueId -> Right cmd + PING -> noAuthCmd + PRXY {} -> noAuthCmd + PFWD {} + | B.null entId -> Left $ CMD NO_ENTITY + | isNothing auth -> Right cmd | otherwise -> Left $ CMD HAS_AUTH + RFWD _ -> noAuthCmd -- other client commands must have both signature and queue ID _ - | isNothing auth || B.null queueId -> Left $ CMD NO_AUTH + | isNothing auth || B.null entId -> Left $ CMD NO_AUTH | otherwise -> Right cmd + where + -- command must not have entity ID (queue or session ID) or signature + noAuthCmd :: Either ErrorType (Command p) + noAuthCmd + | isNothing auth && B.null entId = Right cmd + | otherwise = Left $ CMD HAS_AUTH instance ProtocolEncoding SMPVersion ErrorType Cmd where type Tag Cmd = CmdTag @@ -1255,9 +1333,11 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where Cmd SSender <$> case tag of SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP) PING_ -> pure PING - PFWD_ -> error "TODO: PFWD_" - RFWD_ -> error "TODO: RFWD_" - PRXY_ -> PRXY <$> (_smpP >>= either fail pure . strDecode) <*> _smpP + RFWD_ -> RFWD <$> (EncFwdTransmission . unTail <$> _smpP) + CT SProxiedClient tag -> + Cmd SProxiedClient <$> case tag of + PFWD_ -> PFWD <$> _smpP <*> (EncTransmission . unTail <$> smpP) + PRXY_ -> PRXY <$> _smpP <*> smpP CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg @@ -1273,7 +1353,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where e (MSG_, ' ', msgId, Tail body) NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh) NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta) - PKEY cert key -> e (PKEY_, ' ', C.encodeCertChain cert, C.SignedObject key) + PKEY sid vr (cert, key) -> e (PKEY_, ' ', sid, vr, C.encodeCertChain cert, C.SignedObject key) RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock) PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock) END -> e END_ @@ -1293,7 +1373,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP) NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP - PKEY_ -> PKEY <$> (A.space *> C.certChainP) <*> (C.getSignedExact <$> smpP) + PKEY_ -> PKEY <$> _smpP <*> smpP <*> ((,) <$> C.certChainP <*> (C.getSignedExact <$> smpP)) RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP) PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP) END_ -> pure END @@ -1308,19 +1388,24 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where PEBlock -> BLOCK {-# INLINE fromProtocolError #-} - checkCredentials (_, _, queueId, _) cmd = case cmd of + checkCredentials (_, _, entId, _) cmd = case cmd of -- IDS response should not have queue ID IDS _ -> Right cmd -- ERR response does not always have queue ID ERR _ -> Right cmd -- PONG response must not have queue ID - PONG - | B.null queueId -> Right cmd - | otherwise -> Left $ CMD HAS_AUTH + PONG -> noEntityMsg + PKEY {} -> noEntityMsg + RRES _ -> noEntityMsg -- other broker responses must have queue ID _ - | B.null queueId -> Left $ CMD NO_ENTITY + | B.null entId -> Left $ CMD NO_ENTITY | otherwise -> Right cmd + where + noEntityMsg :: Either ErrorType BrokerMsg + noEntityMsg + | B.null entId = Right cmd + | otherwise = Left $ CMD HAS_AUTH -- | Parse SMP protocol commands and broker messages parseProtocol :: forall v err msg. ProtocolEncoding v err msg => Version v -> ByteString -> Either err msg @@ -1343,6 +1428,7 @@ instance Encoding ErrorType where BLOCK -> "BLOCK" SESSION -> "SESSION" CMD err -> "CMD " <> smpEncode err + PROXY err -> "PROXY " <> smpEncode err AUTH -> "AUTH" QUOTA -> "QUOTA" EXPIRED -> "EXPIRED" @@ -1356,6 +1442,7 @@ instance Encoding ErrorType where "BLOCK" -> pure BLOCK "SESSION" -> pure SESSION "CMD" -> CMD <$> _smpP + "PROXY" -> PROXY <$> _smpP "AUTH" -> pure AUTH "QUOTA" -> pure QUOTA "EXPIRED" -> pure EXPIRED @@ -1381,7 +1468,29 @@ instance Encoding CommandError where "NO_AUTH" -> pure NO_AUTH "HAS_AUTH" -> pure HAS_AUTH "NO_ENTITY" -> pure NO_ENTITY - "NO_QUEUE" -> pure NO_ENTITY + "NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility + _ -> fail "bad command error type" + +instance Encoding ProxyError where + smpEncode e = case e of + PROTOCOL -> "PROTOCOL" + RESPONSE -> "RESPONSE" + UNEXPECTED -> "UNEXPECTED" + TIMEOUT -> "TIMEOUT" + NETWORK -> "NETWORK" + BAD_HOST -> "BAD_HOST" + NO_SESSION -> "NO_SESSION" + TRANSPORT -> "TRANSPORT" + smpP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> pure PROTOCOL + "RESPONSE" -> pure RESPONSE + "UNEXPECTED" -> pure UNEXPECTED + "TIMEOUT" -> pure TIMEOUT + "NETWORK" -> pure NETWORK + "BAD_HOST" -> pure BAD_HOST + "NO_SESSION" -> pure NO_SESSION + "TRANSPORT" -> pure TRANSPORT _ -> fail "bad command error type" -- | Send signed SMP transmission to TCP transport. @@ -1521,4 +1630,6 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) +$(J.deriveJSON (sumTypeJSON id) ''ProxyError) + $(J.deriveJSON (sumTypeJSON id) ''ErrorType) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 2ff0d5e51..0991cd2a8 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -13,7 +13,6 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeApplications #-} -- | -- Module : Simplex.Messaging.Server @@ -43,6 +42,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random import Data.Bifunctor (first) import Data.ByteString.Base64 (encode) @@ -54,6 +54,7 @@ import Data.Functor (($>)) import Data.Int (Int64) import qualified Data.IntMap.Strict as IM import Data.List (intercalate) +import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (isNothing) @@ -67,8 +68,10 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Client (ProtocolClient (thParams), forwardSMPMessage, smpProxyError) +import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), getSMPServerClient', lookupSMPServerClient) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Encoding (Encoding (smpEncode)) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol import Simplex.Messaging.Server.Control @@ -90,6 +93,7 @@ import System.Exit (exitFailure) import System.IO (hPrint, hPutStrLn, hSetNewlineMode, universalNewlineMode) import System.Mem.Weak (deRefWeak) import UnliftIO (timeout) +import UnliftIO.Async (mapConcurrently) import UnliftIO.Concurrent import UnliftIO.Directory (doesFileExist, renameFile) import UnliftIO.Exception @@ -122,11 +126,13 @@ type M a = ReaderT Env IO a smpServer :: TMVar Bool -> ServerConfig -> M () smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do s <- asks server + pa <- asks proxyAgent expired <- restoreServerMessages restoreServerStats expired raceAny_ ( serverThread s "server subscribedQ" subscribedQ subscribers subscriptions cancelSub : serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers ntfSubscriptions (\_ -> pure ()) + : receiveFromProxyAgent pa : map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg ) `finally` withLock' (savingLock s) "final" (saveServer False) @@ -179,6 +185,19 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do mkWeakThreadId t >>= atomically . modifyTVar' (endThreads c) . IM.insert tId atomically $ TM.lookupDelete qId (clientSubs c) + receiveFromProxyAgent :: ProxyAgent -> M () + receiveFromProxyAgent ProxyAgent {smpAgent = SMPClientAgent {agentQ}} = + forever $ + atomically (readTBQueue agentQ) >>= \case + CAConnected srv -> logInfo $ "SMP server connected " <> showServer' srv + CADisconnected srv [] -> logInfo $ "SMP server disconnected " <> showServer' srv + CADisconnected srv subs -> logError $ "SMP server disconnected " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) + CAReconnected srv -> logInfo $ "SMP server reconnected " <> showServer' srv + CAResubscribed srv subs -> logError $ "SMP server resubscribed " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) + CASubError srv errs -> logError $ "SMP server subscription errors " <> showServer' srv <> " / errors: " <> tshow (length errs) + where + showServer' = decodeLatin1 . strEncode . host + expireMessagesThread_ :: ServerConfig -> [M ()] expireMessagesThread_ ServerConfig {messageExpiration = Just msgExp} = [expireMessages msgExp] expireMessagesThread_ _ = [] @@ -314,7 +333,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do CPResume -> withAdminRole $ hPutStrLn h "resume not implemented" CPClients -> withAdminRole $ do active <- unliftIO u (asks clients) >>= readTVarIO - hPutStrLn h $ "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions" + hPutStrLn h "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions" forM_ (IM.toList active) $ \(cid, Client {sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, subscriptions}) -> do connected' <- bshow <$> readTVarIO connected rcvActiveAt' <- strEncode <$> readTVarIO rcvActiveAt @@ -410,7 +429,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do hPutStrLn h "AUTH" runClientTransport :: Transport c => THandleSMP c 'TServer -> M () -runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = do +runClientTransport th@THandle {params = thParams@THandleParams {thVersion, sessionId}} = do q <- asks $ tbqSize . config ts <- liftIO getSystemTime active <- asks clients @@ -422,7 +441,7 @@ runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = s <- asks server expCfg <- asks $ inactiveClientExpiration . config labelMyThread . B.unpack $ "client $" <> encode sessionId - raceAny_ ([liftIO $ send th c, client c s, receive th c] <> disconnectThread_ c expCfg) + raceAny_ ([liftIO $ send th c, client thParams c s, receive th c] <> disconnectThread_ c expCfg) `finally` clientDisconnected c where disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport th (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)] @@ -463,19 +482,19 @@ receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActi forever $ do ts <- L.toList <$> liftIO (tGet th) atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime - as <- partitionEithers <$> mapM cmdAction ts - write sndQ $ fst as - write rcvQ $ snd as + (errs, cmds) <- partitionEithers <$> mapM cmdAction ts + write sndQ errs + write rcvQ cmds where cmdAction :: SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) - cmdAction (tAuth, authorized, (corrId, queueId, cmdOrError)) = + cmdAction (tAuth, authorized, (corrId, entId, cmdOrError)) = case cmdOrError of - Left e -> pure $ Left (corrId, queueId, ERR e) - Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized queueId cmd + Left e -> pure $ Left (corrId, entId, ERR e) + Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized entId cmd where verified = \case - VRVerified qr -> Right (qr, (corrId, queueId, cmd)) - VRFailed -> Left (corrId, queueId, ERR AUTH) + VRVerified qr -> Right (qr, (corrId, entId, cmd)) + VRFailed -> Left (corrId, entId, ERR AUTH) write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty send :: Transport c => THandleSMP c 'TServer -> Client -> IO () @@ -522,19 +541,18 @@ verifyTransmission auth_ tAuth authorized queueId cmd = -- SEND will be accepted without authorization before the queue is secured with KEY command Cmd SSender SEND {} -> verifyQueue (\q -> Just q `verified` maybe (isNothing tAuth) verify (senderKey q)) <$> get SSender Cmd SSender PING -> pure $ VRVerified Nothing - -- NSUB will not be accepted without authorization - Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (Just q `verifiedWith`) (notifierKey <$> notifier q)) <$> get SNotifier - Cmd SSender PRXY {} -> pure $ VRVerified Nothing - Cmd SSender PFWD {} -> pure $ VRVerified Nothing Cmd SSender RFWD {} -> pure $ VRVerified Nothing + -- NSUB will not be accepted without authorization + Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (\n -> Just q `verifiedWith` notifierKey n) (notifier q)) <$> get SNotifier + Cmd SProxiedClient _ -> pure $ VRVerified Nothing where verify = verifyCmdAuthorization auth_ tAuth authorized dummyVerify = verify (dummyAuthKey tAuth) `seq` VRFailed verifyQueue :: (QueueRec -> VerificationResult) -> Either ErrorType QueueRec -> VerificationResult - verifyQueue = either (\_ -> dummyVerify) + verifyQueue = either (const dummyVerify) verified q cond = if cond then VRVerified q else VRFailed verifiedWith q k = q `verified` verify k - get :: SParty p -> M (Either ErrorType QueueRec) + get :: DirectParty p => SParty p -> M (Either ErrorType QueueRec) get party = do st <- asks queueStore atomically $ getQueue st party queueId @@ -584,36 +602,55 @@ dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/Xo dummyKeyX25519 :: C.PublicKey 'C.X25519 dummyKeyX25519 = "MCowBQYDK2VuAyEA4JGSMYht18H4mas/jHeBwfcM7jLwNYJNOAhi2/g4RXg=" -client :: Client -> Server -> M () -client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do +client :: THandleParams SMPVersion 'TServer -> Client -> Server -> M () +client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands" - forever $ - atomically (readTBQueue rcvQ) - >>= mapM processCommand - >>= atomically . writeTBQueue sndQ + forever $ do + (proxied, rs) <- partitionEithers . L.toList <$> (mapM processCommand =<< atomically (readTBQueue rcvQ)) + forM_ (L.nonEmpty rs) reply + -- TODO cancel this thread if the client gets disconnected + -- TODO limit client concurrency + forM_ (L.nonEmpty proxied) $ \cmds -> forkIO $ mapConcurrently processProxiedCmd cmds >>= reply where - processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Transmission BrokerMsg) + reply :: MonadIO m => NonEmpty (Transmission BrokerMsg) -> m () + reply = atomically . writeTBQueue sndQ + processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg) + processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of + PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR AUTH) + where + allowProxy = do + ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config + pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth + getRelay = do + ProxyAgent {smpAgent} <- asks proxyAgent + -- TODO catch IO errors too + liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) + where + proxyResp = \case + Right smp -> + let THandleParams {sessionId = srvSessId, thAuth} = thParams smp + vr = supportedServerSMPRelayVRange + in case thAuth of + Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey + Nothing -> ERR $ PROXY TRANSPORT -- TODO different error? + Left err -> ERR $ smpProxyError err + PFWD pubKey encBlock -> do + ProxyAgent {smpAgent} <- asks proxyAgent + atomically (lookupSMPServerClient smpAgent sessId) >>= \case + Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) + Nothing -> pure $ ERR $ PROXY NO_SESSION + processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do st <- asks queueStore case cmd of - Cmd SSender command -> - case command of - SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody - PING -> pure (corrId, "", PONG) - PRXY relay auth -> - ifM - allowProxy - (setupProxy relay) - (pure (corrId, queueId, ERR AUTH)) - where - allowProxy = do - ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config - pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth - PFWD _dhPub _encBlock -> error "TODO: processCommand.PFWD" - RFWD _encBlock -> error "TODO: processCommand.RFWD" - Cmd SNotifier NSUB -> subscribeNotifications + Cmd SProxiedClient command -> pure $ Left (corrId, queueId, command) + Cmd SSender command -> Right <$> case command of + SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody + PING -> pure (corrId, "", PONG) + RFWD encBlock -> (corrId, "",) <$> processForwardedCommand encBlock + Cmd SNotifier NSUB -> Right <$> subscribeNotifications Cmd SRecipient command -> - case command of + Right <$> case command of NEW rKey dhKey auth subMode -> ifM allowNew @@ -877,6 +914,59 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv encNMsgMeta = C.cbEncrypt rcvNtfDhSecret cbNonce (smpEncode msgMeta) 128 pure . (cbNonce,) $ fromRight "" encNMsgMeta + processForwardedCommand :: EncFwdTransmission -> M BrokerMsg + processForwardedCommand (EncFwdTransmission s) = fmap (either id id) . runExceptT $ do + -- TODO error + THAuthServer {clientPeerPubKey, serverPrivKey} <- maybe (throwError $ ERR INTERNAL) pure thAuth + -- TODO compute during handshake? + let sessSecret = C.dh' clientPeerPubKey serverPrivKey + proxyNonce = C.cbNonce $ bs corrId + -- TODO error + s' <- liftEitherWith internalErr $ C.cbDecrypt sessSecret proxyNonce s + -- TODO error + FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s' + -- TODO error - this error is reported to proxy, as we failed to get to client's transmission + let clientSecret = C.dh' fwdKey serverPrivKey + clientNonce = C.cbNonce $ bs fwdCorrId + b <- liftEitherWith internalErr $ C.cbDecrypt clientSecret clientNonce et + -- only allowing single forwarded transactions + let t' = tDecodeParseValidate thParams' $ L.head $ tParse thParams' b + clntThAuth = Just $ THAuthServer {clientPeerPubKey = fwdKey, serverPrivKey} + -- TODO error + r <- + lift (rejectOrVerify clntThAuth t') >>= \case + Left r -> pure r + Right t''@(_, (corrId', entId', _)) -> + -- Left will not be returned by processCommand, as only SEND command is allowed + fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') + + -- encode response + r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of + [] -> throwE $ ERR INTERNAL -- TODO error + TBError _ _ : _ -> throwE $ ERR INTERNAL -- TODO error + TBTransmission b' _ : _ -> pure b' + TBTransmissions b' _ _ : _ -> pure b' + -- encrypt to client + r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength + -- encrypt to proxy + let fr = FwdResponse {fwdCorrId, fwdResponse = r2} + r3 <- liftEitherWith internalErr $ EncFwdResponse <$> C.cbEncrypt sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) paddedForwardedMsgLength + pure $ RRES r3 + where + internalErr _ = ERR INTERNAL -- TODO errors + THandleParams {thAuth} = thParams' + rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) + rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = + case cmdOrError of + Left e -> pure $ Left (corrId', entId', ERR e) + -- flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody + Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' + where + verified = \case + VRVerified qr -> Right (qr, (corrId', entId', cmd')) + VRFailed -> Left (corrId', entId', ERR AUTH) + Right _ -> pure $ Left (corrId', entId', ERR $ CMD PROHIBITED) + deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> M (Transmission BrokerMsg) deliverMessage name qr rId sub q msg_ = time (name <> " deliver") $ do readTVarIO sub >>= \case @@ -936,15 +1026,6 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv Right q -> updateDeletedStats q $> ok Left e -> pure $ err e - setupProxy :: SMPServer -> M (Transmission BrokerMsg) - setupProxy todo'relay = undefined - -- do - -- let relaySessionId = "TODO: relaySessionId" - -- (dummyRelayDhPublic, _) <- atomically . C.generateKeyPair =<< asks random - -- (_, dummySignKey) <- atomically . C.generateKeyPair =<< asks random - -- let dummyRelayKeySignature = C.sign' dummySignKey $ smpEncode dummyRelayDhPublic - -- pure (corrId, relaySessionId, PKEY dummyRelayDhPublic dummyRelayKeySignature) - ok :: Transmission BrokerMsg ok = (corrId, queueId, OK) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 74d7d96e3..11ea4fd07 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -15,15 +15,14 @@ import qualified Data.IntMap.Strict as IM import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Text (Text) import Data.Time.Clock (getCurrentTime) import Data.Time.Clock.System (SystemTime) import Data.X509.Validation (Fingerprint (..)) import Network.Socket (ServiceName) import qualified Network.TLS as T import Numeric.Natural -import Simplex.Messaging.Agent.Env.SQLite (Worker) import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Client.Agent (SMPClientAgent, SMPClientAgentConfig, newSMPClientAgent) import Simplex.Messaging.Crypto (KeyHash (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol @@ -35,7 +34,7 @@ import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, SessionId, VersionSMP, VersionRangeSMP) +import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP) import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) @@ -82,6 +81,7 @@ data ServerConfig = ServerConfig transportConfig :: TransportServerConfig, -- | run listener on control port controlPort :: Maybe ServiceName, + smpAgentCfg :: SMPClientAgentConfig, allowSMPProxy :: Bool -- auth is the same with `newQueueBasicAuth` } @@ -115,7 +115,7 @@ data Env = Env sockets :: SocketState, clientSeq :: TVar ClientId, clients :: TVar (IntMap Client), - proxyServer :: SMPProxyServer -- senders served on this proxy + proxyAgent :: ProxyAgent -- senders served on this proxy } data Server = Server @@ -126,16 +126,8 @@ data Server = Server savingLock :: Lock } -data SMPProxyServer = SMPProxyServer - { relaySessions :: TMap SessionId SMPProxiedRelay, - relayServers :: TMap Text SessionId -- speed up client lookups by server URI - } - -data SMPProxiedRelay = SMPProxiedRelay - { worker :: Worker, - proxyKey :: C.DhSecretX25519, - fwdQ :: TBQueue (ClientId, CorrId, C.PublicKeyX25519, ByteString) -- FWD args from multiple clients using this server - -- can be used for QUOTA retries until the session is gone +data ProxyAgent = ProxyAgent + { smpAgent :: SMPClientAgent } type ClientId = Int @@ -194,7 +186,7 @@ newSubscription subThread = do return Sub {subThread, delivered} newEnv :: ServerConfig -> IO Env -newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile} = do +newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg} = do server <- atomically newServer queueStore <- atomically newQueueStore msgStore <- atomically newMsgStore @@ -207,8 +199,8 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, sockets <- atomically newSocketState clientSeq <- newTVarIO 0 clients <- newTVarIO mempty - proxyServer <- newSMPProxyServer - return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyServer} + proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random + return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent} where restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode) restoreQueues QueueStore {queues, senders, notifiers} f = do @@ -225,8 +217,7 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, Nothing -> id Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q) -newSMPProxyServer :: MonadIO m => m SMPProxyServer -newSMPProxyServer = do - relayServers <- atomically TM.empty - relaySessions <- atomically TM.empty - pure SMPProxyServer {relayServers, relaySessions} +newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent +newSMPProxyAgent smpAgentCfg random = do + smpAgent <- newSMPClientAgent smpAgentCfg random + pure ProxyAgent {smpAgent} diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index d14bdac1f..67064f1c7 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -18,6 +18,8 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Network.Socket (HostName) import Options.Applicative +import Simplex.Messaging.Client (ProtocolClientConfig (..)) +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer) @@ -25,10 +27,11 @@ import Simplex.Messaging.Server (runSMPServer) import Simplex.Messaging.Server.CLI import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration) import Simplex.Messaging.Server.Expiration -import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange) +import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange, batchCmdsSMPVersion, sendingProxySMPVersion) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Version (mkVersionRange) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) @@ -214,6 +217,7 @@ smpServerCLI cfgPath logPath = { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini }, controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini, + smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion}}, allowSMPProxy = True -- TODO: "get from INI" } @@ -306,4 +310,3 @@ cliCommandP cfgPath logPath iniFile = pure InitOptions {enableStoreLog, logStats, signAlgorithm, ip, fqdn, password, scripted} parseBasicAuth :: ReadM ServerPassword parseBasicAuth = eitherReader $ fmap ServerPassword . strDecode . B.pack - diff --git a/src/Simplex/Messaging/Server/QueueStore/STM.hs b/src/Simplex/Messaging/Server/QueueStore/STM.hs index b76ad4998..8de7a38c6 100644 --- a/src/Simplex/Messaging/Server/QueueStore/STM.hs +++ b/src/Simplex/Messaging/Server/QueueStore/STM.hs @@ -54,7 +54,7 @@ addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = where hasId = (||) <$> TM.member rId queues <*> TM.member sId senders -getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec) +getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec) getQueue QueueStore {queues, senders, notifiers} party qId = toResult <$> (mapM readTVar =<< getVar) where diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 565199c36..33aae5c60 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -115,6 +115,11 @@ import UnliftIO.STM -- * Transport parameters +-- min size it works with: +-- unsigned message: 16292 (paddedProxiedMsgLength = 16151, paddedForwardedMsgLength = 16239) +-- Ed448: 16406 (16384 + 22, fails with 21) +-- Ed25519: 16356 +-- X25519: 16381 smpBlockSize :: Int smpBlockSize = 16384 @@ -358,6 +363,7 @@ data ServerHandshake = ServerHandshake { smpVersionRange :: VersionRangeSMP, sessionId :: SessionId, -- pub key to agree shared secrets for command authorization and entity ID encryption. + -- todo C.PublicKeyX25519 authPubKey :: Maybe (X.CertificateChain, X.SignedExact X.PubKey) } diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index c349096dc..3ca78a8c4 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -281,12 +281,12 @@ randomSUB_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionSM randomSUB_ a v sessId = do g <- C.newRandom rId <- atomically $ C.randomBytes 24 g - corrId <- atomically $ CorrId <$> C.randomBytes 24 g + nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g (rKey, rpKey) <- atomically $ C.generateAuthKeyPair a g thAuth_ <- testTHandleAuth v g rKey let thParams = testTHandleParams v sessId - TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, rId, Cmd SRecipient SUB) - pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) corrId tForAuth + TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, rId, Cmd SRecipient SUB) + pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) nonce tForAuth randomSUBCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> IO (PCTransmission ErrorType BrokerMsg) randomSUBCmd = randomSUBCmd_ C.SEd25519 @@ -311,13 +311,13 @@ randomSEND_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionS randomSEND_ a v sessId len = do g <- C.newRandom sId <- atomically $ C.randomBytes 24 g - corrId <- atomically $ CorrId <$> C.randomBytes 3 g + nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g (sKey, spKey) <- atomically $ C.generateAuthKeyPair a g thAuth_ <- testTHandleAuth v g sKey msg <- atomically $ C.randomBytes len g let thParams = testTHandleParams v sessId - TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, sId, Cmd SSender $ SEND noMsgFlags msg) - pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) corrId tForAuth + TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, sId, Cmd SSender $ SEND noMsgFlags msg) + pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) nonce tForAuth testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient testTHandleParams v sessionId = diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 7b1a7b813..a486e6549 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -2,7 +2,6 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} module CoreTests.ProtocolErrorTests where @@ -14,9 +13,10 @@ import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.FileTransfer.Transport (XFTPErrorType (..)) import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Agent.Protocol as Agent import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..)) +import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..)) import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) import Test.Hspec @@ -33,7 +33,7 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do || strDecode (strEncode err) == Right err where errHasSpaces = \case - BROKER srv (RESPONSE e) -> hasSpaces srv || hasSpaces e + BROKER srv (Agent.RESPONSE e) -> hasSpaces srv || hasSpaces e BROKER srv _ -> hasSpaces srv _ -> False hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) @@ -54,6 +54,8 @@ deriving instance Generic ErrorType deriving instance Generic CommandError +deriving instance Generic ProxyError + deriving instance Generic TransportError deriving instance Generic HandshakeError @@ -78,6 +80,8 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU instance Arbitrary CommandError where arbitrary = genericArbitraryU +instance Arbitrary ProxyError where arbitrary = genericArbitraryU + instance Arbitrary TransportError where arbitrary = genericArbitraryU instance Arbitrary HandshakeError where arbitrary = genericArbitraryU diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index cbdf4319d..df2db2ae1 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -16,7 +16,8 @@ import Control.Monad.Except (runExceptT) import Data.ByteString.Char8 (ByteString) import Data.List.NonEmpty (NonEmpty) import Network.Socket -import Simplex.Messaging.Client (chooseTransportHost, defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultNetworkConfig) +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Protocol @@ -112,6 +113,7 @@ cfg = smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig, controlPort = Nothing, + smpAgentCfg = defaultSMPClientAgentConfig, allowSMPProxy = False } @@ -119,7 +121,12 @@ cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} proxyCfg :: ServerConfig -proxyCfg = cfg {allowSMPProxy = True} +proxyCfg = + cfgV7 + { allowSMPProxy = True, + smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, + smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion}} + } withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 6cdf1f590..1ab2779c8 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -1,38 +1,117 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} module SMPProxyTests where -import Debug.Trace +import AgentTests.FunctionalAPITests (runRight_) +import Data.ByteString.Char8 (ByteString) import SMPAgentClient (testSMPServer, testSMPServer2) import SMPClient -import ServerTests (sendRecv) +import qualified SMPClient as SMP +import ServerTests (decryptMsgV3, sendRecv) +import Simplex.Messaging.Client +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Version (mkVersionRange) import Test.Hspec +import UnliftIO smpProxyTests :: Spec smpProxyTests = do describe "server configuration" $ do it "refuses proxy handshake unless enabled" testNoProxy it "checks basic auth in proxy requests" testProxyAuth - xdescribe "proxy requests" $ do - xdescribe "bad relay URIs" $ do - it "host not resolved" todo - it "when SMP port blackholed" todo - it "no SMP service at host/port" todo - it "bad SMP fingerprint" todo - it "connects to relay" testProxyConnect - xit "connects to itself as a relay" todo + describe "proxy requests" $ do + describe "bad relay URIs" $ do + xit "host not resolved" todo + xit "when SMP port blackholed" todo + xit "no SMP service at host/port" todo + xit "bad SMP fingerprint" todo xit "batching proxy requests" todo - xdescribe "forwarding requests" $ do - it "sender-proxy-relay-recipient works" todo - it "similar timing for proxied and direct sends" todo + describe "forwarding requests" $ do + describe "deliver message via SMP proxy" $ do + it "same server" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = proxyServ + deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" + it "different servers" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" + xit "max message size, Ed448 keys" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + g <- C.newRandom + msg <- atomically $ C.randomBytes maxMessageLength g + msg' <- atomically $ C.randomBytes maxMessageLength g + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SEd448 msg msg' + it "max message size, Ed25519 keys" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + g <- C.newRandom + msg <- atomically $ C.randomBytes maxMessageLength g + msg' <- atomically $ C.randomBytes maxMessageLength g + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg msg' + it "max message size, X25519 keys" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + g <- C.newRandom + msg <- atomically $ C.randomBytes maxMessageLength g + msg' <- atomically $ C.randomBytes maxMessageLength g + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SX25519 msg msg' + xit "sender-proxy-relay-recipient works" todo + xit "similar timing for proxied and direct sends" todo + +deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> ByteString -> ByteString -> IO () +deliverMessageViaProxy proxyServ relayServ alg msg msg' = do + g <- C.newRandom + -- set up proxy + Right pc <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ()) + THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc + -- set up relay + msgQ <- newTBQueueIO 4 + Right rc <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) (\_ -> pure ()) + runRight_ $ do + -- prepare receiving queue + (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g + (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g + QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe + let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv + -- get proxy session + (sessId, v, relayKey) <- createSMPProxySession pc relayServ (Just "correct") + -- send via proxy to unsecured queue + proxySMPMessage pc sessId v relayKey Nothing sndId noMsgFlags msg + -- receive 1 + (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ + liftIO $ dec msgId encBody `shouldBe` Right msg + ackSMPMessage rc rPriv rcvId msgId + -- secure queue + (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g + secureSMPQueue rc rPriv rcvId sPub + -- send via proxy to secured queue + proxySMPMessage pc sessId v relayKey (Just sPriv) sndId noMsgFlags msg' + -- receive 2 + (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ + liftIO $ dec msgId' encBody' `shouldBe` Right msg' + ackSMPMessage rc rPriv rcvId msgId' proxyVRange :: VersionRangeSMP proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion @@ -48,21 +127,11 @@ testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do - (_, s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") - traceShowM s + (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") reply `shouldBe` Right (ERR AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} -testProxyConnect :: IO () -testProxyConnect = do - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do - testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do - (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 Nothing) - case reply of - Right PKEY {} -> pure () - _ -> fail $ "bad reply: " <> show reply - todo :: IO () todo = do fail "TODO" diff --git a/tests/Test.hs b/tests/Test.hs index f9fb2a2c0..cd2b0d8c3 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -47,7 +47,7 @@ main = do $ do describe "Agent SQLite schema dump" schemaDumpTest describe "Core tests" $ do - describe "Batching tests" batchingTests + xdescribe "Batching tests" batchingTests describe "Encoding tests" encodingTests describe "Protocol error tests" protocolErrorTests describe "Version range" versionRangeTests From 58ede38bf4e011d99248bb571283a0d255b7a833 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:58:15 +0300 Subject: [PATCH 04/13] put smp errors into proxy wrappers (#1103) * put smp errors into proxy wrappers * use substring in PROXY UNEXPECTED error * fix encoding * revert String encoding, discard invalid errors in QC --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Client.hs | 9 ++-- src/Simplex/Messaging/Encoding/String.hs | 2 + src/Simplex/Messaging/Protocol.hs | 54 +++++++++++++++--------- src/Simplex/Messaging/Server.hs | 2 +- tests/CoreTests/ProtocolErrorTests.hs | 19 ++++++--- 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 88afea56b..11243b4e0 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -484,16 +484,15 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} --- TODO keep error params smpProxyError :: SMPClientError -> ErrorType smpProxyError = \case - PCEProtocolError _ -> PROXY PROTOCOL - PCEResponseError _ -> PROXY RESPONSE - PCEUnexpectedResponse _ -> PROXY UNEXPECTED + PCEProtocolError et -> PROXY (PROTOCOL et) + PCEResponseError et -> PROXY (RESPONSE et) + PCEUnexpectedResponse bs -> PROXY (UNEXPECTED $ B.unpack $ B.take 32 bs) PCEResponseTimeout -> PROXY TIMEOUT PCENetworkError -> PROXY NETWORK PCEIncompatibleHost -> PROXY BAD_HOST - PCETransportError _ -> PROXY TRANSPORT + PCETransportError t -> PROXY (TRANSPORT t) PCECryptoError _ -> INTERNAL PCEIOError _ -> INTERNAL diff --git a/src/Simplex/Messaging/Encoding/String.hs b/src/Simplex/Messaging/Encoding/String.hs index fcefdc73d..6b9fb5624 100644 --- a/src/Simplex/Messaging/Encoding/String.hs +++ b/src/Simplex/Messaging/Encoding/String.hs @@ -75,6 +75,8 @@ instance StrEncoding Str where strEncode = unStr strP = Str <$> A.takeTill (== ' ') <* optional A.space +-- inherited from ByteString, the parser only allows non-empty strings +-- only Char8 elements may round-trip as B.pack truncates unicode instance StrEncoding String where strEncode = strEncode . B.pack strP = B.unpack <$> strP diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 3ad510481..fafd8a340 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -195,6 +195,8 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) @@ -210,7 +212,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -1167,11 +1169,11 @@ data ErrorType instance StrEncoding ErrorType where strEncode = \case CMD e -> "CMD " <> bshow e - PROXY e -> "PROXY " <> bshow e + PROXY e -> "PROXY " <> strEncode e e -> bshow e strP = "CMD " *> (CMD <$> parseRead1) - <|> "PROXY " *> (PROXY <$> parseRead1) + <|> "PROXY " *> (PROXY <$> strP) <|> parseRead1 -- | SMP command error type. @@ -1190,20 +1192,19 @@ data CommandError NO_ENTITY deriving (Eq, Read, Show) --- TODO keep error params data ProxyError = -- | Correctly parsed SMP server ERR response. -- This error is forwarded to the agent client as `ERR SMP err`. - PROTOCOL -- {protocolErr :: String} + PROTOCOL {protocolErr :: ErrorType} | -- | Invalid server response that failed to parse. -- Forwarded to the agent client as `ERR BROKER RESPONSE`. - RESPONSE -- {responseErr :: String} - | UNEXPECTED + RESPONSE {responseErr :: ErrorType} + | UNEXPECTED {unexpectedResponse :: String} -- 'String' for using derived JSON and Arbitrary instances | TIMEOUT | NETWORK | BAD_HOST | NO_SESSION - | TRANSPORT -- {transportErr :: TransportError} + | TRANSPORT {transportErr :: TransportError} deriving (Eq, Read, Show) -- | SMP transmission parser. @@ -1473,26 +1474,42 @@ instance Encoding CommandError where instance Encoding ProxyError where smpEncode e = case e of - PROTOCOL -> "PROTOCOL" - RESPONSE -> "RESPONSE" - UNEXPECTED -> "UNEXPECTED" + PROTOCOL et -> "PROTOCOL " <> smpEncode et + RESPONSE et -> "RESPONSE " <> smpEncode et + UNEXPECTED s -> "UNEXPECTED " <> smpEncode (encodeUtf8 $ T.pack s) TIMEOUT -> "TIMEOUT" NETWORK -> "NETWORK" BAD_HOST -> "BAD_HOST" NO_SESSION -> "NO_SESSION" - TRANSPORT -> "TRANSPORT" + TRANSPORT t -> "TRANSPORT " <> serializeTransportError t smpP = A.takeTill (== ' ') >>= \case - "PROTOCOL" -> pure PROTOCOL - "RESPONSE" -> pure RESPONSE - "UNEXPECTED" -> pure UNEXPECTED + "PROTOCOL" -> PROTOCOL <$> _smpP + "RESPONSE" -> RESPONSE <$> _smpP + "UNEXPECTED" -> UNEXPECTED . (T.unpack . safeDecodeUtf8) <$> _smpP "TIMEOUT" -> pure TIMEOUT "NETWORK" -> pure NETWORK "BAD_HOST" -> pure BAD_HOST "NO_SESSION" -> pure NO_SESSION - "TRANSPORT" -> pure TRANSPORT + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) _ -> fail "bad command error type" +instance StrEncoding ProxyError where + strEncode = \case + PROTOCOL et -> "PROTOCOL " <> strEncode et + RESPONSE et -> "RESPONSE " <> strEncode et + UNEXPECTED "" -> "UNEXPECTED" -- Arbitrary instance generates empty strings which String instance can't handle + UNEXPECTED s -> "UNEXPECTED " <> strEncode s + TRANSPORT t -> "TRANSPORT " <> serializeTransportError t + e -> bshow e + strP = + "PROTOCOL " *> (PROTOCOL <$> strP) + <|> "RESPONSE " *> (RESPONSE <$> strP) + <|> "UNEXPECTED " *> (UNEXPECTED <$> strP) + <|> "UNEXPECTED" $> UNEXPECTED "" + <|> "TRANSPORT " *> (TRANSPORT <$> transportErrorP) + <|> parseRead1 + -- | Send signed SMP transmission to TCP transport. tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (batch params) (blockSize params) @@ -1630,6 +1647,5 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) -$(J.deriveJSON (sumTypeJSON id) ''ProxyError) - -$(J.deriveJSON (sumTypeJSON id) ''ErrorType) +-- run deriveJSON in one TH splice to allow mutual instance +$(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType]) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 959339fad..b4c9722f7 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -632,7 +632,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi vr = supportedServerSMPRelayVRange in case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR $ PROXY TRANSPORT -- TODO different error? + Nothing -> ERR $ PROXY (TRANSPORT TENoServerAuth) Left err -> ERR $ smpProxyError err PFWD pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index a486e6549..8f5ad70e7 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -17,6 +17,7 @@ import qualified Simplex.Messaging.Agent.Protocol as Agent import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..)) +import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) import Test.Hspec @@ -28,15 +29,19 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do describe "errors parsing / serializing" $ do it "should parse SMP protocol errors" . property $ \(err :: ErrorType) -> smpDecode (smpEncode err) == Right err - it "should parse SMP agent errors" . property $ \(err :: AgentErrorType) -> - errHasSpaces err - || strDecode (strEncode err) == Right err + it "should parse SMP agent errors" . property . forAll possible $ \err -> + strDecode (strEncode err) == Right err where - errHasSpaces = \case - BROKER srv (Agent.RESPONSE e) -> hasSpaces srv || hasSpaces e - BROKER srv _ -> hasSpaces srv - _ -> False + possible :: Gen AgentErrorType + possible = + arbitrary >>= \case + BROKER srv (Agent.RESPONSE e) | hasSpaces srv || hasSpaces e -> discard + BROKER srv _ | hasSpaces srv -> discard + SMP (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard + NTF (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard + ok -> pure ok hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) + hasUnicode = any (>= '\255') deriving instance Generic AgentErrorType From 2d1609f222a38250db49d43998743d9bf52fc3a5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 19 Apr 2024 20:24:25 +0100 Subject: [PATCH 05/13] update envelope sizes for proxied messages, remove unnecessary proxy-relay encryption padding (#1107) * update envelope sizes for proxied messages * remove unnecessary padding from proxy-relay encryption --- src/Simplex/Messaging/Client.hs | 4 ++-- src/Simplex/Messaging/Protocol.hs | 26 +++++++++----------------- src/Simplex/Messaging/Server.hs | 7 ++++--- src/Simplex/Messaging/Transport.hs | 5 ----- tests/SMPProxyTests.hs | 15 ++++++++------- tests/ServerTests.hs | 4 ++-- 6 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 11243b4e0..8d4db29d9 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -732,12 +732,12 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} - eft <- liftEitherWith PCECryptoError $ EncFwdTransmission <$> C.cbEncrypt sessSecret nonce (smpEncode fwdT) paddedForwardedMsgLength + eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) -- send sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case RRES (EncFwdResponse efr) -> do -- unwrap - r' <- liftEitherWith PCECryptoError $ C.cbDecrypt sessSecret (C.reverseNonce nonce) efr + r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' pure fwdResponse r -> throwE . PCEUnexpectedResponse $ bshow r diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index fafd8a340..118f3b084 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -44,7 +44,6 @@ module Simplex.Messaging.Protocol supportedSMPClientVRange, maxMessageLength, paddedProxiedMsgLength, - paddedForwardedMsgLength, e2eEncConfirmationLength, e2eEncMessageLength, @@ -243,23 +242,16 @@ currentSMPClientVersion = VersionSMPC 2 supportedSMPClientVRange :: VersionRangeSMPC supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClientVersion -maxMessageLength :: Int -maxMessageLength = 16088 +-- TODO v6.0 remove dependency on version +maxMessageLength :: VersionSMP -> Int +maxMessageLength v + | v >= sendingProxySMPVersion = 16064 -- max 16067 + | otherwise = 16088 -- 16064 - always use this size to determine allowed ranges --- without signature works with min 16151 (fails with 16150) --- with Ed448: 16265 (fails with 16264) --- with Ed25519: 16215 (fails with 16214) --- with X25519: 16232 (fails with 16231) paddedProxiedMsgLength :: Int -paddedProxiedMsgLength = 16232 - --- without signature works with min 16239 (fails with 16238) --- with Ed448: 16353 (fails with 16352) --- with Ed25519: 16303 (fails with 16302) --- with X25519: 16320 (fails with 16319) -paddedForwardedMsgLength :: Int -paddedForwardedMsgLength = 16320 +paddedProxiedMsgLength = 16244 -- 16241 .. 16245 +-- TODO v6.0 change to 16064 type MaxMessageLen = 16088 -- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used) @@ -267,10 +259,10 @@ type MaxRcvMessageLen = MaxMessageLen + 16 -- 16104, the padded size is 16106 -- it is shorter to allow per-queue e2e encryption DH key in the "public" header e2eEncConfirmationLength :: Int -e2eEncConfirmationLength = 15936 +e2eEncConfirmationLength = 15920 -- 15881 .. 15976 e2eEncMessageLength :: Int -e2eEncMessageLength = 16032 +e2eEncMessageLength = 16016 -- 16004 .. 16021 -- | SMP protocol clients data Party = Recipient | Sender | Notifier | ProxiedClient diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index b4c9722f7..d1fcfbc24 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -856,7 +856,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi sendMessage :: QueueRec -> MsgFlags -> MsgBody -> M (Transmission BrokerMsg) sendMessage qr msgFlags msgBody - | B.length msgBody > maxMessageLength = pure $ err LARGE_MSG + | B.length msgBody > maxMessageLength thVersion = pure $ err LARGE_MSG | otherwise = case status qr of QueueOff -> return $ err AUTH QueueActive -> @@ -880,6 +880,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi atomically $ updatePeriodStats (activeQueues stats) (recipientId qr) pure ok where + THandleParams {thVersion} = thParams' mkMessage :: C.MaxLenBS MaxMessageLen -> M Message mkMessage body = do msgId <- randomId =<< asks (msgIdBytes . config) @@ -921,7 +922,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi sessSecret <- maybe (throwError $ ERR INTERNAL) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId -- TODO error - s' <- liftEitherWith internalErr $ C.cbDecrypt sessSecret proxyNonce s + s' <- liftEitherWith internalErr $ C.cbDecryptNoPad sessSecret proxyNonce s -- TODO error FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s' -- TODO error - this error is reported to proxy, as we failed to get to client's transmission @@ -949,7 +950,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength -- encrypt to proxy let fr = FwdResponse {fwdCorrId, fwdResponse = r2} - r3 <- liftEitherWith internalErr $ EncFwdResponse <$> C.cbEncrypt sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) paddedForwardedMsgLength + r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) pure $ RRES r3 where internalErr _ = ERR INTERNAL -- TODO errors diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 001d77fa7..a353849da 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -115,11 +115,6 @@ import UnliftIO.STM -- * Transport parameters --- min size it works with: --- unsigned message: 16292 (paddedProxiedMsgLength = 16151, paddedForwardedMsgLength = 16239) --- Ed448: 16406 (16384 + 22, fails with 21) --- Ed25519: 16356 --- X25519: 16381 smpBlockSize :: Int smpBlockSize = 16384 diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 1ab2779c8..2a33ec055 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -39,6 +39,7 @@ smpProxyTests = do xit "batching proxy requests" todo describe "forwarding requests" $ do describe "deliver message via SMP proxy" $ do + let maxLen = maxMessageLength sendingProxySMPVersion it "same server" $ withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash @@ -50,12 +51,12 @@ smpProxyTests = do let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" - xit "max message size, Ed448 keys" $ + it "max message size, Ed448 keys" $ withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do g <- C.newRandom - msg <- atomically $ C.randomBytes maxMessageLength g - msg' <- atomically $ C.randomBytes maxMessageLength g + msg <- atomically $ C.randomBytes maxLen g + msg' <- atomically $ C.randomBytes maxLen g let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SEd448 msg msg' @@ -63,8 +64,8 @@ smpProxyTests = do withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do g <- C.newRandom - msg <- atomically $ C.randomBytes maxMessageLength g - msg' <- atomically $ C.randomBytes maxMessageLength g + msg <- atomically $ C.randomBytes maxLen g + msg' <- atomically $ C.randomBytes maxLen g let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg msg' @@ -72,8 +73,8 @@ smpProxyTests = do withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do g <- C.newRandom - msg <- atomically $ C.randomBytes maxMessageLength g - msg' <- atomically $ C.randomBytes maxMessageLength g + msg <- atomically $ C.randomBytes maxLen g + msg' <- atomically $ C.randomBytes maxLen g let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SX25519 msg msg' diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index a7e8fee86..b0ed67913 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -183,12 +183,12 @@ testCreateSecure (ATransport t) = Resp "dabc" _ err5 <- sendRecv s ("", "dabc", sId, _SEND "hello") (err5, ERR AUTH) #== "rejects unsigned SEND" - let maxAllowedMessage = B.replicate maxMessageLength '-' + let maxAllowedMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion) '-' Resp "bcda" _ OK <- signSendRecv s sKey ("bcda", sId, _SEND maxAllowedMessage) Resp "" _ (Msg mId3 msg3) <- tGet1 r (dec mId3 msg3, Right maxAllowedMessage) #== "delivers message of max size" - let biggerMessage = B.replicate (maxMessageLength + 1) '-' + let biggerMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion + 1) '-' Resp "bcda" _ (ERR LARGE_MSG) <- signSendRecv s sKey ("bcda", sId, _SEND biggerMessage) pure () From 6d60de2429187f2db9bbf25a762846d3945823f5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 1 May 2024 08:48:33 +0100 Subject: [PATCH 06/13] proxy: agent implementation (#1106) * proxy: agent implementation * revert change * update rfc * test stuck subscription mock * store proxy sessions inside SMP client var * rename * create and use proxy session * tests * return proxy in SENT event * rename, more tests * rename * more tests * remove comment --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> --- rfcs/2023-09-12-second-relays.md | 50 +++++ src/Simplex/Messaging/Agent.hs | 22 +- src/Simplex/Messaging/Agent/Client.hs | 273 +++++++++++++++++------- src/Simplex/Messaging/Agent/Protocol.hs | 28 +-- src/Simplex/Messaging/Client.hs | 46 +++- tests/AgentTests.hs | 8 +- tests/AgentTests/FunctionalAPITests.hs | 86 ++++---- tests/AgentTests/NotificationTests.hs | 20 +- tests/SMPAgentClient.hs | 11 +- tests/SMPClient.hs | 8 +- tests/SMPProxyTests.hs | 157 +++++++++----- 11 files changed, 491 insertions(+), 218 deletions(-) diff --git a/rfcs/2023-09-12-second-relays.md b/rfcs/2023-09-12-second-relays.md index a47eb7bde..cad6c4a92 100644 --- a/rfcs/2023-09-12-second-relays.md +++ b/rfcs/2023-09-12-second-relays.md @@ -196,6 +196,56 @@ dhPublic = length x509encoded The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response). +### Implementation considerations for the client + +While client/server protocol is rather straightforward to implement, and it is already working, there are some decisions to make about how the client makes decisions about. + +1. When to use proxy and when to connect directly to the destination relay. + +While from the perspective of threat model improvement it may be beneficial to always use the proxy, choosing the proxy that is different from other relays in the connection, initially we need to make it opt-in, with an option to only use it for unknown destination relays, to minimize any unexpected adverse effect on the delivery latency. + +Proxy mode will be passed from the client via NetworkConfig. + +2. Which proxying relays to use. + +Ability to request access to the session with the destination relay (and to create such session) is protected with the same basic auth approach as creating queues - the logic here is that opening private servers to all users as proxies would increase the scenarios for DoS attacks (which is the case with the public servers). + +The open question is whether the client should choose proxies from: +- all configured relays. +- there should be a subset of configured relays. +- there should be a separate list. + +E.g., there could be a second toggle in the relay configuration to allow using relay as proxy, in addition to the current toggle that allows creating queues. + +For simplicity, initially we will just use all enabled relays as potential proxies. + +3. How many proxying relays should be used during one session. + +This is not a simple question, and it creates a contradiction between two risks: +- collusion between proxies and destination relays simplifies correlating sending clients by session - from the point of view of this risk, clients should follow the same policy for creating connections with proxies, that is to create a new connection for each user profile, and if transport isolation is set to "per connection" - for each destination queue. +- traffic correlation by observable traffic sessions (particularly if an attacker can observe user's ISP traffic or multiple proxies) - from this point of view, it would be beneficial to use fewer proxies and fewer connections with proxies and see the risk of proxy colluding with the destination relay as lower than the risk of traffic observation that in the case of multiple sessions would allow to correlate traffic to rarely used destination relays (any private self-hosted relays) and the traffic of the user to a given proxy, to prove the fact of user communicating with the destination relay via the proxy. + +While we can transfer this choice on the users, it seems a complex decision to make, and overall the second risk (traffic correlation) seems more important to address than the first. + +In any case possible options are: +1. Extreme option 1: Create a new proxy session, with the new random proxy, for each potential transport session that would exist if the user were to be connected to destination relays directly. That is, never to mix access to multiple relays from multiple user profiles (and in case of per-connection isolation, to multiple queues) into a one client session with proxy. This is a rather radical option that nullifies any advantages of having fewer sessions with proxies than there would have been with the destination relays and removes any benefits of batching destination server session requests (PRXY comands). +2. Extreme option 2: Use only one proxy session at the time, mixing traffic from all user profiles and to all destination servers (and for all queues) into a session with one proxy. This minimizes the risks of traffic correlation in case of non-colluding proxy, but maximises the risk in case it colludes with the destination relays. +3. Balanced option: Use one proxy session per user profile, but mix traffic to multiple queues irrespective of connection isolation option and to all destination servers. Given that connection isolation is an experimental option, this makes the most sense, but it would have to be disclosed. +4. Less balanced option: take connection isolation option into account and create a new proxy connection for each destination queue. This feels worse than option 3. + +If option 3 is chosen, then the transport session key with the proxy would be different from the transport session key with the relay - proxy session will only use UserId as the key, and the relay session uses (UserId, Server, Maybe EntityId) as the key. + +If option 4 is chosen, the keys would also be different, as the proxy would then use (UserId, Maybe (Server, EntityId)) as the key. + +We could potentially key proxy sessions (and create proxy connections) per each destination relay, in the same way as we key relays themselves, but it seems to have the least sense, as we neither achieve isolation by queue in case proxy and destination relay collude, nor we sufficiently protect from traffic correlation by any observers. + +The implemented design is this: +- for each destination relay a random proxy is chosen and used to send all messages - all requests from a client coalesce to a single session. +- transport isolation mode is taken into account, that is if per-connection isolation is enabled, then a separate proxy connection will be created for each messaging queue. +- supported modes when proxy is used: always, for unknown relays, for unknown relays when IP address is not protected, never. + +This decision is made because the argument for protection against collusion between proxy and relay and more balanced traffic distribution is stronger than the argument for protection against traffic correlation, because even mixing all messages to one proxy connection does not provide protection against traffic correlation by time, so in any case it requires adding delays. + ### Threat model for SMP proxy and changes to threat model for SMP #### SMP proxy diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 2d9fedc65..600396700 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -149,7 +149,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Util (removePath) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Agent.Lock (withLock', withLock) +import Simplex.Messaging.Agent.Lock (withLock, withLock') import Simplex.Messaging.Agent.NtfSubSupervisor import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval @@ -160,7 +160,7 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs) -import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOn, pattern PQEncOff, pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -198,7 +198,7 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = liftIO $ newSMPAgentEnv cfg store >>= runReaderT runAgent where runAgent = do - c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask + c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask t <- runAgentThreads c `forkFinally` const (liftIO $ disconnectAgentClient c) atomically . writeTVar acThread . Just =<< mkWeakThreadId t pure c @@ -239,7 +239,7 @@ createUser c = withAgentEnv c .: createUser' c {-# INLINE createUser #-} -- | Delete user record optionally deleting all user's connections on SMP servers -deleteUser :: AgentClient -> UserId -> Bool -> AE () +deleteUser :: AgentClient -> UserId -> Bool -> AE () deleteUser c = withAgentEnv c .: deleteUser' c {-# INLINE deleteUser #-} @@ -815,7 +815,7 @@ joinConnSrv c userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMo lift (compatibleContactUri cReqUri pqSup) >>= \case Just (qInfo, vrsn) -> do (connId', cReq) <- newConnSrv c userId connId enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv - sendInvitation c userId qInfo vrsn cReq cInfo + void $ sendInvitation c userId qInfo vrsn cReq cInfo pure connId' Nothing -> throwError $ AGENT A_VERSION @@ -1209,7 +1209,7 @@ enqueueMessage c cData sq msgFlags aMessage = {-# INLINE enqueueMessage #-} -- this function is used only for sending messages in batch, it returns the list of successes to enqueue additional deliveries -enqueueMessageB :: forall t. (Traversable t) => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) +enqueueMessageB :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) enqueueMessageB c reqs = do cfg <- asks config reqMids <- withStoreBatch c $ \db -> fmap (bindRight $ storeSentMsg db cfg) reqs @@ -1242,7 +1242,7 @@ enqueueSavedMessage :: AgentClient -> ConnData -> AgentMsgId -> SndQueue -> AM' enqueueSavedMessage c cData msgId sq = enqueueSavedMessageB c $ Identity (cData, [sq], msgId) {-# INLINE enqueueSavedMessage #-} -enqueueSavedMessageB :: (Foldable t) => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () +enqueueSavedMessageB :: Foldable t => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () enqueueSavedMessageB c reqs = do -- saving to the database is in the start to avoid race conditions when delivery is read from queue before it is saved void $ withStoreBatch' c $ \db -> concatMap (storeDeliveries db) reqs @@ -1333,7 +1333,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork retrySndMsg riMode = do withStore' c $ \db -> updatePendingMsgRIState db connId msgId riState retrySndOp c $ loop riMode - Right () -> do + Right proxySrv_ -> do case msgType of AM_CONN_INFO -> setConfirmed AM_CONN_INFO_REPLY -> setConfirmed @@ -1355,7 +1355,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork when (status == Active) $ notify $ CON pqEncryption -- this branch should never be reached as receive queue is created before the confirmation, _ -> logError "HELLO sent without receive queue" - AM_A_MSG_ -> notify $ SENT mId + AM_A_MSG_ -> notify $ SENT mId proxySrv_ AM_A_RCVD_ -> pure () AM_QCONT_ -> pure () AM_QADD_ -> pure () @@ -2212,7 +2212,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, where processEND = \case Just (Right clnt) - | sessId == sessionId (thParams clnt) -> do + | sessId == sessionId (thParams $ connectedClient clnt) -> do removeSubscription c connId notify' END pure "END" @@ -2574,7 +2574,7 @@ confirmQueueAsync c cData sq srv connInfo e2eEncryption_ subMode = do confirmQueue :: Compatible VersionSMPA -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () confirmQueue (Compatible agentVersion) c cData@ConnData {connId, pqSupport} sq srv connInfo e2eEncryption_ subMode = do msg <- mkConfirmation =<< mkAgentConfirmation c cData sq srv connInfo subMode - sendConfirmation c sq msg + void $ sendConfirmation c sq msg withStore' c $ \db -> setSndQueueStatus db sq Confirmed where mkConfirmation :: AgentMessage -> AM MsgBody diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 27223b12f..c29e35499 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -132,6 +132,8 @@ module Simplex.Messaging.Agent.Client SMPTransportSession, NtfTransportSession, XFTPTransportSession, + ProxiedRelay (..), + SMPConnectedClient (..), ) where @@ -230,7 +232,7 @@ import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (SMPVersion) -import Simplex.Messaging.Transport.Client (TransportHost) +import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version import System.Mem.Weak (Weak) @@ -263,6 +265,10 @@ data AgentClient = AgentClient msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg), smpServers :: TMap UserId (NonEmpty SMPServerWithAuth), smpClients :: TMap SMPTransportSession SMPClientVar, + -- smpProxiedRelays: + -- SMPTransportSession defines connection from proxy to relay, + -- SMPServerWithAuth defines client connected to SMP proxy (with the same userId and entityId in TransportSession) + smpProxiedRelays :: TMap SMPTransportSession SMPServerWithAuth, ntfServers :: TVar [NtfServer], ntfClients :: TMap NtfTransportSession NtfClientVar, xftpServers :: TMap UserId (NonEmpty XFTPServerWithAuth), @@ -297,6 +303,13 @@ data AgentClient = AgentClient agentEnv :: Env } +data SMPConnectedClient = SMPConnectedClient + { connectedClient :: SMPClient, + proxiedRelays :: TMap SMPServer ProxiedRelayVar + } + +type ProxiedRelayVar = SessionVar (Either AgentErrorType ProxiedRelay) + getAgentWorker :: (Ord k, Show k) => String -> Bool -> AgentClient -> k -> TMap k Worker -> (Worker -> AM ()) -> AM' Worker getAgentWorker = getAgentWorker' id pure {-# INLINE getAgentWorker #-} @@ -428,6 +441,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = msgQ <- newTBQueue qSize smpServers <- newTVar smp smpClients <- TM.empty + smpProxiedRelays <- TM.empty ntfServers <- newTVar ntf ntfClients <- TM.empty xftpServers <- newTVar xftp @@ -463,6 +477,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = msgQ, smpServers, smpClients, + smpProxiedRelays, ntfServers, ntfClients, xftpServers, @@ -511,15 +526,19 @@ agentDRG AgentClient {agentEnv = Env {random}} = random class (Encoding err, Show err) => ProtocolServerClient v err msg | msg -> v, msg -> err where type Client msg = c | c -> msg getProtocolServerClient :: AgentClient -> TransportSession msg -> AM (Client msg) + type ProtoClient msg = c | c -> msg + protocolClient :: Client msg -> ProtoClient msg clientProtocolError :: err -> AgentErrorType - closeProtocolServerClient :: Client msg -> IO () - clientServer :: Client msg -> String - clientTransportHost :: Client msg -> TransportHost - clientSessionTs :: Client msg -> UTCTime + closeProtocolServerClient :: ProtoClient msg -> IO () + clientServer :: ProtoClient msg -> String + clientTransportHost :: ProtoClient msg -> TransportHost + clientSessionTs :: ProtoClient msg -> UTCTime instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where - type Client BrokerMsg = ProtocolClient SMPVersion ErrorType BrokerMsg + type Client BrokerMsg = SMPConnectedClient getProtocolServerClient = getSMPServerClient + type ProtoClient BrokerMsg = ProtocolClient SMPVersion ErrorType BrokerMsg + protocolClient = connectedClient clientProtocolError = SMP closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer @@ -529,6 +548,8 @@ instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where instance ProtocolServerClient NTFVersion ErrorType NtfResponse where type Client NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse getProtocolServerClient = getNtfServerClient + type ProtoClient NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse + protocolClient = id clientProtocolError = NTF closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer @@ -538,61 +559,120 @@ instance ProtocolServerClient NTFVersion ErrorType NtfResponse where instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where type Client FileResponse = XFTPClient getProtocolServerClient = getXFTPServerClient + type ProtoClient FileResponse = XFTPClient + protocolClient = id clientProtocolError = XFTP closeProtocolServerClient = X.closeXFTPClient clientServer = X.xftpClientServer clientTransportHost = X.xftpTransportHost clientSessionTs = X.xftpSessionTs -getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPClient -getSMPServerClient c@AgentClient {active, smpClients, msgQ, workerSeq} tSess@(userId, srv, _) = do +getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClient +getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do unlessM (readTVarIO active) . throwError $ INACTIVE atomically (getSessVar workerSeq tSess smpClients) >>= either newClient (waitForProtocolClient c tSess) where - -- we resubscribe only on newClient error, but not on waitForProtocolClient error, - -- as the large number of delivery workers waiting for the client TMVar - -- make it expensive to check for pending subscriptions. - newClient v = - newProtocolClient c tSess smpClients connectClient v - `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwError e - connectClient :: SMPClientVar -> AM SMPClient - connectClient v = do + newClient v = do + prs <- atomically TM.empty + smpConnectClient c tSess prs v + +getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, ProxiedRelay) +getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do + unlessM (readTVarIO active) . throwError $ INACTIVE + proxySrv <- getNextServer c userId [destSrv] + atomically (getClientVar proxySrv) >>= \(tSess, auth, v) -> + either (newProxyClient tSess auth) (waitForProxyClient tSess auth) v + where + getClientVar :: SMPServerWithAuth -> STM (SMPTransportSession, Maybe SMP.BasicAuth, Either SMPClientVar SMPClientVar) + getClientVar proxySrv = do + ProtoServerWithAuth srv auth <- TM.lookup destSess smpProxiedRelays >>= maybe (TM.insert destSess proxySrv smpProxiedRelays $> proxySrv) pure + let tSess = (userId, srv, qId) + (tSess,auth,) <$> getSessVar workerSeq tSess smpClients + newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + newProxyClient tSess auth v = do + (prs, rv) <- atomically $ do + prs <- TM.empty + -- we do not need to check if it is a new proxied relay session, + -- as the client is just created and there are no sessions yet + (prs,) . either id id <$> getSessVar workerSeq destSrv prs + clnt <- smpConnectClient c tSess prs v + (clnt,) <$> newProxiedRelay clnt auth rv + waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + waitForProxyClient tSess auth v = do + clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess v + sess <- + atomically (getSessVar workerSeq destSrv prs) + >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) + pure (clnt, sess) + newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM ProxiedRelay + newProxiedRelay clnt@(SMPConnectedClient smp prs) proxyAuth rv = + tryAgentError (liftClient SMP (clientServer smp) $ connectSMPProxiedRelay smp destSrv proxyAuth) >>= \case + Right sess -> do + atomically $ putTMVar (sessionVar rv) (Right sess) + liftIO $ incClientStat c userId clnt "PROXY" "OK" + pure sess + Left e -> do + liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e + atomically $ do + removeSessVar rv destSrv prs + TM.delete destSess smpProxiedRelays + putTMVar (sessionVar rv) (Left e) + throwError e -- signal error to caller + waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM ProxiedRelay + waitForProxiedRelay (_, srv, _) rv = do + NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c + sess_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar rv) + liftEither $ case sess_ of + Just (Right sess) -> Right sess + Just (Left e) -> Left e + Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT + +smpConnectClient :: AgentClient -> SMPTransportSession -> TMap SMPServer ProxiedRelayVar -> SMPClientVar -> AM SMPConnectedClient +smpConnectClient c@AgentClient {smpClients, msgQ} tSess@(_, srv, _) prs v = + newProtocolClient c tSess smpClients connectClient v + `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwError e + where + connectClient :: SMPClientVar -> AM SMPConnectedClient + connectClient v' = do cfg <- lift $ getClientConfig c smpCfg g <- asks random env <- ask - liftError' (protocolClientError SMP $ B.unpack $ strEncode srv) $ - getProtocolClient g tSess cfg (Just msgQ) $ - clientDisconnected env v + liftError (protocolClientError SMP $ B.unpack $ strEncode srv) $ do + smp <- ExceptT $ getProtocolClient g tSess cfg (Just msgQ) $ smpClientDisconnected c tSess env v' prs + pure SMPConnectedClient {connectedClient = smp, proxiedRelays = prs} - clientDisconnected :: Env -> SMPClientVar -> SMPClient -> IO () - clientDisconnected env v client = do - removeClientAndSubs >>= serverDown - logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv +smpClientDisconnected :: AgentClient -> SMPTransportSession -> Env -> SMPClientVar -> TMap SMPServer ProxiedRelayVar -> SMPClient -> IO () +smpClientDisconnected c@AgentClient {active, smpClients, smpProxiedRelays} tSess@(userId, srv, qId) env v prs client = do + removeClientAndSubs >>= serverDown + logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv + where + -- we make active subscriptions pending only if the client for tSess was current (in the map) and active, + -- because we can have a race condition when a new current client could have already + -- made subscriptions active, and the old client would be processing diconnection later. + removeClientAndSubs :: IO ([RcvQueue], [ConnId]) + removeClientAndSubs = atomically $ ifM currentActiveClient removeSubs $ pure ([], []) where - -- we make active subscriptions pending only if the client for tSess was current (in the map) and active, - -- because we can have a race condition when a new current client could have already - -- made subscriptions active, and the old client would be processing diconnection later. - removeClientAndSubs :: IO ([RcvQueue], [ConnId]) - removeClientAndSubs = atomically $ ifM currentActiveClient removeSubs $ pure ([], []) - where - currentActiveClient = (&&) <$> removeSessVar' v tSess smpClients <*> readTVar active - removeSubs = do - (qs, cs) <- RQ.getDelSessQueues tSess $ activeSubs c - RQ.batchAddQueues (pendingSubs c) qs - pure (qs, cs) + currentActiveClient = (&&) <$> removeSessVar' v tSess smpClients <*> readTVar active + removeSubs = do + (qs, cs) <- RQ.getDelSessQueues tSess $ activeSubs c + RQ.batchAddQueues (pendingSubs c) qs + -- this removes proxied relays that this client created sessions to + destSrvs <- M.keys <$> readTVar prs + forM_ destSrvs $ \destSrv -> TM.delete (userId, destSrv, qId) smpProxiedRelays + pure (qs, cs) - serverDown :: ([RcvQueue], [ConnId]) -> IO () - serverDown (qs, conns) = whenM (readTVarIO active) $ do - incClientStat c userId client "DISCONNECT" "" - notifySub "" $ hostEvent DISCONNECT client - unless (null conns) $ notifySub "" $ DOWN srv conns - unless (null qs) $ do - atomically $ mapM_ (releaseGetLock c) qs - runReaderT (resubscribeSMPSession c tSess) env + serverDown :: ([RcvQueue], [ConnId]) -> IO () + serverDown (qs, conns) = whenM (readTVarIO active) $ do + incClientStat' c userId client "DISCONNECT" "" + notifySub "" $ hostEvent' DISCONNECT client + unless (null conns) $ notifySub "" $ DOWN srv conns + unless (null qs) $ do + atomically $ mapM_ (releaseGetLock c) qs + runReaderT (resubscribeSMPSession c tSess) env - notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () - notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) + notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () + notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) resubscribeSMPSession :: AgentClient -> SMPTransportSession -> AM' () resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = @@ -735,7 +815,11 @@ newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v = throwError e -- signal error to caller hostEvent :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> Client msg -> ACommand 'Agent 'AENone -hostEvent event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost +hostEvent event = hostEvent' event . protocolClient +{-# INLINE hostEvent #-} + +hostEvent' :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> ProtoClient msg -> ACommand 'Agent 'AENone +hostEvent' event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost getClientConfig :: AgentClient -> (AgentConfig -> ProtocolClientConfig v) -> AM' (ProtocolClientConfig v) getClientConfig c cfgSel = do @@ -842,7 +926,7 @@ closeClient_ c v = do NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c E.handle (\BlockedIndefinitelyOnSTM -> pure ()) $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) >>= \case - Just (Right client) -> closeProtocolServerClient client `catchAll_` pure () + Just (Right client) -> closeProtocolServerClient (protocolClient client) `catchAll_` pure () _ -> pure () closeXFTPServerClient :: AgentClient -> UserId -> XFTPServer -> FileDigest -> IO () @@ -895,6 +979,22 @@ withClient_ c tSess@(userId, srv, _) statCmd action = do stat cl $ strEncode e throwError e +withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a +withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do + cp@(cl, _) <- getSMPProxyClient c destSess + logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr + r <- (action cp <* stat cl "OK") `catchAgentError` logServerError cl + logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" + pure r + where + stat cl = liftIO . incClientStat c userId cl cmdStr + proxySrv = showServer . protocolClientServer' . protocolClient + logServerError :: SMPConnectedClient -> AgentErrorType -> AM a + logServerError cl e = do + logServer ("<-- " <> proxySrv cl <> " <") c destSrv "" $ strEncode e + stat cl $ strEncode e + throwError e + withLogClient_ :: ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> AM a) -> AM a withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do logServer "-->" c srv entId cmdStr @@ -903,22 +1003,46 @@ withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do return res withClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a -withClient c tSess statKey action = withClient_ c tSess statKey $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer client) $ action client +withClient c tSess statKey action = withClient_ c tSess statKey $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client {-# INLINE withClient #-} withLogClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a -withLogClient c tSess entId cmdStr action = withLogClient_ c tSess entId cmdStr $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer client) $ action client +withLogClient c tSess entId cmdStr action = withLogClient_ c tSess entId cmdStr $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client {-# INLINE withLogClient #-} withSMPClient :: SMPQueueRec q => AgentClient -> q -> ByteString -> (SMPClient -> ExceptT SMPClientError IO a) -> AM a withSMPClient c q cmdStr action = do tSess <- liftIO $ mkSMPTransportSession c q - withLogClient c tSess (queueId q) cmdStr action + withLogClient c tSess (queueId q) cmdStr $ action . connectedClient -withSMPClient_ :: SMPQueueRec q => AgentClient -> q -> ByteString -> (SMPClient -> AM a) -> AM a -withSMPClient_ c q cmdStr action = do - tSess <- liftIO $ mkSMPTransportSession c q - withLogClient_ c tSess (queueId q) cmdStr action +sendOrProxySMPMessage :: AgentClient -> UserId -> SMPServer -> ByteString -> Maybe SMP.SndPrivateAuthKey -> SMP.SenderId -> MsgFlags -> SMP.MsgBody -> AM (Maybe SMPServer) +sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do + sess <- liftIO $ mkTransportSession c userId destSrv senderId + ifM (atomically shouldUseProxy) (sendViaProxy sess) (sendDirectly sess $> Nothing) + where + shouldUseProxy = do + cfg <- getNetworkConfig c + case smpProxyMode cfg of + SPMAlways -> pure True + SPMUnknown -> unknownServer + SPMUnprotected + | ipAddressProtected cfg destSrv -> pure False + | otherwise -> unknownServer + SPMNever -> pure False + unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) + sendViaProxy destSess = + withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do + liftClient SMP (clientServer smp) $ proxySMPMessage smp proxySess spKey_ senderId msgFlags msg + pure . Just $ protocolClientServer' smp + sendDirectly tSess = + withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> + liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg + +ipAddressProtected :: NetworkConfig -> ProtocolServer p -> Bool +ipAddressProtected NetworkConfig {socksProxy, hostMode} (ProtocolServer _ hosts _ _) = do + isJust socksProxy || (hostMode == HMOnion && any isOnionHost hosts) + where + isOnionHost = \case THOnionHost _ -> True; _ -> False withNtfClient :: AgentClient -> NtfServer -> EntityId -> ByteString -> (NtfClient -> ExceptT NtfClientError IO a) -> AM a withNtfClient c srv = withLogClient c (0, srv, Nothing) @@ -989,7 +1113,6 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do liftError (testErr TSSecureQueue) $ secureSMPQueue smp rpKey rcvId sKey liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp rpKey rcvId ok <- tcpTimeout (networkConfig cfg) `timeout` closeProtocolClient smp - incClientStat c userId smp "SMP_TEST" "OK" pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok Left e -> pure (Just $ testErr TSConnect e) where @@ -1104,7 +1227,7 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do logServer "-->" c srv "" "NEW" tSess <- liftIO $ mkTransportSession c userId srv connId QIK {rcvId, sndId, rcvPublicDhKey} <- - withClient c tSess "NEW" $ \smp -> createSMPQueue smp rKeys dhKey auth subMode + withClient c tSess "NEW" $ \smp -> createSMPQueue (connectedClient smp) rKeys dhKey auth subMode liftIO . logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] let rq = RcvQueue @@ -1193,7 +1316,7 @@ sendTSessionBatches statCmd statBatchSize toRQ action c qs = sendClientBatch (tSess@(userId, srv, _), qs') = tryAgentError' (getSMPServerClient c tSess) >>= \case Left e -> pure $ L.map ((,Left e) . toRQ) qs' - Right smp -> liftIO $ do + Right (SMPConnectedClient smp _) -> liftIO $ do logServer "-->" c srv (bshow (length qs') <> " queues") statCmd rs <- L.map agentError <$> action smp qs' statBatch @@ -1243,20 +1366,17 @@ logSecret :: ByteString -> ByteString logSecret bs = encode $ B.take 3 bs {-# INLINE logSecret #-} -sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM () -sendConfirmation c sq@SndQueue {sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = - withSMPClient_ c sq "SEND " $ \smp -> do - let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation - msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg - liftClient SMP (clientServer smp) $ sendSMPMessage smp Nothing sndId (SMP.MsgFlags {notification = True}) msg +sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM (Maybe SMPServer) +sendConfirmation c sq@SndQueue {userId, server, sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do + let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation + msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg + sendOrProxySMPMessage c userId server "" Nothing sndId (MsgFlags {notification = True}) msg sendConfirmation _ _ _ = throwError $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database" -sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM () +sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM (Maybe SMPServer) sendInvitation c userId (Compatible (SMPQueueInfo v SMPQueueAddress {smpServer, senderId, dhPublicKey})) (Compatible agentVersion) connReq connInfo = do - tSess <- liftIO $ mkTransportSession c userId smpServer senderId - withLogClient_ c tSess senderId "SEND " $ \smp -> do - msg <- mkInvitation - liftClient SMP (clientServer smp) $ sendSMPMessage smp Nothing senderId MsgFlags {notification = True} msg + msg <- mkInvitation + sendOrProxySMPMessage c userId smpServer "" Nothing senderId (MsgFlags {notification = True}) msg where mkInvitation :: AM ByteString -- this is only encrypted with per-queue E2E, not with double ratchet @@ -1340,12 +1460,11 @@ deleteQueue c rq@RcvQueue {rcvId, rcvPrivateKey} = do deleteQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] deleteQueues = sendTSessionBatches "DEL" 90 id $ sendBatch deleteSMPQueues -sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM () -sendAgentMessage c sq@SndQueue {sndId, sndPrivateKey} msgFlags agentMsg = - withSMPClient_ c sq "SEND " $ \smp -> do - let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg - msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg - liftClient SMP (clientServer smp) $ sendSMPMessage smp (Just sndPrivateKey) sndId msgFlags msg +sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM (Maybe SMPServer) +sendAgentMessage c sq@SndQueue {userId, server, sndId, sndPrivateKey} msgFlags agentMsg = do + let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg + msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg + sendOrProxySMPMessage c userId server "" (Just sndPrivateKey) sndId msgFlags msg agentNtfRegisterToken :: AgentClient -> NtfToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> AM (NtfTokenId, C.PublicKeyX25519) agentNtfRegisterToken c NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey = @@ -1606,9 +1725,13 @@ incStat AgentClient {agentStats} n k = do _ -> newTVar n >>= \v -> TM.insert k v agentStats incClientStat :: ProtocolServerClient v err msg => AgentClient -> UserId -> Client msg -> ByteString -> ByteString -> IO () -incClientStat c userId pc = incClientStatN c userId pc 1 +incClientStat c userId = incClientStat' c userId . protocolClient {-# INLINE incClientStat #-} +incClientStat' :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> ByteString -> ByteString -> IO () +incClientStat' c userId pc = incClientStatN c userId pc 1 +{-# INLINE incClientStat' #-} + incServerStat :: AgentClient -> UserId -> ProtocolServer p -> ByteString -> ByteString -> IO () incServerStat c userId ProtocolServer {host} cmd res = do threadDelay 100000 @@ -1616,7 +1739,7 @@ incServerStat c userId ProtocolServer {host} cmd res = do where statsKey = AgentStatsKey {userId, host = strEncode $ L.head host, clientTs = "", cmd, res} -incClientStatN :: ProtocolServerClient v err msg => AgentClient -> UserId -> Client msg -> Int -> ByteString -> ByteString -> IO () +incClientStatN :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> Int -> ByteString -> ByteString -> IO () incClientStatN c userId pc n cmd res = do atomically $ incStat c n statsKey where diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 4ee8d373f..602ffafe4 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -193,13 +193,13 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet ( InitialKeys (..), PQEncryption (..), - pattern PQEncOff, PQSupport, - pattern PQSupportOn, - pattern PQSupportOff, RcvE2ERatchetParams, RcvE2ERatchetParamsUri, - SndE2ERatchetParams + SndE2ERatchetParams, + pattern PQEncOff, + pattern PQSupportOff, + pattern PQSupportOn, ) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -213,14 +213,14 @@ import Simplex.Messaging.Protocol MsgId, NMsgMeta, ProtocolServer (..), + SMPClientVersion, SMPMsgMeta, SMPServer, SMPServerWithAuth, SndPublicAuthKey, SubscriptionMode, - SMPClientVersion, - VersionSMPC, VersionRangeSMPC, + VersionSMPC, initialSMPClientVersion, legacyEncodeServer, legacyServerP, @@ -398,7 +398,7 @@ data ACommand (p :: AParty) (e :: AEntity) where RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> ACommand Agent AEConn SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn - SENT :: AgentMsgId -> ACommand Agent AEConn + SENT :: AgentMsgId -> Maybe SMPServer -> ACommand Agent AEConn MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn @@ -517,7 +517,7 @@ aCommandTag = \case RSYNC {} -> RSYNC_ SEND {} -> SEND_ MID {} -> MID_ - SENT _ -> SENT_ + SENT {} -> SENT_ MERR {} -> MERR_ MERRS {} -> MERRS_ MSG {} -> MSG_ @@ -913,7 +913,7 @@ instance Encoding AgentMsgEnvelope where -- AgentRatchetInfo is not encrypted with double ratchet, but with per-queue E2E encryption data AgentMessage = -- used by the initiating party when confirming reply queue - AgentConnInfo ConnInfo + AgentConnInfo ConnInfo | -- AgentConnInfoReply is used by accepting party in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation. -- It made removed REPLY message unnecessary. AgentConnInfoReply (NonEmpty SMPQueueInfo) ConnInfo @@ -1387,9 +1387,9 @@ deriving instance Show (ConnectionRequestUri m) data AConnectionRequestUri = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequestUri m) instance Eq AConnectionRequestUri where - ACR m cr == ACR m' cr' = case testEquality m m' of - Just Refl -> cr == cr' - _ -> False + ACR m cr == ACR m' cr' = case testEquality m m' of + Just Refl -> cr == cr' + _ -> False deriving instance Show AConnectionRequestUri @@ -1793,7 +1793,7 @@ commandP binaryP = SWITCH_ -> s (SWITCH <$> strP_ <*> strP_ <*> strP) RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP) MID_ -> s (MID <$> A.decimal <*> _strP) - SENT_ -> s (SENT <$> A.decimal) + SENT_ -> s (SENT <$> A.decimal <*> _strP) MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP) MERRS_ -> s (MERRS <$> strP_ <*> strP) MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP) @@ -1856,7 +1856,7 @@ serializeCommand = \case RSYNC rrState cryptoErr cstats -> s (RSYNC_, rrState, cryptoErr, cstats) SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody] MID mId pqEnc -> s (MID_, mId, pqEnc) - SENT mId -> s (SENT_, mId) + SENT mId proxySrv_ -> s (SENT_, mId, proxySrv_) MERR mId e -> s (MERR_, mId, e) MERRS mIds e -> s (MERRS_, mIds, e) MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody] diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 8d4db29d9..ebcfc79b2 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -30,9 +30,11 @@ module Simplex.Messaging.Client TransportSession, ProtocolClient (thParams, sessionTs), SMPClient, + ProxiedRelay (..), getProtocolClient, closeProtocolClient, protocolClientServer, + protocolClientServer', transportHost', transportSession', @@ -54,7 +56,7 @@ module Simplex.Messaging.Client suspendSMPQueue, deleteSMPQueue, deleteSMPQueues, - createSMPProxySession, + connectSMPProxiedRelay, proxySMPMessage, forwardSMPMessage, sendProtocolCommand, @@ -65,6 +67,8 @@ module Simplex.Messaging.Client ProtocolClientConfig (..), NetworkConfig (..), TransportSessionMode (..), + HostMode (..), + SMPProxyMode (..), defaultClientConfig, defaultSMPClientConfig, defaultNetworkConfig, @@ -207,6 +211,8 @@ data NetworkConfig = NetworkConfig requiredHostMode :: Bool, -- | transport sessions are created per user or per entity sessionMode :: TransportSessionMode, + -- | SMP proxy mode + smpProxyMode :: SMPProxyMode, -- | timeout for the initial client TCP/TLS connection (microseconds) tcpConnectTimeout :: Int, -- | timeout of protocol commands (microseconds) @@ -226,6 +232,14 @@ data NetworkConfig = NetworkConfig data TransportSessionMode = TSMUser | TSMEntity deriving (Eq, Show) +-- SMP proxy mode for sending messages +data SMPProxyMode + = SPMAlways + | SPMUnknown -- use with unknown relays + | SPMUnprotected -- use with unknown relays when IP address is not protected (i.e., when neither SOCKS proxy nor .onion address is used) + | SPMNever + deriving (Eq, Show) + defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig @@ -233,6 +247,7 @@ defaultNetworkConfig = hostMode = HMOnionViaSocks, requiredHostMode = False, sessionMode = TSMUser, + smpProxyMode = SPMNever, tcpConnectTimeout = 20_000_000, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 5_000, @@ -302,10 +317,14 @@ chooseTransportHost NetworkConfig {socksProxy, hostMode, requiredHostMode} hosts publicHost = find (not . isOnionHost) hosts protocolClientServer :: ProtocolTypeI (ProtoType msg) => ProtocolClient v err msg -> String -protocolClientServer = B.unpack . strEncode . snd3 . transportSession . client_ +protocolClientServer = B.unpack . strEncode . protocolClientServer' +{-# INLINE protocolClientServer #-} + +protocolClientServer' :: ProtocolClient v err msg -> ProtoServer msg +protocolClientServer' = snd3 . transportSession . client_ where snd3 (_, s, _) = s -{-# INLINE protocolClientServer #-} +{-# INLINE protocolClientServer' #-} transportHost' :: ProtocolClient v err msg -> TransportHost transportHost' = transportHost . client_ @@ -650,14 +669,13 @@ deleteSMPQueues = okSMPCommands DEL -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg -createSMPProxySession :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO (SessionId, VersionSMP, C.PublicKeyX25519) -createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = +connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay +connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case - -- XXX: rfc says sessionId should be in the entityId of response PKEY sId vr (chain, key) -> do case supportedClientSMPRelayVRange `compatibleVersion` vr of Nothing -> throwE PCEIncompatibleHost -- TODO different error - Just (Compatible v) -> liftEitherWith x509Error $ (sId,v,) <$> validateRelay chain key + Just (Compatible v) -> liftEitherWith x509Error $ ProxiedRelay sId v <$> validateRelay chain key r -> throwE . PCEUnexpectedResponse $ bshow r where x509Error :: String -> SMPClientError @@ -672,6 +690,12 @@ createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyA pubKey <- C.verifyX509 serverKey exact C.x509ToPublic (pubKey, []) >>= C.pubKey +data ProxiedRelay = ProxiedRelay + { prSessionId :: SessionId, + prVersion :: VersionSMP, + prServerKey :: C.PublicKeyX25519 + } + -- consider how to process slow responses - is it handled somehow locally or delegated to the caller -- this method is used in the client -- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender @@ -679,9 +703,7 @@ createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyA proxySMPMessage :: SMPClient -> -- proxy session from PKEY - SessionId -> - VersionSMP -> - C.PublicKeyX25519 -> + ProxiedRelay -> -- message to deliver Maybe SndPrivateAuthKey -> SenderId -> @@ -689,7 +711,7 @@ proxySMPMessage :: MsgBody -> ExceptT SMPClientError IO () -- TODO use version -proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} sessionId _v serverKey spKey sId flags msg = do +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId _v serverKey) spKey sId flags msg = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams serverThParams = proxyThParams {sessionId, thAuth = serverThAuth} @@ -867,4 +889,6 @@ $(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode) $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) + $(J.deriveJSON defaultJSON ''NetworkConfig) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 8083ef988..b14917c18 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -13,7 +13,7 @@ module AgentTests (agentTests) where import AgentTests.ConnectionRequestTests import AgentTests.DoubleRatchetTests (doubleRatchetTests) -import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg') +import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg', pattern SENT) import AgentTests.MigrationTests (migrationTests) import AgentTests.NotificationTests (notificationTests) import AgentTests.SQLiteTests (storeTests) @@ -27,7 +27,7 @@ import GHC.Stack (withFrozenCallStack) import Network.HTTP.Types (urlEncode) import SMPAgentClient import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) -import Simplex.Messaging.Agent.Protocol hiding (MID, CONF, INFO, REQ) +import Simplex.Messaging.Agent.Protocol hiding (MID, CONF, INFO, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOn, pattern IKPQOff, pattern PQEncOn, pattern PQSupportOn, pattern PQSupportOff) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -437,8 +437,8 @@ testServerConnectionAfterError t _ = do bob #: ("1", "alice", "SUB") =#> \("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT alice #: ("1", "bob", "SUB") =#> \("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT withServer $ do - alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False - alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False + alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False + alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False bob #: ("2", "alice", "ACK 4") #> ("2", "alice", OK) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 301be97b4..13822650f 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -45,6 +45,7 @@ module AgentTests.FunctionalAPITests pattern REQ, pattern Msg, pattern Msg', + pattern SENT, agentCfgV7, ) where @@ -70,17 +71,17 @@ import Data.Word (Word16) import qualified Database.SQLite.Simple as SQL import GHC.Stack (withFrozenCallStack) import SMPAgentClient -import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) +import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerProxy, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import qualified Simplex.Messaging.Agent as A import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore) -import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -171,6 +172,9 @@ pattern MsgErr msgId err msgBody <- MSG MsgMeta {recipient = (msgId, _), integri pattern MsgErr' :: AgentMsgId -> MsgErrorType -> PQEncryption -> MsgBody -> ACommand 'Agent 'AEConn pattern MsgErr' msgId err pq msgBody <- MSG MsgMeta {recipient = (msgId, _), integrity = MsgError err, pqEncryption = pq} _ msgBody +pattern SENT :: AgentMsgId -> ACommand 'Agent 'AEConn +pattern SENT msgId = A.SENT msgId Nothing + pattern Rcvd :: AgentMsgId -> ACommand 'Agent 'AEConn pattern Rcvd agentMsgId <- RCVD MsgMeta {integrity = MsgOk} [MsgReceipt {agentMsgId, msgRcptStatus = MROk}] @@ -448,26 +452,28 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = let v = basicAuthSMPVersion in allowNew && (isNothing srvAuth || (srvVersion >= v && clntVersion >= v && srvAuth == clntAuth)) -testMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn - it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn - it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn - it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff - it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff - it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False + it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False + it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False + it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff False + it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False + it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False -testRatchetMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testRatchetMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn - it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn - it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn - it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff - it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff - it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False + it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False + it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False + it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff False + it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff False + it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff False testServerMatrix2 :: ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do @@ -475,10 +481,14 @@ testServerMatrix2 t runTest = do it "2 servers" $ withSmpServer t . withSmpServerOn t testPort2 $ runTest initAgentServers2 runTestCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () -runTestCfg2 aCfg bCfg baseMsgId runTest = - withAgentClientsCfg2 aCfg bCfg $ \a b -> runTest a b baseMsgId +runTestCfg2 aCfg bCfg = runTestCfgServers2 aCfg bCfg initAgentServers {-# INLINE runTestCfg2 #-} +runTestCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () +runTestCfgServers2 aCfg bCfg servers baseMsgId runTest = + withAgentClientsCfgServers2 aCfg bCfg servers $ \a b -> runTest a b baseMsgId +{-# INLINE runTestCfgServers2 #-} + withAgentClientsCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> (HasCallStack => AgentClient -> AgentClient -> IO ()) -> IO () withAgentClientsCfgServers2 aCfg bCfg servers runTest = withAgent 1 aCfg servers testDB $ \a -> @@ -499,8 +509,8 @@ withAgentClients3 runTest = withAgent 3 agentCfg initAgentServers testDB3 $ \c -> runTest a b c -runAgentClientTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientTest pqSupport alice@AgentClient {} bob baseId = +runAgentClientTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientTest pqSupport viaProxy alice@AgentClient {} bob baseId = runRight_ $ do (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (IKNoPQ pqSupport) SMSubscribe aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe @@ -512,18 +522,19 @@ runAgentClientTest pqSupport alice@AgentClient {} bob baseId = get bob ##> ("", aliceId, A.INFO pqSupport "alice's connInfo") get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let proxySrv = if viaProxy then Just testSMPServer else Nothing 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT $ baseId + 1) + get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv) 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" - get alice ##> ("", bobId, SENT $ baseId + 2) + get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv) get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" - get bob ##> ("", aliceId, SENT $ baseId + 3) + get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv) 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" - get bob ##> ("", aliceId, SENT $ baseId + 4) + get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv) get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False @@ -627,8 +638,8 @@ testAgentClient3 = get c =##> \case ("", connId, Msg "c5") -> connId == aIdForC; _ -> False ackMessage c aIdForC 5 Nothing -runAgentClientContactTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientContactTest pqSupport alice bob baseId = +runAgentClientContactTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientContactTest pqSupport viaProxy alice bob baseId = runRight_ $ do (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing (IKNoPQ pqSupport) SMSubscribe aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe @@ -643,18 +654,19 @@ runAgentClientContactTest pqSupport alice bob baseId = get alice ##> ("", bobId, A.CON pqEnc) get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let proxySrv = if viaProxy then Just testSMPServer else Nothing 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT $ baseId + 1) + get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv) 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" - get alice ##> ("", bobId, SENT $ baseId + 2) + get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv) get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" - get bob ##> ("", aliceId, SENT $ baseId + 3) + get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv) 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" - get bob ##> ("", aliceId, SENT $ baseId + 4) + get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv) get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False @@ -1493,9 +1505,9 @@ testSuspendingAgentCompleteSending t = withAgentClients2 $ \a b -> do liftIO $ suspendAgent b 5000000 withSmpServerStoreLogOn t testPort $ \_ -> runRight_ @AgentErrorType $ do - pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC _ (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False ("", "", SUSPENDED) <- nGet b pGet a =##> \case ("", c, APC _ (Msg "hello too")) -> c == bId; ("", "", APC _ UP {}) -> True; _ -> False diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 2c1045791..b4f6ec3ee 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -6,8 +6,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests.NotificationTests where @@ -17,10 +17,6 @@ import AgentTests.FunctionalAPITests createConnection, exchangeGreetingsMsgId, get, - withAgent, - withAgentClients2, - withAgentClientsCfgServers2, - withAgentClients3, joinConnection, makeConnection, nGet, @@ -29,13 +25,18 @@ import AgentTests.FunctionalAPITests sendMessage, switchComplete, testServerMatrix2, + withAgent, + withAgentClients2, + withAgentClients3, withAgentClientsCfg2, + withAgentClientsCfgServers2, (##>), (=##>), pattern CON, pattern CONF, pattern INFO, pattern Msg, + pattern SENT, ) import Control.Concurrent (ThreadId, killThread, threadDelay) import Control.Monad @@ -55,12 +56,12 @@ import SMPClient (cfg, cfgV7, testPort, testPort2, testStoreLogFile2, withSmpSer import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore') import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers) -import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT) import Simplex.Messaging.Agent.Store.SQLite (getSavedNtfToken) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Protocol +import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Types (NtfToken (..)) import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..)) @@ -151,7 +152,8 @@ testNtfMatrix t runTest = do it "next servers: SMP v7, NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfg runTest it "curr servers: SMP v6, NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfg agentCfg agentCfg runTest skip "this case cannot be supported - see RFC" $ - it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest + it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ + runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest -- servers can be migrated in any order it "servers: next SMP v7, curr NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfg agentCfg agentCfg runTest it "servers: curr SMP v6, next NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfgV2 agentCfg agentCfg runTest @@ -258,7 +260,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do atomically $ readTBQueue apnsQ liftIO $ sendApnsResponse APNSRespOk pure ntfData - -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server + -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server threadDelay 1000000 withAgent 2 agentCfg initAgentServers testDB $ \a' -> -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 59370e654..d509042f0 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -20,7 +20,8 @@ import qualified Database.SQLite.Simple as SQL import Network.Socket (ServiceName) import NtfClient (ntfTestPort) import SMPClient - ( serverBracket, + ( proxyVRange, + serverBracket, testKeyHash, testPort, testPort2, @@ -34,7 +35,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Server (runSMPAgentBlocking) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultSMPClientConfig, defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyMode, chooseTransportHost, defaultSMPClientConfig, defaultNetworkConfig) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth) @@ -198,6 +199,9 @@ initAgentServers = initAgentServers2 :: InitialAgentServers initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]} +initAgentServersProxy :: SMPProxyMode -> InitialAgentServers +initAgentServersProxy smpProxyMode = initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode}} + agentCfg :: AgentConfig agentCfg = defaultAgentConfig @@ -217,6 +221,9 @@ agentCfg = where networkConfig = defaultNetworkConfig {tcpConnectTimeout = 3_000_000, tcpTimeout = 2_000_000} +agentProxyCfg :: AgentConfig +agentProxyCfg = agentCfg {smpCfg = (smpCfg agentCfg) {serverVRange = proxyVRange}} + fastRetryInterval :: RetryInterval fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index ad4d00266..e27970608 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -123,9 +123,12 @@ proxyCfg = cfgV7 { allowSMPProxy = True, smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, agreeSecret = True}} + smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True}} } +proxyVRange :: VersionRangeSMP +proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion + withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} @@ -163,6 +166,9 @@ withSmpServer t = withSmpServerOn t testPort withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const +withSmpServerProxy :: HasCallStack => ATransport -> IO a -> IO a +withSmpServerProxy t = withSmpServerConfigOn t proxyCfg testPort . const + runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c 'TClient -> IO a) -> IO a runSmpTest test = withSmpServer (transport @c) $ testSMPClient test diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 2a33ec055..ae2a05e4d 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -1,8 +1,11 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -10,15 +13,23 @@ module SMPProxyTests where -import AgentTests.FunctionalAPITests (runRight_) +import AgentTests.FunctionalAPITests import Data.ByteString.Char8 (ByteString) -import SMPAgentClient (testSMPServer, testSMPServer2) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import SMPAgentClient import SMPClient -import qualified SMPClient as SMP import ServerTests (decryptMsgV3, sendRecv) +import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) +import qualified Simplex.Messaging.Agent as A +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..)) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ) +import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Protocol +import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOn) +import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Version (mkVersionRange) @@ -37,49 +48,52 @@ smpProxyTests = do xit "no SMP service at host/port" todo xit "bad SMP fingerprint" todo xit "batching proxy requests" todo - describe "forwarding requests" $ do - describe "deliver message via SMP proxy" $ do + describe "deliver message via SMP proxy" $ do + let srv1 = SMPServer testHost testPort testKeyHash + srv2 = SMPServer testHost testPort2 testKeyHash + describe "client API" $ do let maxLen = maxMessageLength sendingProxySMPVersion - it "same server" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = proxyServ + describe "one server" $ do + it "deliver via proxy" . oneServer $ do + deliverMessageViaProxy srv1 srv1 C.SEd448 "hello 1" "hello 2" + describe "two servers" $ do + let proxyServ = srv1 + relayServ = srv2 + (msg1, msg2) <- runIO $ do + g <- C.newRandom + atomically $ (,) <$> C.randomBytes maxLen g <*> C.randomBytes maxLen g + it "deliver via proxy" . twoServersFirstProxy $ deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" - it "different servers" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" - it "max message size, Ed448 keys" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - g <- C.newRandom - msg <- atomically $ C.randomBytes maxLen g - msg' <- atomically $ C.randomBytes maxLen g - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SEd448 msg msg' - it "max message size, Ed25519 keys" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - g <- C.newRandom - msg <- atomically $ C.randomBytes maxLen g - msg' <- atomically $ C.randomBytes maxLen g - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg msg' - it "max message size, X25519 keys" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - g <- C.newRandom - msg <- atomically $ C.randomBytes maxLen g - msg' <- atomically $ C.randomBytes maxLen g - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SX25519 msg msg' - xit "sender-proxy-relay-recipient works" todo - xit "similar timing for proxied and direct sends" todo + it "max message size, Ed448 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SEd448 msg1 msg2 + it "max message size, Ed25519 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg1 msg2 + it "max message size, X25519 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SX25519 msg1 msg2 + describe "agent API" $ do + describe "one server" $ do + it "always via proxy" . oneServer $ + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + it "without proxy" . oneServer $ + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" + describe "two servers" $ do + it "always via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + it "both via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" + it "first via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + it "without proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + it "first via proxy for unknown" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + where + oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg testPort . const + twoServers = twoServers_ proxyCfg proxyCfg + twoServersFirstProxy = twoServers_ proxyCfg cfgV7 + twoServers_ cfg1 cfg2 runTest = + withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfg2 testPort2 $ const runTest deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> ByteString -> ByteString -> IO () deliverMessageViaProxy proxyServ relayServ alg msg msg' = do @@ -97,39 +111,74 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv -- get proxy session - (sessId, v, relayKey) <- createSMPProxySession pc relayServ (Just "correct") + sess <- connectSMPProxiedRelay pc relayServ (Just "correct") -- send via proxy to unsecured queue - proxySMPMessage pc sessId v relayKey Nothing sndId noMsgFlags msg + proxySMPMessage pc sess Nothing sndId noMsgFlags msg -- receive 1 - (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ + (_tSess, _v, _sid, _ety, SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId encBody `shouldBe` Right msg ackSMPMessage rc rPriv rcvId msgId -- secure queue (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g secureSMPQueue rc rPriv rcvId sPub -- send via proxy to secured queue - proxySMPMessage pc sessId v relayKey (Just sPriv) sndId noMsgFlags msg' + proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' -- receive 2 - (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ + (_tSess, _v, _sid, _ety, SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId' encBody' `shouldBe` Right msg' ackSMPMessage rc rPriv rcvId msgId' -proxyVRange :: VersionRangeSMP -proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion +agentDeliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => (NonEmpty SMPServer, SMPProxyMode, Bool) -> (NonEmpty SMPServer, SMPProxyMode, Bool) -> C.SAlgorithm a -> ByteString -> ByteString -> IO () +agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, bViaProxy) alg msg1 msg2 = + withAgent 1 aCfg (servers aTestCfg) testDB $ \alice -> + withAgent 2 aCfg (servers bTestCfg) testDB2 $ \bob -> runRight_ $ do + (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" PQSupportOn SMSubscribe + ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice + liftIO $ pqSup' `shouldBe` PQSupportOn + allowConnection alice bobId confId "alice's connInfo" + let pqEnc = CR.PQEncOn + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo") + get bob ##> ("", aliceId, A.CON pqEnc) + -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let aProxySrv = if aViaProxy then Just $ L.head aSrvs else Nothing + 1 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1 + get alice ##> ("", bobId, A.SENT (baseId + 1) aProxySrv) + 2 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg2 + get alice ##> ("", bobId, A.SENT (baseId + 2) aProxySrv) + get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage bob aliceId (baseId + 1) Nothing + get bob =##> \case ("", c, Msg' _ pq msg2') -> c == aliceId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage bob aliceId (baseId + 2) Nothing + let bProxySrv = if bViaProxy then Just $ L.head bSrvs else Nothing + 3 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg1 + get bob ##> ("", aliceId, A.SENT (baseId + 3) bProxySrv) + 4 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2 + get bob ##> ("", aliceId, A.SENT (baseId + 4) bProxySrv) + get alice =##> \case ("", c, Msg' _ pq msg1') -> c == bobId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage alice bobId (baseId + 3) Nothing + get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage alice bobId (baseId + 4) Nothing + where + baseId = 3 + msgId = subtract baseId . fst + aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} + servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode) {smp = userServers $ L.map noAuthSrv srvs} testNoProxy :: IO () testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) - reply `shouldBe` Right (ERR AUTH) + reply `shouldBe` Right (SMP.ERR SMP.AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") - reply `shouldBe` Right (ERR AUTH) + reply `shouldBe` Right (SMP.ERR SMP.AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} From c85f6a2f0e6c12272f314e8c4cdf0b313deda4ea Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Tue, 7 May 2024 00:00:42 +0300 Subject: [PATCH 07/13] proxy: reporting errors (#1108) * smp-proxy: iron out errors * treat proxy timeouts as temporary * update errors * proxy errors (missing encoding) * update * enable tests * update * update * fix * fix * simplify * test --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Agent.hs | 4 +- src/Simplex/Messaging/Agent/Client.hs | 25 ++++- src/Simplex/Messaging/Agent/Protocol.hs | 28 ++--- src/Simplex/Messaging/Client.hs | 121 +++++++++++++++------- src/Simplex/Messaging/Protocol.hs | 129 ++++++++++++++++-------- src/Simplex/Messaging/Server.hs | 59 +++++------ tests/CoreTests/ProtocolErrorTests.hs | 45 ++++++--- tests/SMPProxyTests.hs | 8 +- 8 files changed, 266 insertions(+), 153 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3b68667e4..3a0e13157 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -2250,9 +2250,9 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, | otherwise -> ignored _ -> ignored ignored = pure "END from disconnected client - ignored" - _ -> do + r -> do logServer "<--" c srv rId $ "unexpected: " <> bshow cmd - notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED + notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED $ take 32 $ show r where notify :: forall e m. MonadIO m => AEntityI e => ACommand 'Agent e -> m () notify = atomically . notify' diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index c29e35499..35015af5a 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1032,8 +1032,15 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) sendViaProxy destSess = withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do - liftClient SMP (clientServer smp) $ proxySMPMessage smp proxySess spKey_ senderId msgFlags msg - pure . Just $ protocolClientServer' smp + liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case + Right () -> pure . Just $ protocolClientServer' smp + Left proxyErr -> + throwError + PROXY + { proxyServer = protocolClientServer smp, + relayServer = B.unpack $ strEncode destSrv, + proxyErr + } sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg @@ -1066,7 +1073,7 @@ protocolClientError :: (Show err, Encoding err) => (err -> AgentErrorType) -> Ho protocolClientError protocolError_ host = \case PCEProtocolError e -> protocolError_ e PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e - PCEUnexpectedResponse _ -> BROKER host UNEXPECTED + PCEUnexpectedResponse r -> BROKER host $ UNEXPECTED $ take 32 $ show r PCEResponseTimeout -> BROKER host TIMEOUT PCENetworkError -> BROKER host NETWORK PCEIncompatibleHost -> BROKER host HOST @@ -1263,15 +1270,23 @@ processSubResult c rq r = do temporaryAgentError :: AgentErrorType -> Bool temporaryAgentError = \case - BROKER _ NETWORK -> True - BROKER _ TIMEOUT -> True + BROKER _ e -> tempBrokerError e + SMP (SMP.PROXY (SMP.BROKER e)) -> tempBrokerError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> tempBrokerError e INACTIVE -> True _ -> False + where + tempBrokerError = \case + NETWORK -> True + TIMEOUT -> True + _ -> False {-# INLINE temporaryAgentError #-} temporaryOrHostError :: AgentErrorType -> Bool temporaryOrHostError = \case BROKER _ HOST -> True + SMP (SMP.PROXY (SMP.BROKER HOST)) -> True + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER HOST))) -> True e -> temporaryAgentError e {-# INLINE temporaryOrHostError #-} diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 895a8ddc1..61ab2400b 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -189,6 +189,7 @@ import Simplex.FileTransfer.Description import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Transport (XFTPErrorType) import Simplex.Messaging.Agent.QueryString +import Simplex.Messaging.Client (ProxyClientError) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet ( InitialKeys (..), @@ -206,6 +207,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol ( AProtocolType, + BrokerErrorType (..), EntityId, ErrorType, MsgBody, @@ -233,7 +235,7 @@ import Simplex.Messaging.Protocol ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme -import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP) +import Simplex.Messaging.Transport (Transport (..), serializeTransportError, transportErrorP) import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -1474,6 +1476,8 @@ data AgentErrorType NTF {ntfErr :: ErrorType} | -- | XFTP protocol errors forwarded to agent clients XFTP {xftpErr :: XFTPErrorType} + | -- | SMP proxy errors + PROXY {proxyServer :: String, relayServer :: String, proxyErr :: ProxyClientError} | -- | XRCP protocol errors forwarded to agent clients RCP {rcpErr :: RCErrorType} | -- | SMP server errors @@ -1516,22 +1520,6 @@ data ConnectionErrorType NOT_AVAILABLE deriving (Eq, Read, Show, Exception) --- | SMP server errors. -data BrokerErrorType - = -- | invalid server response (failed to parse) - RESPONSE {smpErr :: String} - | -- | unexpected response - UNEXPECTED - | -- | network error - NETWORK - | -- | no compatible server host (e.g. onion when public is required, or vice versa) - HOST - | -- | handshake or other transport error - TRANSPORT {transportErr :: TransportError} - | -- | command response timeout - TIMEOUT - deriving (Eq, Read, Show, Exception) - -- | Errors of another SMP agent. data SMPAgentError = -- | client or agent message that failed to parse @@ -1587,8 +1575,10 @@ instance StrEncoding AgentErrorType where <|> "SMP " *> (SMP <$> strP) <|> "NTF " *> (NTF <$> strP) <|> "XFTP " *> (XFTP <$> strP) + <|> "PROXY " *> (PROXY <$> textP <* A.space <*> textP <*> _strP) <|> "RCP " *> (RCP <$> strP) <|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP)) + <|> "BROKER " *> (BROKER <$> textP <* " UNEXPECTED " <*> (UNEXPECTED <$> textP)) <|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP)) <|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1) <|> "AGENT CRYPTO " *> (AGENT . A_CRYPTO <$> parseRead A.takeByteString) @@ -1605,8 +1595,10 @@ instance StrEncoding AgentErrorType where SMP e -> "SMP " <> strEncode e NTF e -> "NTF " <> strEncode e XFTP e -> "XFTP " <> strEncode e + PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e] RCP e -> "RCP " <> strEncode e BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e + BROKER srv (UNEXPECTED e) -> "BROKER " <> text srv <> " UNEXPECTED " <> text e BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e AGENT (A_CRYPTO e) -> "AGENT CRYPTO " <> bshow e @@ -1977,8 +1969,6 @@ $(J.deriveJSON (sumTypeJSON id) ''CommandErrorType) $(J.deriveJSON (sumTypeJSON id) ''ConnectionErrorType) -$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) - $(J.deriveJSON (sumTypeJSON id) ''AgentCryptoError) $(J.deriveJSON (sumTypeJSON id) ''SMPAgentError) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 2936e3841..8a1349089 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -64,6 +64,7 @@ module Simplex.Messaging.Client -- * Supporting types and client configuration ProtocolClientError (..), SMPClientError, + ProxyClientError (..), ProtocolClientConfig (..), NetworkConfig (..), TransportSessionMode (..), @@ -97,6 +98,7 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J +import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) @@ -534,16 +536,17 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} +-- converts error of client running on proxy to the error sent to client connected to proxy smpProxyError :: SMPClientError -> ErrorType smpProxyError = \case - PCEProtocolError et -> PROXY (PROTOCOL et) - PCEResponseError et -> PROXY (RESPONSE et) - PCEUnexpectedResponse bs -> PROXY (UNEXPECTED $ B.unpack $ B.take 32 bs) - PCEResponseTimeout -> PROXY TIMEOUT - PCENetworkError -> PROXY NETWORK - PCEIncompatibleHost -> PROXY BAD_HOST - PCETransportError t -> PROXY (TRANSPORT t) - PCECryptoError _ -> INTERNAL + PCEProtocolError e -> PROXY $ PROTOCOL e + PCEResponseError e -> PROXY $ BROKER $ RESPONSE $ B.unpack $ strEncode e + PCEUnexpectedResponse s -> PROXY $ BROKER $ UNEXPECTED $ B.unpack $ B.take 32 s + PCEResponseTimeout -> PROXY $ BROKER TIMEOUT + PCENetworkError -> PROXY $ BROKER NETWORK + PCEIncompatibleHost -> PROXY $ BROKER HOST + PCETransportError t -> PROXY $ BROKER $ TRANSPORT t + PCECryptoError _ -> CRYPTO PCEIOError _ -> INTERNAL -- | Create a new SMP queue. @@ -699,21 +702,18 @@ deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO deleteSMPQueues = okSMPCommands DEL {-# INLINE deleteSMPQueues #-} --- TODO picture - -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case - PKEY sId vr (chain, key) -> do + PKEY sId vr (chain, key) -> case supportedClientSMPRelayVRange `compatibleVersion` vr of - Nothing -> throwE PCEIncompatibleHost -- TODO different error - Just (Compatible v) -> liftEitherWith x509Error $ ProxiedRelay sId v <$> validateRelay chain key + Nothing -> throwE $ relayErr VERSION + Just (Compatible v) -> liftEitherWith (const $ relayErr IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key r -> throwE . PCEUnexpectedResponse $ bshow r where - x509Error :: String -> SMPClientError - x509Error _msg = PCEResponseError $ error "TODO: x509 error" -- TODO different error + relayErr = PCEProtocolError . PROXY . BROKER . TRANSPORT . TEHandshake validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 validateRelay (X.CertificateChain cert) exact = do serverKey <- case cert of @@ -730,10 +730,53 @@ data ProxiedRelay = ProxiedRelay prServerKey :: C.PublicKeyX25519 } +data ProxyClientError + = -- | protocol error response from proxy + ProxyProtocolError ErrorType + | -- | unexpexted response + ProxyUnexpectedResponse String + | -- | error between proxy and server + ProxyResponseError ErrorType + deriving (Eq, Show, Exception) + +instance StrEncoding ProxyClientError where + strEncode = \case + ProxyProtocolError e -> "PROTOCOL " <> strEncode e + ProxyUnexpectedResponse s -> "UNEXPECTED " <> B.pack s + ProxyResponseError e -> "SYNTAX " <> strEncode e + strP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> ProxyProtocolError <$> _strP + "UNEXPECTED" -> ProxyUnexpectedResponse . B.unpack <$> (A.space *> A.takeByteString) + "SYNTAX" -> ProxyResponseError <$> _strP + _ -> fail "bad ProxyClientError" + -- consider how to process slow responses - is it handled somehow locally or delegated to the caller -- this method is used in the client -- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender -- receives PRES :: EncResponse -> BrokerMsg -- proxy to client + +-- When client sends message via proxy, there may be one successful scenario and 9 error scenarios +-- as shown below (WTF stands for unexpected response, ??? for response that failed to parse). +-- client proxy relay proxy client +-- 0) PFWD(SEND) -> RFWD -> RRES -> PRES(OK) -> ok +-- 1) PFWD(SEND) -> RFWD -> RRES -> PRES(ERR) -> PCEProtocolError - business logic error for client +-- 2) PFWD(SEND) -> RFWD -> RRES -> PRES(WTF) -> PCEUnexpectedReponse - relay/client protocol logic error +-- 3) PFWD(SEND) -> RFWD -> RRES -> PRES(???) -> PCEResponseError - relay/client syntax error +-- 4) PFWD(SEND) -> RFWD -> ERR -> ERR PROXY PROTOCOL -> ProxyProtocolError - proxy/relay business logic error +-- 5) PFWD(SEND) -> RFWD -> WTF -> ERR PROXY $ BROKER (UNEXPECTED s) -> ProxyProtocolError - proxy/relay protocol logic +-- 6) PFWD(SEND) -> RFWD -> ??? -> ERR PROXY $ BROKER (RESPONSE s) -> ProxyProtocolError - - proxy/relay syntax +-- 7) PFWD(SEND) -> ERR -> ProxyProtocolError - client/proxy business logic +-- 8) PFWD(SEND) -> WTF -> ProxyUnexpectedResponse - client/proxy protocol logic +-- 9) PFWD(SEND) -> ??? -> ProxyResponseError - client/proxy syntax +-- +-- We report as proxySMPMessage error (ExceptT error) the errors of two kinds: +-- - protocol errors from the destination relay wrapped in PRES - to simplify processing of AUTH and QUOTA errors, in this case proxy is "transparent" for such errors (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other response/transport/connection errors from the client connected to proxy itself +-- Other errors are reported in the function result as `Either ProxiedRelayError ()`, including +-- - protocol errors from the client connected to proxy in ProxyClientError (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other errors from the client running on proxy and connected to relay in PREProxiedRelayError + proxySMPMessage :: SMPClient -> -- proxy session from PKEY @@ -743,7 +786,7 @@ proxySMPMessage :: SenderId -> MsgFlags -> MsgBody -> - ExceptT SMPClientError IO () + ExceptT SMPClientError IO (Either ProxyClientError ()) -- TODO use version proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId _v serverKey) spKey sId flags msg = do -- prepare params @@ -756,24 +799,32 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg) auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of - [] -> throwE $ PCETransportError TELargeMsg -- some other error. Internal? - TBError e _ : _ -> throwE $ PCETransportError e -- large message error? + [] -> throwE $ PCETransportError TELargeMsg + TBError e _ : _ -> throwE $ PCETransportError e TBTransmission s _ : _ -> pure s TBTransmissions s _ _ : _ -> pure s et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength - sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et)) >>= \case - -- TODO support PKEY + resend? - PRES (EncResponse er) -> do - t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er - case tParse proxyThParams t' of - t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of - (_auth, _signed, (_c, _e, r)) -> case r of -- TODO: verify - Left e -> throwE $ PCEResponseError e - Right OK -> pure () - Right (ERR e) -> throwE $ PCEProtocolError e - Right u -> throwE . PCEUnexpectedResponse $ bshow u -- possibly differentiate unexpected response from server/proxy - _ -> throwE $ PCETransportError TEBadBlock - r -> throwE . PCEUnexpectedResponse $ bshow r -- from proxy + -- proxy interaction errors are wrapped + tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et))) >>= \case + Right r -> case r of + PRES (EncResponse er) -> do + -- server interaction errors are thrown directly + t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er + case tParse proxyThParams t' of + t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of + (_auth, _signed, (_c, _e, cmd)) -> case cmd of + Right OK -> pure $ Right () + Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay + Right e -> throwE $ PCEUnexpectedResponse $ B.take 32 $ bshow e + Left e -> throwE $ PCEResponseError e + _ -> throwE $ PCETransportError TEBadBlock + ERR e -> pure . Left $ ProxyProtocolError e -- this will not happen, this error is returned via Left + _ -> pure . Left $ ProxyUnexpectedResponse $ take 32 $ show r + Left e -> case e of + PCEProtocolError e' -> pure . Left $ ProxyProtocolError e' + PCEUnexpectedResponse r -> pure . Left $ ProxyUnexpectedResponse $ B.unpack r + PCEResponseError e' -> pure . Left $ ProxyResponseError e' + _ -> throwE e -- this method is used in the proxy -- sends RFWD :: EncFwdTransmission -> Command Sender @@ -783,8 +834,8 @@ forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of - Nothing -> throwError $ PCEProtocolError INTERNAL -- different error - proxy didn't pass key? - Just THAuthClient {sessSecret} -> maybe (throwError $ PCEProtocolError INTERNAL) pure sessSecret + Nothing -> throwError $ PCETransportError TENoServerAuth + Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} @@ -796,7 +847,7 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' pure fwdResponse - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE . PCEUnexpectedResponse $ B.take 32 $ bshow r okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = @@ -933,3 +984,5 @@ $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) $(J.deriveJSON defaultJSON ''NetworkConfig) + +$(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 118f3b084..d86d65251 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -66,6 +66,7 @@ module Simplex.Messaging.Protocol ErrorType (..), CommandError (..), ProxyError (..), + BrokerErrorType (..), Transmission, TransmissionAuth (..), SignedTransmission, @@ -176,6 +177,7 @@ module Simplex.Messaging.Protocol where import Control.Applicative (optional, (<|>)) +import Control.Exception (Exception) import Control.Monad import Control.Monad.Except import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -194,8 +196,6 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) @@ -211,7 +211,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -1144,6 +1144,8 @@ data ErrorType PROXY {proxyErr :: ProxyError} | -- | command authorization error - bad signature or non-existing SMP queue AUTH + | -- | encryption/decryption error in proxy protocol + CRYPTO | -- | SMP queue capacity is exceeded on the server QUOTA | -- | ACK command is sent without message to be acknowledged @@ -1186,19 +1188,32 @@ data CommandError data ProxyError = -- | Correctly parsed SMP server ERR response. - -- This error is forwarded to the agent client as `ERR SMP err`. + -- This error is forwarded to the agent client as AgentErrorType `ERR PROXY PROTOCOL err`. PROTOCOL {protocolErr :: ErrorType} - | -- | Invalid server response that failed to parse. - -- Forwarded to the agent client as `ERR BROKER RESPONSE`. - RESPONSE {responseErr :: ErrorType} - | UNEXPECTED {unexpectedResponse :: String} -- 'String' for using derived JSON and Arbitrary instances - | TIMEOUT - | NETWORK - | BAD_HOST - | NO_SESSION - | TRANSPORT {transportErr :: TransportError} + | -- | destination server error + BROKER {brokerErr :: BrokerErrorType} + | -- | basic auth provided to proxy is invalid + BASIC_AUTH + | -- no destination server error + NO_SESSION deriving (Eq, Read, Show) +-- | SMP server errors. +data BrokerErrorType + = -- | invalid server response (failed to parse) + RESPONSE {respErr :: String} + | -- | unexpected response + UNEXPECTED {respErr :: String} + | -- | network error + NETWORK + | -- | no compatible server host (e.g. onion when public is required, or vice versa) + HOST + | -- | handshake or other transport error + TRANSPORT {transportErr :: TransportError} + | -- | command response timeout + TIMEOUT + deriving (Eq, Read, Show, Exception) + -- | SMP transmission parser. transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do @@ -1423,6 +1438,7 @@ instance Encoding ErrorType where CMD err -> "CMD " <> smpEncode err PROXY err -> "PROXY " <> smpEncode err AUTH -> "AUTH" + CRYPTO -> "CRYPTO" QUOTA -> "QUOTA" EXPIRED -> "EXPIRED" NO_MSG -> "NO_MSG" @@ -1437,13 +1453,14 @@ instance Encoding ErrorType where "CMD" -> CMD <$> _smpP "PROXY" -> PROXY <$> _smpP "AUTH" -> pure AUTH + "CRYPTO" -> pure CRYPTO "QUOTA" -> pure QUOTA "EXPIRED" -> pure EXPIRED "NO_MSG" -> pure NO_MSG "LARGE_MSG" -> pure LARGE_MSG "INTERNAL" -> pure INTERNAL "DUPLICATE_" -> pure DUPLICATE_ - _ -> fail "bad error type" + _ -> fail "bad ErrorType" instance Encoding CommandError where smpEncode e = case e of @@ -1462,45 +1479,71 @@ instance Encoding CommandError where "HAS_AUTH" -> pure HAS_AUTH "NO_ENTITY" -> pure NO_ENTITY "NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility - _ -> fail "bad command error type" + _ -> fail "bad CommandError" instance Encoding ProxyError where - smpEncode e = case e of - PROTOCOL et -> "PROTOCOL " <> smpEncode et - RESPONSE et -> "RESPONSE " <> smpEncode et - UNEXPECTED s -> "UNEXPECTED " <> smpEncode (encodeUtf8 $ T.pack s) - TIMEOUT -> "TIMEOUT" - NETWORK -> "NETWORK" - BAD_HOST -> "BAD_HOST" + smpEncode = \case + PROTOCOL e -> "PROTOCOL " <> smpEncode e + BROKER e -> "BROKER " <> smpEncode e + BASIC_AUTH -> "BASIC_AUTH" NO_SESSION -> "NO_SESSION" - TRANSPORT t -> "TRANSPORT " <> serializeTransportError t smpP = A.takeTill (== ' ') >>= \case "PROTOCOL" -> PROTOCOL <$> _smpP - "RESPONSE" -> RESPONSE <$> _smpP - "UNEXPECTED" -> UNEXPECTED . (T.unpack . safeDecodeUtf8) <$> _smpP - "TIMEOUT" -> pure TIMEOUT - "NETWORK" -> pure NETWORK - "BAD_HOST" -> pure BAD_HOST + "BROKER" -> BROKER <$> _smpP + "BASIC_AUTH" -> pure BASIC_AUTH "NO_SESSION" -> pure NO_SESSION - "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) - _ -> fail "bad command error type" + _ -> fail "bad ProxyError" instance StrEncoding ProxyError where strEncode = \case - PROTOCOL et -> "PROTOCOL " <> strEncode et - RESPONSE et -> "RESPONSE " <> strEncode et - UNEXPECTED "" -> "UNEXPECTED" -- Arbitrary instance generates empty strings which String instance can't handle - UNEXPECTED s -> "UNEXPECTED " <> strEncode s - TRANSPORT t -> "TRANSPORT " <> serializeTransportError t - e -> bshow e + PROTOCOL e -> "PROTOCOL " <> strEncode e + BROKER e -> "BROKER " <> strEncode e + BASIC_AUTH -> "BASIC_AUTH" + NO_SESSION -> "NO_SESSION" strP = - "PROTOCOL " *> (PROTOCOL <$> strP) - <|> "RESPONSE " *> (RESPONSE <$> strP) - <|> "UNEXPECTED " *> (UNEXPECTED <$> strP) - <|> "UNEXPECTED" $> UNEXPECTED "" - <|> "TRANSPORT " *> (TRANSPORT <$> transportErrorP) - <|> parseRead1 + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> PROTOCOL <$> _strP + "BROKER" -> BROKER <$> _strP + "BASIC_AUTH" -> pure BASIC_AUTH + "NO_SESSION" -> pure NO_SESSION + _ -> fail "bad ProxyError" + +instance Encoding BrokerErrorType where + smpEncode = \case + RESPONSE e -> "RESPONSE " <> smpEncode e + UNEXPECTED e -> "UNEXPECTED " <> smpEncode e + TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + smpP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _smpP + "UNEXPECTED" -> UNEXPECTED <$> _smpP + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" + +instance StrEncoding BrokerErrorType where + strEncode = \case + RESPONSE e -> "RESPONSE " <> strEncode e + UNEXPECTED e -> "UNEXPECTED " <> strEncode e + TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + strP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _strP + "UNEXPECTED" -> UNEXPECTED <$> _strP + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" -- | Send signed SMP transmission to TCP transport. tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] @@ -1639,5 +1682,7 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) +$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) + -- run deriveJSON in one TH splice to allow mutual instance $(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType]) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index d1fcfbc24..0c0426958 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -54,7 +54,7 @@ import Data.Functor (($>)) import Data.Int (Int64) import qualified Data.IntMap.Strict as IM import Data.List (intercalate) -import Data.List.NonEmpty (NonEmpty) +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (isNothing) @@ -68,7 +68,7 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (ProtocolClient (thParams), forwardSMPMessage, smpProxyError) +import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError) import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), getSMPServerClient', lookupSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -616,28 +616,27 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi reply = atomically . writeTBQueue sndQ processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg) processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of - PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR AUTH) + PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR $ PROXY BASIC_AUTH) where allowProxy = do ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth getRelay = do ProxyAgent {smpAgent} <- asks proxyAgent - -- TODO catch IO errors too - liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) + liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) `catch` (pure . Left . PCEIOError) where proxyResp = \case + Left err -> ERR $ smpProxyError err Right smp -> let THandleParams {sessionId = srvSessId, thAuth} = thParams smp vr = supportedServerSMPRelayVRange in case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR $ PROXY (TRANSPORT TENoServerAuth) - Left err -> ERR $ smpProxyError err + Nothing -> ERR . PROXY . BROKER $ TRANSPORT TENoServerAuth PFWD pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case - Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) + Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) Nothing -> pure $ ERR $ PROXY NO_SESSION processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do @@ -916,50 +915,48 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi pure . (cbNonce,) $ fromRight "" encNMsgMeta processForwardedCommand :: EncFwdTransmission -> M BrokerMsg - processForwardedCommand (EncFwdTransmission s) = fmap (either id id) . runExceptT $ do - -- TODO error - THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwError $ ERR INTERNAL) pure thAuth - sessSecret <- maybe (throwError $ ERR INTERNAL) pure sessSecret' + processForwardedCommand (EncFwdTransmission s) = fmap (either ERR id) . runExceptT $ do + THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE noRelayAuth) pure (thAuth thParams') + sessSecret <- maybe (throwE noRelayAuth) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId - -- TODO error - s' <- liftEitherWith internalErr $ C.cbDecryptNoPad sessSecret proxyNonce s - -- TODO error - FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s' - -- TODO error - this error is reported to proxy, as we failed to get to client's transmission + s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s + FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' let clientSecret = C.dh' fwdKey serverPrivKey clientNonce = C.cbNonce $ bs fwdCorrId - b <- liftEitherWith internalErr $ C.cbDecrypt clientSecret clientNonce et + b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et -- only allowing single forwarded transactions - let t' = tDecodeParseValidate thParams' $ L.head $ tParse thParams' b - clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} - -- TODO error + t' <- case tParse thParams' b of + t :| [] -> pure $ tDecodeParseValidate thParams' t + _ -> throwE BLOCK + let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} + -- process forwarded SEND r <- lift (rejectOrVerify clntThAuth t') >>= \case Left r -> pure r - Right t''@(_, (corrId', entId', _)) -> - -- Left will not be returned by processCommand, as only SEND command is allowed - fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') - + Right t''@(_, (corrId', entId', cmd')) -> case cmd' of + Cmd SSender SEND {} -> + -- Left will not be returned by processCommand, as only SEND command is allowed + fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') + _ -> + pure (corrId', entId', ERR $ CMD PROHIBITED) -- encode response r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of - [] -> throwE $ ERR INTERNAL -- TODO error - TBError _ _ : _ -> throwE $ ERR INTERNAL -- TODO error + [] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right + TBError _ _ : _ -> throwE BLOCK TBTransmission b' _ : _ -> pure b' TBTransmissions b' _ _ : _ -> pure b' -- encrypt to client - r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength + r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength -- encrypt to proxy let fr = FwdResponse {fwdCorrId, fwdResponse = r2} r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) pure $ RRES r3 where - internalErr _ = ERR INTERNAL -- TODO errors - THandleParams {thAuth} = thParams' + noRelayAuth = PROXY $ BROKER $ TRANSPORT TENoServerAuth rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = case cmdOrError of Left e -> pure $ Left (corrId', entId', ERR e) - -- flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' where verified = \case diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 8f5ad70e7..0bf60afdd 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -6,17 +6,15 @@ module CoreTests.ProtocolErrorTests where -import qualified Data.ByteString.Char8 as B -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.FileTransfer.Transport (XFTPErrorType (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as Agent +import Simplex.Messaging.Client (ProxyClientError (..)) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..)) +import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) @@ -27,21 +25,32 @@ import Test.QuickCheck protocolErrorTests :: Spec protocolErrorTests = modifyMaxSuccess (const 1000) $ do describe "errors parsing / serializing" $ do - it "should parse SMP protocol errors" . property $ \(err :: ErrorType) -> + it "should parse SMP protocol errors" . property . forAll possibleErrorType $ \err -> smpDecode (smpEncode err) == Right err - it "should parse SMP agent errors" . property . forAll possible $ \err -> + it "should parse SMP agent errors" . property . forAll possibleAgentErrorType $ \err -> strDecode (strEncode err) == Right err where - possible :: Gen AgentErrorType - possible = + possibleErrorType :: Gen ErrorType + possibleErrorType = arbitrary >>= \e -> if skipErrorType e then discard else pure e + possibleAgentErrorType :: Gen AgentErrorType + possibleAgentErrorType = arbitrary >>= \case - BROKER srv (Agent.RESPONSE e) | hasSpaces srv || hasSpaces e -> discard - BROKER srv _ | hasSpaces srv -> discard - SMP (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard - NTF (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard + BROKER srv _ | skip srv -> discard + BROKER _ (RESPONSE e) | skip e -> discard + BROKER _ (UNEXPECTED e) | skip e -> discard + SMP e | skipErrorType e -> discard + NTF e | skipErrorType e -> discard + Agent.PROXY pxy srv _ | skip pxy || skip srv -> discard + Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard + Agent.PROXY _ _ (ProxyUnexpectedResponse e) | skip e -> discard + Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard ok -> pure ok - hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) - hasUnicode = any (>= '\255') + skip s = null s || any (\c -> c <= ' ' || c >= '\255') s + skipErrorType = \case + SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e + SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> skip s + SMP.PROXY (SMP.BROKER (RESPONSE s)) -> skip s + _ -> False deriving instance Generic AgentErrorType @@ -49,6 +58,8 @@ deriving instance Generic CommandErrorType deriving instance Generic ConnectionErrorType +deriving instance Generic ProxyClientError + deriving instance Generic BrokerErrorType deriving instance Generic SMPAgentError @@ -59,7 +70,7 @@ deriving instance Generic ErrorType deriving instance Generic CommandError -deriving instance Generic ProxyError +deriving instance Generic SMP.ProxyError deriving instance Generic TransportError @@ -75,6 +86,8 @@ instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU +instance Arbitrary ProxyClientError where arbitrary = genericArbitraryU + instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU @@ -85,7 +98,7 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU instance Arbitrary CommandError where arbitrary = genericArbitraryU -instance Arbitrary ProxyError where arbitrary = genericArbitraryU +instance Arbitrary SMP.ProxyError where arbitrary = genericArbitraryU instance Arbitrary TransportError where arbitrary = genericArbitraryU diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index b70c88883..52145a992 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -113,7 +113,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do -- get proxy session sess <- connectSMPProxiedRelay pc relayServ (Just "correct") -- send via proxy to unsecured queue - proxySMPMessage pc sess Nothing sndId noMsgFlags msg + Right () <- proxySMPMessage pc sess Nothing sndId noMsgFlags msg -- receive 1 (_tSess, _v, _sid, _isResp, _entId, SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId encBody `shouldBe` Right msg @@ -122,7 +122,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g secureSMPQueue rc rPriv rcvId sPub -- send via proxy to secured queue - proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' + Right () <- proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' -- receive 2 (_tSess, _v, _sid, _isResp, _entId, SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId' encBody' `shouldBe` Right msg' @@ -171,14 +171,14 @@ testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) - reply `shouldBe` Right (SMP.ERR SMP.AUTH) + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") - reply `shouldBe` Right (SMP.ERR SMP.AUTH) + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} From a70f492f4dbb9ec997b39ab3ea89c2858b5dde73 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 7 May 2024 13:37:40 +0100 Subject: [PATCH 08/13] proxy: fallback to direct connection if destination relay does not support proxy protocol (#1132) * proxy: fallback to direct connection if destination relay does not support proxy protocol * move version to TransportError, refactor --- src/Simplex/FileTransfer/Client.hs | 4 +- src/Simplex/FileTransfer/Transport.hs | 5 +- src/Simplex/Messaging/Agent/Client.hs | 61 +++++++++----- src/Simplex/Messaging/Agent/Protocol.hs | 83 +++++++++++-------- src/Simplex/Messaging/Client.hs | 30 +++++-- .../Messaging/Notifications/Transport.hs | 5 +- src/Simplex/Messaging/Protocol.hs | 22 +++-- src/Simplex/Messaging/Server.hs | 27 ++++-- src/Simplex/Messaging/Transport.hs | 46 +++++----- tests/AgentTests/FunctionalAPITests.hs | 6 +- tests/CoreTests/ProtocolErrorTests.hs | 17 ++-- tests/SMPAgentClient.hs | 7 +- tests/SMPClient.hs | 5 +- tests/SMPProxyTests.hs | 20 ++++- tests/Test.hs | 2 +- 15 files changed, 212 insertions(+), 128 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index a788e39c2..4efff9388 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -51,7 +51,7 @@ import Simplex.Messaging.Protocol RecipientId, SenderId, ) -import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) +import Simplex.Messaging.Transport (ALPN, THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client @@ -114,7 +114,7 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, thParams@THandleParams {thVersion} <- case sessionALPN of Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0 Nothing -> pure thParams0 - _ -> throwError $ PCETransportError (TEHandshake VERSION) + _ -> throwError $ PCETransportError TEVersion logDebug $ "Client negotiated protocol: " <> tshow thVersion let c = XFTPClient {http2Client, thParams, transportSession, config} atomically $ writeTVar clientVar $ Just c diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index 27f1b8b95..244e00972 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -37,6 +37,7 @@ import qualified Control.Exception as E import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Control.Monad.Trans.Except import qualified Data.Aeson.TH as J import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (bimap, first) @@ -53,7 +54,7 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol (CommandError) -import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) +import Simplex.Messaging.Transport (SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.File import Simplex.Messaging.Util (bshow) import Simplex.Messaging.Version @@ -95,7 +96,7 @@ supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion -- XFTP protocol does not use this handshake method xftpClientHandshakeStub :: c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient) -xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwError $ TEHandshake VERSION +xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwE TEVersion data XFTPServerHandshake = XFTPServerHandshake { xftpVersionRange :: VersionRangeXFTP, diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 35015af5a..ab9f3eb5f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -147,6 +147,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J import qualified Data.Aeson.TH as J @@ -231,7 +232,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SMPVersion) +import Simplex.Messaging.Transport (SMPVersion, TransportError (..)) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -577,7 +578,7 @@ getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do prs <- atomically TM.empty smpConnectClient c tSess prs v -getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, ProxiedRelay) +getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do unlessM (readTVarIO active) . throwError $ INACTIVE proxySrv <- getNextServer c userId [destSrv] @@ -589,7 +590,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq ProtoServerWithAuth srv auth <- TM.lookup destSess smpProxiedRelays >>= maybe (TM.insert destSess proxySrv smpProxiedRelays $> proxySrv) pure let tSess = (userId, srv, qId) (tSess,auth,) <$> getSessVar workerSeq tSess smpClients - newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) newProxyClient tSess auth v = do (prs, rv) <- atomically $ do prs <- TM.empty @@ -598,32 +599,33 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq (prs,) . either id id <$> getSessVar workerSeq destSrv prs clnt <- smpConnectClient c tSess prs v (clnt,) <$> newProxiedRelay clnt auth rv - waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) waitForProxyClient tSess auth v = do clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess v sess <- atomically (getSessVar workerSeq destSrv prs) >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) pure (clnt, sess) - newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM ProxiedRelay + newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) newProxiedRelay clnt@(SMPConnectedClient smp prs) proxyAuth rv = tryAgentError (liftClient SMP (clientServer smp) $ connectSMPProxiedRelay smp destSrv proxyAuth) >>= \case Right sess -> do atomically $ putTMVar (sessionVar rv) (Right sess) liftIO $ incClientStat c userId clnt "PROXY" "OK" - pure sess + pure $ Right sess Left e -> do liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e atomically $ do - removeSessVar rv destSrv prs - TM.delete destSess smpProxiedRelays + unless (persistentProxyError e) $ do + removeSessVar rv destSrv prs + TM.delete destSess smpProxiedRelays putTMVar (sessionVar rv) (Left e) - throwError e -- signal error to caller - waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM ProxiedRelay + pure $ Left e + waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) waitForProxiedRelay (_, srv, _) rv = do NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c sess_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar rv) - liftEither $ case sess_ of + pure $ case sess_ of Just (Right sess) -> Right sess Just (Left e) -> Left e Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT @@ -874,6 +876,7 @@ closeAgentClient c = do closeProtocolServerClients c smpClients closeProtocolServerClients c ntfClients closeProtocolServerClients c xftpClients + atomically $ writeTVar (smpProxiedRelays c) M.empty atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect clearWorkers smpDeliveryWorkers >>= mapM_ (cancelWorker . fst) clearWorkers asyncCmdWorkers >>= mapM_ cancelWorker @@ -981,11 +984,14 @@ withClient_ c tSess@(userId, srv, _) statCmd action = do withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do - cp@(cl, _) <- getSMPProxyClient c destSess + (cl, sess_) <- getSMPProxyClient c destSess logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr - r <- (action cp <* stat cl "OK") `catchAgentError` logServerError cl - logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" - pure r + case sess_ of + Right sess -> do + r <- (action (cl, sess) <* stat cl "OK") `catchAgentError` logServerError cl + logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" + pure r + Left e -> logServerError cl e where stat cl = liftIO . incClientStat c userId cl cmdStr proxySrv = showServer . protocolClientServer' . protocolClient @@ -1029,18 +1035,29 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do | ipAddressProtected cfg destSrv -> pure False | otherwise -> unknownServer SPMNever -> pure False + directAllowed = do + cfg <- getNetworkConfig c + pure $ case smpProxyFallback cfg of + SPFAllow -> True + SPFAllowProtected -> ipAddressProtected cfg destSrv + SPFProhibit -> False unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) - sendViaProxy destSess = - withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do + sendViaProxy destSess = do + r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case Right () -> pure . Just $ protocolClientServer' smp Left proxyErr -> - throwError + throwE PROXY { proxyServer = protocolClientServer smp, relayServer = B.unpack $ strEncode destSrv, proxyErr } + case r of + Right r' -> pure r' + Left e + | persistentProxyError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) + | otherwise -> throwE e sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg @@ -1280,7 +1297,6 @@ temporaryAgentError = \case NETWORK -> True TIMEOUT -> True _ -> False -{-# INLINE temporaryAgentError #-} temporaryOrHostError :: AgentErrorType -> Bool temporaryOrHostError = \case @@ -1288,7 +1304,12 @@ temporaryOrHostError = \case SMP (SMP.PROXY (SMP.BROKER HOST)) -> True PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER HOST))) -> True e -> temporaryAgentError e -{-# INLINE temporaryOrHostError #-} + +persistentProxyError :: AgentErrorType -> Bool +persistentProxyError = \case + BROKER _ (SMP.TRANSPORT TEVersion) -> True + SMP (SMP.PROXY (SMP.BROKER (SMP.TRANSPORT TEVersion))) -> True + _ -> False -- | Subscribe to queues. The list of results can have a different order. subscribeQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 61ab2400b..5cfd7af03 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -235,7 +235,7 @@ import Simplex.Messaging.Protocol ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme -import Simplex.Messaging.Transport (Transport (..), serializeTransportError, transportErrorP) +import Simplex.Messaging.Transport (Transport (..)) import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -1554,12 +1554,14 @@ data AgentCryptoError instance StrEncoding AgentCryptoError where strP = - "DECRYPT_AES" $> DECRYPT_AES - <|> "DECRYPT_CB" $> DECRYPT_CB - <|> "RATCHET_HEADER" $> RATCHET_HEADER - <|> "RATCHET_EARLIER " *> (RATCHET_EARLIER <$> strP) - <|> "RATCHET_SKIPPED " *> (RATCHET_SKIPPED <$> strP) - <|> "RATCHET_SYNC" $> RATCHET_SYNC + A.takeTill (== ' ') >>= \case + "DECRYPT_AES" -> pure DECRYPT_AES + "DECRYPT_CB" -> pure DECRYPT_CB + "RATCHET_HEADER" -> pure RATCHET_HEADER + "RATCHET_EARLIER" -> RATCHET_EARLIER <$> _strP + "RATCHET_SKIPPED" -> RATCHET_SKIPPED <$> _strP + "RATCHET_SYNC" -> pure RATCHET_SYNC + _ -> fail "AgentCryptoError" strEncode = \case DECRYPT_AES -> "DECRYPT_AES" DECRYPT_CB -> "DECRYPT_CB" @@ -1570,25 +1572,24 @@ instance StrEncoding AgentCryptoError where instance StrEncoding AgentErrorType where strP = - "CMD " *> (CMD <$> parseRead1) - <|> "CONN " *> (CONN <$> parseRead1) - <|> "SMP " *> (SMP <$> strP) - <|> "NTF " *> (NTF <$> strP) - <|> "XFTP " *> (XFTP <$> strP) - <|> "PROXY " *> (PROXY <$> textP <* A.space <*> textP <*> _strP) - <|> "RCP " *> (RCP <$> strP) - <|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP)) - <|> "BROKER " *> (BROKER <$> textP <* " UNEXPECTED " <*> (UNEXPECTED <$> textP)) - <|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP)) - <|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1) - <|> "AGENT CRYPTO " *> (AGENT . A_CRYPTO <$> parseRead A.takeByteString) - <|> "AGENT QUEUE " *> (AGENT . A_QUEUE <$> parseRead A.takeByteString) - <|> "AGENT " *> (AGENT <$> parseRead1) - <|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString) - <|> "CRITICAL " *> (CRITICAL <$> parseRead1 <* A.space <*> parseRead A.takeByteString) - <|> "INACTIVE" $> INACTIVE + A.takeTill (== ' ') + >>= \case + "CMD" -> CMD <$> (A.space *> parseRead1) + "CONN" -> CONN <$> (A.space *> parseRead1) + "SMP" -> SMP <$> _strP + "NTF" -> NTF <$> _strP + "XFTP" -> XFTP <$> _strP + "PROXY" -> PROXY <$> (A.space *> srvP) <* A.space <*> srvP <*> _strP + "RCP" -> RCP <$> _strP + "BROKER" -> BROKER <$> (A.space *> srvP) <*> _strP + "AGENT" -> AGENT <$> _strP + "INTERNAL" -> INTERNAL <$> (A.space *> textP) + "CRITICAL" -> CRITICAL <$> (A.space *> parseRead1) <*> (A.space *> textP) + "INACTIVE" -> pure INACTIVE + _ -> fail "bad AgentErrorType" where - textP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') + srvP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') + textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString strEncode = \case CMD e -> "CMD " <> bshow e CONN e -> "CONN " <> bshow e @@ -1597,19 +1598,33 @@ instance StrEncoding AgentErrorType where XFTP e -> "XFTP " <> strEncode e PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e] RCP e -> "RCP " <> strEncode e - BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e - BROKER srv (UNEXPECTED e) -> "BROKER " <> text srv <> " UNEXPECTED " <> text e - BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e - BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e - AGENT (A_CRYPTO e) -> "AGENT CRYPTO " <> bshow e - AGENT (A_QUEUE e) -> "AGENT QUEUE " <> bshow e - AGENT e -> "AGENT " <> bshow e - INTERNAL e -> "INTERNAL " <> bshow e - CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> bshow e + BROKER srv e -> B.unwords ["BROKER", text srv, strEncode e] + AGENT e -> "AGENT " <> strEncode e + INTERNAL e -> "INTERNAL " <> encodeUtf8 (T.pack e) + CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> encodeUtf8 (T.pack e) INACTIVE -> "INACTIVE" where text = encodeUtf8 . T.pack +instance StrEncoding SMPAgentError where + strP = + A.takeTill (== ' ') + >>= \case + "MESSAGE" -> pure A_MESSAGE + "PROHIBITED" -> pure A_PROHIBITED + "VERSION" -> pure A_VERSION + "CRYPTO" -> A_CRYPTO <$> _strP + "DUPLICATE" -> pure A_DUPLICATE + "QUEUE" -> A_QUEUE . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + _ -> fail "bad SMPAgentError" + strEncode = \case + A_MESSAGE -> "MESSAGE" + A_PROHIBITED -> "PROHIBITED" + A_VERSION -> "VERSION" + A_CRYPTO e -> "CRYPTO " <> strEncode e + A_DUPLICATE -> "DUPLICATE" + A_QUEUE e -> "QUEUE " <> encodeUtf8 (T.pack e) + cryptoErrToSyncState :: AgentCryptoError -> RatchetSyncState cryptoErrToSyncState = \case DECRYPT_AES -> RSAllowed diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 8a1349089..c38e56810 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -70,6 +70,7 @@ module Simplex.Messaging.Client TransportSessionMode (..), HostMode (..), SMPProxyMode (..), + SMPProxyFallback (..), defaultClientConfig, defaultSMPClientConfig, defaultNetworkConfig, @@ -224,6 +225,8 @@ data NetworkConfig = NetworkConfig sessionMode :: TransportSessionMode, -- | SMP proxy mode smpProxyMode :: SMPProxyMode, + -- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions + smpProxyFallback :: SMPProxyFallback, -- | timeout for the initial client TCP/TLS connection (microseconds) tcpConnectTimeout :: Int, -- | timeout of protocol commands (microseconds) @@ -253,6 +256,12 @@ data SMPProxyMode | SPMNever deriving (Eq, Show) +data SMPProxyFallback + = SPFAllow -- connect directly when chosen proxy or destination relay do not support proxy protocol. + | SPFAllowProtected -- connect directly only when IP address is protected (SOCKS proxy or .onion address is used). + | SPFProhibit -- prohibit direct connection to destination relay. + deriving (Eq, Show) + defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig @@ -261,6 +270,7 @@ defaultNetworkConfig = requiredHostMode = False, sessionMode = TSMUser, smpProxyMode = SPMNever, + smpProxyFallback = SPFAllow, tcpConnectTimeout = defaultTcpConnectTimeout, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 5_000, @@ -705,15 +715,17 @@ deleteSMPQueues = okSMPCommands DEL -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay -connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = - sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case - PKEY sId vr (chain, key) -> - case supportedClientSMPRelayVRange `compatibleVersion` vr of - Nothing -> throwE $ relayErr VERSION - Just (Compatible v) -> liftEitherWith (const $ relayErr IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key - r -> throwE . PCEUnexpectedResponse $ bshow r +connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth + | thVersion (thParams c) >= sendingProxySMPVersion = + sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case + PKEY sId vr (chain, key) -> + case supportedClientSMPRelayVRange `compatibleVersion` vr of + Nothing -> throwE $ transportErr TEVersion + Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key + r -> throwE . PCEUnexpectedResponse $ bshow r + | otherwise = throwE $ PCETransportError TEVersion where - relayErr = PCEProtocolError . PROXY . BROKER . TRANSPORT . TEHandshake + transportErr = PCEProtocolError . PROXY . BROKER . TRANSPORT validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 validateRelay (X.CertificateChain cert) exact = do serverKey <- case cert of @@ -983,6 +995,8 @@ $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback) + $(J.deriveJSON defaultJSON ''NetworkConfig) $(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index e2c287437..564b0c0df 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -11,6 +11,7 @@ module Simplex.Messaging.Notifications.Transport where import Control.Monad (forM) import Control.Monad.Except +import Control.Monad.Trans.Except import Data.Attoparsec.ByteString.Char8 (Parser) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -111,7 +112,7 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do throwError $ TEHandshake IDENTITY | v `isCompatible` ntfVRange -> pure $ ntfThHandleServer th v pk - | otherwise -> throwError $ TEHandshake VERSION + | otherwise -> throwE TEVersion -- | Notifcations server client transport handshake. ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient) @@ -128,7 +129,7 @@ ntfClientHandshake c keyHash ntfVRange = do (,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash} pure $ ntfThHandleClient th v ck_ - Nothing -> throwError $ TEHandshake VERSION + Nothing -> throwE TEVersion ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer ntfThHandleServer th v pk = diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index d86d65251..250c76fcf 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -196,6 +196,8 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) @@ -211,7 +213,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -1513,7 +1515,7 @@ instance Encoding BrokerErrorType where smpEncode = \case RESPONSE e -> "RESPONSE " <> smpEncode e UNEXPECTED e -> "UNEXPECTED " <> smpEncode e - TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + TRANSPORT e -> "TRANSPORT " <> smpEncode e NETWORK -> "NETWORK" TIMEOUT -> "TIMEOUT" HOST -> "HOST" @@ -1521,7 +1523,7 @@ instance Encoding BrokerErrorType where A.takeTill (== ' ') >>= \case "RESPONSE" -> RESPONSE <$> _smpP "UNEXPECTED" -> UNEXPECTED <$> _smpP - "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "TRANSPORT" -> TRANSPORT <$> _smpP "NETWORK" -> pure NETWORK "TIMEOUT" -> pure TIMEOUT "HOST" -> pure HOST @@ -1529,21 +1531,23 @@ instance Encoding BrokerErrorType where instance StrEncoding BrokerErrorType where strEncode = \case - RESPONSE e -> "RESPONSE " <> strEncode e - UNEXPECTED e -> "UNEXPECTED " <> strEncode e - TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + RESPONSE e -> "RESPONSE " <> encodeUtf8 (T.pack e) + UNEXPECTED e -> "UNEXPECTED " <> encodeUtf8 (T.pack e) + TRANSPORT e -> "TRANSPORT " <> smpEncode e NETWORK -> "NETWORK" TIMEOUT -> "TIMEOUT" HOST -> "HOST" strP = A.takeTill (== ' ') >>= \case - "RESPONSE" -> RESPONSE <$> _strP - "UNEXPECTED" -> UNEXPECTED <$> _strP - "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "RESPONSE" -> RESPONSE <$> _textP + "UNEXPECTED" -> UNEXPECTED <$> _textP + "TRANSPORT" -> TRANSPORT <$> _smpP "NETWORK" -> pure NETWORK "TIMEOUT" -> pure TIMEOUT "HOST" -> pure HOST _ -> fail "bad BrokerErrorType" + where + _textP = A.space *> (T.unpack . safeDecodeUtf8 <$> A.takeByteString) -- | Send signed SMP transmission to TCP transport. tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 0c0426958..4195bffba 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -628,16 +628,26 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi proxyResp = \case Left err -> ERR $ smpProxyError err Right smp -> - let THandleParams {sessionId = srvSessId, thAuth} = thParams smp - vr = supportedServerSMPRelayVRange - in case thAuth of - Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR . PROXY . BROKER $ TRANSPORT TENoServerAuth + let THandleParams {sessionId = srvSessId, thVersion, thAuth} = thParams smp + vr = supportedServerSMPRelayVRange -- TODO this should be destination relay version range + in if thVersion >= sendingProxySMPVersion + then case thAuth of + Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey + Nothing -> ERR $ transportErr TENoServerAuth + else ERR $ transportErr TEVersion PFWD pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case - Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) + Just smp + | v >= sendingProxySMPVersion -> + liftIO $ either (ERR . smpProxyError) PRES <$> + runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) + | otherwise -> pure . ERR $ transportErr TEVersion + where + THandleParams {thVersion = v} = thParams smp Nothing -> pure $ ERR $ PROXY NO_SESSION + transportErr :: TransportError -> ErrorType + transportErr = PROXY . BROKER . TRANSPORT processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do st <- asks queueStore @@ -916,8 +926,8 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi processForwardedCommand :: EncFwdTransmission -> M BrokerMsg processForwardedCommand (EncFwdTransmission s) = fmap (either ERR id) . runExceptT $ do - THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE noRelayAuth) pure (thAuth thParams') - sessSecret <- maybe (throwE noRelayAuth) pure sessSecret' + THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE $ transportErr TENoServerAuth) pure (thAuth thParams') + sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' @@ -952,7 +962,6 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) pure $ RRES r3 where - noRelayAuth = PROXY $ BROKER $ TRANSPORT TENoServerAuth rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = case cmdOrError of diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6f0f04ff7..6d1f05852 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -72,14 +72,12 @@ module Simplex.Messaging.Transport smpClientHandshake, tPutBlock, tGetBlock, - serializeTransportError, - transportErrorP, sendHandshake, getHandshake, ) where -import Control.Applicative (optional, (<|>)) +import Control.Applicative (optional) import Control.Monad (forM) import Control.Monad.Except import Control.Monad.Trans.Except (throwE) @@ -410,6 +408,8 @@ authEncryptCmdsP v p = if v >= authCmdsSMPVersion then optional p else pure Noth data TransportError = -- | error parsing transport block TEBadBlock + | -- | incompatible client or server version + TEVersion | -- | message does not fit in transport block TELargeMsg | -- | incorrect session ID @@ -425,31 +425,29 @@ data TransportError data HandshakeError = -- | parsing error PARSE - | -- | incompatible peer version - VERSION | -- | incorrect server identity IDENTITY | -- | v7 authentication failed BAD_AUTH deriving (Eq, Read, Show, Exception) --- | SMP encrypted transport error parser. -transportErrorP :: Parser TransportError -transportErrorP = - "BLOCK" $> TEBadBlock - <|> "LARGE_MSG" $> TELargeMsg - <|> "SESSION" $> TEBadSession - <|> "NO_AUTH" $> TENoServerAuth - <|> "HANDSHAKE " *> (TEHandshake <$> parseRead1) - --- | Serialize SMP encrypted transport error. -serializeTransportError :: TransportError -> ByteString -serializeTransportError = \case - TEBadBlock -> "BLOCK" - TELargeMsg -> "LARGE_MSG" - TEBadSession -> "SESSION" - TENoServerAuth -> "NO_AUTH" - TEHandshake e -> "HANDSHAKE " <> bshow e +instance Encoding TransportError where + smpP = + A.takeTill (== ' ') >>= \case + "BLOCK" -> pure TEBadBlock + "VERSION" -> pure TEVersion + "LARGE_MSG" -> pure TELargeMsg + "SESSION" -> pure TEBadSession + "NO_AUTH" -> pure TENoServerAuth + "HANDSHAKE" -> TEHandshake <$> (A.space *> parseRead1) + _ -> fail "bad TransportError" + smpEncode = \case + TEBadBlock -> "BLOCK" + TEVersion -> "VERSION" + TELargeMsg -> "LARGE_MSG" + TEBadSession -> "SESSION" + TENoServerAuth -> "NO_AUTH" + TEHandshake e -> "HANDSHAKE " <> bshow e -- | Pad and send block to SMP transport. tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) @@ -480,7 +478,7 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do throwE $ TEHandshake IDENTITY | v `isCompatible` smpVRange -> pure $ smpThHandleServer th v pk k' - | otherwise -> throwE $ TEHandshake VERSION + | otherwise -> throwE TEVersion -- | Client SMP transport handshake. -- @@ -503,7 +501,7 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_} pure $ smpThHandleClient th v (snd <$> ks_) ck_ - Nothing -> throwE $ TEHandshake VERSION + Nothing -> throwE TEVersion smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer smpThHandleServer th v pk k_ = diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 05c461f6e..64447da23 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -81,7 +81,7 @@ import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyFallback (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -454,7 +454,7 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = testMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False @@ -466,7 +466,7 @@ testMatrix2 t runTest = do testRatchetMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 0bf60afdd..af13ba030 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -35,21 +35,22 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do possibleAgentErrorType :: Gen AgentErrorType possibleAgentErrorType = arbitrary >>= \case - BROKER srv _ | skip srv -> discard - BROKER _ (RESPONSE e) | skip e -> discard - BROKER _ (UNEXPECTED e) | skip e -> discard + BROKER srv _ | hasSpaces srv -> discard SMP e | skipErrorType e -> discard NTF e | skipErrorType e -> discard - Agent.PROXY pxy srv _ | skip pxy || skip srv -> discard + Agent.PROXY pxy srv _ | hasSpaces pxy || hasSpaces srv -> discard Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard - Agent.PROXY _ _ (ProxyUnexpectedResponse e) | skip e -> discard + Agent.PROXY _ _ (ProxyUnexpectedResponse e) | hasUnicode e -> discard Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard ok -> pure ok - skip s = null s || any (\c -> c <= ' ' || c >= '\255') s + hasSpaces :: String -> Bool + hasSpaces = any (== ' ') + hasUnicode :: String -> Bool + hasUnicode = any (>= '\255') skipErrorType = \case SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e - SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> skip s - SMP.PROXY (SMP.BROKER (RESPONSE s)) -> skip s + SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> hasUnicode s + SMP.PROXY (SMP.BROKER (RESPONSE s)) -> hasUnicode s _ -> False deriving instance Generic AgentErrorType diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index b18b264e1..3cf09e5db 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -35,7 +35,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Server (runSMPAgentBlocking) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyMode, chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth) @@ -199,8 +199,9 @@ initAgentServers = initAgentServers2 :: InitialAgentServers initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]} -initAgentServersProxy :: SMPProxyMode -> InitialAgentServers -initAgentServersProxy smpProxyMode = initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode}} +initAgentServersProxy :: SMPProxyMode -> SMPProxyFallback -> InitialAgentServers +initAgentServersProxy smpProxyMode smpProxyFallback = + initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode, smpProxyFallback}} agentCfg :: AgentConfig agentCfg = diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index e27970608..3d9d77033 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -118,10 +118,13 @@ cfg = cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} +cfgV8 :: ServerConfig +cfgV8 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} + proxyCfg :: ServerConfig proxyCfg = cfgV7 - { allowSMPProxy = True, + { allowSMPProxy = True, smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True}} } diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 52145a992..a9c0c5fb1 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -14,6 +14,7 @@ module SMPProxyTests where import AgentTests.FunctionalAPITests +import Control.Monad.Trans.Except (runExceptT) import Data.ByteString.Char8 (ByteString) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L @@ -87,10 +88,14 @@ smpProxyTests = do agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" it "first via proxy for unknown" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ + agentViaProxyVersionError where oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg testPort . const twoServers = twoServers_ proxyCfg proxyCfg - twoServersFirstProxy = twoServers_ proxyCfg cfgV7 + twoServersFirstProxy = twoServers_ proxyCfg cfgV8 twoServers_ cfg1 cfg2 runTest = withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfg2 testPort2 $ const runTest @@ -164,7 +169,18 @@ agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, b baseId = 3 msgId = subtract baseId . fst aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} - servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode) {smp = userServers $ L.map noAuthSrv srvs} + servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} + +agentViaProxyVersionError :: IO () +agentViaProxyVersionError = + withAgent 1 agentProxyCfg (servers [SMPServer testHost testPort testKeyHash]) testDB $ \alice -> do + Left (A.BROKER _ (TRANSPORT TEVersion)) <- + withAgent 2 agentProxyCfg (servers [SMPServer testHost testPort2 testKeyHash]) testDB2 $ \bob -> runExceptT $ do + (_bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + pure () + where + servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers $ L.map noAuthSrv srvs} testNoProxy :: IO () testNoProxy = do diff --git a/tests/Test.hs b/tests/Test.hs index cd2b0d8c3..f9fb2a2c0 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -47,7 +47,7 @@ main = do $ do describe "Agent SQLite schema dump" schemaDumpTest describe "Core tests" $ do - xdescribe "Batching tests" batchingTests + describe "Batching tests" batchingTests describe "Encoding tests" encodingTests describe "Protocol error tests" protocolErrorTests describe "Version range" versionRangeTests From b27f126bab3d47212fd1f3a713aac3a6e3ed3062 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 8 May 2024 23:00:00 +0100 Subject: [PATCH 09/13] include server version range in transport handle (#1135) * include server version range in transport handle * xftp handshake * remove coment * simplify * comments --- src/Simplex/FileTransfer/Client.hs | 21 +++++---- src/Simplex/FileTransfer/Server.hs | 31 +++++++------ src/Simplex/FileTransfer/Server/Env.hs | 3 ++ src/Simplex/FileTransfer/Server/Main.hs | 2 + src/Simplex/Messaging/Agent/Protocol.hs | 1 + src/Simplex/Messaging/Client.hs | 1 + src/Simplex/Messaging/Crypto/Ratchet.hs | 31 +++++++------ .../Messaging/Notifications/Transport.hs | 44 +++++++++++------- src/Simplex/Messaging/Transport.hs | 46 ++++++++++++------- src/Simplex/Messaging/Version.hs | 23 ++++++++++ tests/CoreTests/BatchingTests.hs | 1 + tests/CoreTests/VersionRangeTests.hs | 36 +++++++++++++++ tests/XFTPClient.hs | 2 + 13 files changed, 173 insertions(+), 69 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 4efff9388..90067e22d 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -57,7 +57,7 @@ import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client import Simplex.Messaging.Transport.HTTP2.File import Simplex.Messaging.Util (bshow, liftEitherWith, liftError', tshow, whenM) -import Simplex.Messaging.Version (compatibleVersion, pattern Compatible) +import Simplex.Messaging.Version import UnliftIO import UnliftIO.Directory @@ -109,7 +109,9 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected let HTTP2Client {sessionId, sessionALPN} = http2Client - thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True} + v = VersionXFTP 1 + thServerVRange = versionToRange v + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True} logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN thParams@THandleParams {thVersion} <- case sessionALPN of Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0 @@ -123,9 +125,10 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpClientHandshakeV1 :: VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient) xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake - (v, sk) <- processServerHandshake shs + (vr, sk) <- processServerHandshake shs + let v = maxVersion vr sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash} - pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v} + pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v, thServerVRange = vr} where getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake getServerHandshake = do @@ -133,13 +136,13 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing liftHS . smpDecode =<< liftHS (C.unPad shsBody) - processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionXFTP, C.PublicKeyX25519) + processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION - case xftpVersionRange `compatibleVersion` serverVRange of - Nothing -> throwError $ PCEResponseError HANDSHAKE - Just (Compatible v) -> - fmap (v,) . liftHS $ do + case xftpVersionRange `compatibleVRange` serverVRange of + Nothing -> throwError $ PCETransportError TEVersion + Just (Compatible vr) -> + fmap (vr,) . liftHS $ do let (X.CertificateChain cert, exact) = serverAuth case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index 7b6787a43..41c652ea2 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -63,7 +63,7 @@ import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize) import Simplex.Messaging.Transport.HTTP2.Server import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials) import Simplex.Messaging.Util -import Simplex.Messaging.Version (isCompatible) +import Simplex.Messaging.Version import System.Exit (exitFailure) import System.FilePath (()) import System.IO (hPrint, hPutStrLn, universalNewlineMode) @@ -91,10 +91,10 @@ runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpSer data Handshake = HandshakeSent C.PrivateKeyX25519 - | HandshakeAccepted (THandleAuth 'TServer) VersionXFTP + | HandshakeAccepted (THandleParams XFTPVersion 'TServer) xftpServer :: XFTPServerConfig -> TMVar Bool -> M () -xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do +xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do mapM_ (expireServerFiles Nothing) fileExpiration restoreServerStats raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer @@ -111,7 +111,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira let cleanup sessionId = atomically $ TM.delete sessionId sessions liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do reqBody <- getHTTP2Body r xftpBlockSize - let thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True} + let v = VersionXFTP 1 + thServerVRange = versionToRange v + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True} req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse} flip runReaderT env $ case sessionALPN of Nothing -> processRequest req0 @@ -121,12 +123,12 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira Just thParams -> processRequest req0 {thParams} -- proceed with new version (XXX: may as well switch the request handler here) _ -> liftIO . sendResponse $ H.responseNoBody N.ok200 [] -- shouldn't happen: means server picked handshake protocol it doesn't know about xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion 'TServer)) - xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do + xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams0@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do s <- atomically $ TM.lookup sessionId sessions r <- runExceptT $ case s of Nothing -> processHello Just (HandshakeSent pk) -> processClientHandshake pk - Just (HandshakeAccepted auth v) -> pure $ Just thParams {thAuth = Just auth, thVersion = v} + Just (HandshakeAccepted thParams) -> pure $ Just thParams either sendError pure r where processHello = do @@ -134,21 +136,24 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira (k, pk) <- atomically . C.generateKeyPair =<< asks random atomically $ TM.insert sessionId (HandshakeSent pk) sessions let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k) - let hs = XFTPServerHandshake {xftpVersionRange = supportedFileServerVRange, sessionId, authPubKey} + let hs = XFTPServerHandshake {xftpVersionRange = xftpServerVRange, sessionId, authPubKey} shs <- encodeXftp hs liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing processClientHandshake pk = do unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE body <- liftHS $ C.unPad bodyHead - XFTPClientHandshake {xftpVersion, keyHash} <- liftHS $ smpDecode body + XFTPClientHandshake {xftpVersion = v, keyHash} <- liftHS $ smpDecode body kh <- asks serverIdentity unless (keyHash == kh) $ throwError HANDSHAKE - unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE - let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} - atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions - liftIO . sendResponse $ H.responseNoBody N.ok200 [] - pure Nothing + case compatibleVRange' xftpServerVRange v of + Just (Compatible vr) -> do + let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} + thParams = thParams0 {thAuth = Just auth, thVersion = v, thServerVRange = vr} + atomically $ TM.insert sessionId (HandshakeAccepted thParams) sessions + liftIO . sendResponse $ H.responseNoBody N.ok200 [] + pure Nothing + Nothing -> throwError HANDSHAKE sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer)) sendError err = do runExceptT (encodeXftp err) >>= \case diff --git a/src/Simplex/FileTransfer/Server/Env.hs b/src/Simplex/FileTransfer/Server/Env.hs index 414bfb4c4..58c1393f3 100644 --- a/src/Simplex/FileTransfer/Server/Env.hs +++ b/src/Simplex/FileTransfer/Server/Env.hs @@ -25,6 +25,7 @@ import Simplex.FileTransfer.Protocol (FileCmd, FileInfo (..), XFTPFileId) import Simplex.FileTransfer.Server.Stats import Simplex.FileTransfer.Server.Store import Simplex.FileTransfer.Server.StoreLog +import Simplex.FileTransfer.Transport (VersionRangeXFTP) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey) import Simplex.Messaging.Server.Expiration @@ -61,6 +62,8 @@ data XFTPServerConfig = XFTPServerConfig caCertificateFile :: FilePath, privateKeyFile :: FilePath, certificateFile :: FilePath, + -- | XFTP client-server protocol version range + xftpServerVRange :: VersionRangeXFTP, -- stats config - see SMP server config logStatsInterval :: Maybe Int64, logStatsStartTime :: Int64, diff --git a/src/Simplex/FileTransfer/Server/Main.hs b/src/Simplex/FileTransfer/Server/Main.hs index d53b3f4fa..b909b1617 100644 --- a/src/Simplex/FileTransfer/Server/Main.hs +++ b/src/Simplex/FileTransfer/Server/Main.hs @@ -20,6 +20,7 @@ import Simplex.FileTransfer.Chunks import Simplex.FileTransfer.Description (FileSize (..)) import Simplex.FileTransfer.Server (runXFTPServer) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern XFTPServer) @@ -174,6 +175,7 @@ xftpServerCLI cfgPath logPath = do caCertificateFile = c caCrtFile, privateKeyFile = c serverKeyFile, certificateFile = c serverCrtFile, + xftpServerVRange = supportedFileServerVRange, logStatsInterval = logStats $> 86400, -- seconds logStatsStartTime = 0, -- seconds from 00:00 UTC serverStatsLogFile = combine logPath "file-server-stats.daily.log", diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 5cfd7af03..7160863f1 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -1302,6 +1302,7 @@ instance VersionRangeI SMPClientVersion SMPQueueUri where type VersionT SMPClientVersion SMPQueueUri = SMPQueueInfo versionRange = clientVRange toVersionT (SMPQueueUri _vr addr) v = SMPQueueInfo v addr + toVersionRange (SMPQueueUri _vr addr) vr = SMPQueueUri vr addr -- | SMP queue information sent out-of-band. -- diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 6b1beb6a1..59fbd4c12 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -172,6 +172,7 @@ smpClientStub g sessionId thVersion thAuth = do THandleParams { sessionId, thVersion, + thServerVRange = supportedServerSMPRelayVRange, thAuth, blockSize = smpBlockSize, implySessId = thVersion >= authCmdsSMPVersion, diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index 6ab84aa30..db13fddc1 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -117,7 +117,7 @@ import Simplex.Messaging.Crypto.SNTRUP761.Bindings import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, parseE, parseE') -import Simplex.Messaging.Util ((<$?>), ($>>=)) +import Simplex.Messaging.Util (($>>=), (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import UnliftIO.STM @@ -266,6 +266,7 @@ instance VersionRangeI E2EVersion (E2ERatchetParamsUri s a) where type VersionT E2EVersion (E2ERatchetParamsUri s a) = (E2ERatchetParams s a) versionRange (E2ERatchetParamsUri vr _ _ _) = vr toVersionT (E2ERatchetParamsUri _ k1 k2 kem_) v = E2ERatchetParams v k1 k2 kem_ + toVersionRange (E2ERatchetParamsUri _ k1 k2 kem_) vr = E2ERatchetParamsUri vr k1 k2 kem_ type RcvE2ERatchetParamsUri a = E2ERatchetParamsUri 'RKSProposed a @@ -377,13 +378,15 @@ generateE2EParams g v useKEM_ = do where kemParams :: IO (Maybe (RKEMParams s, PrivRKEMParams s)) kemParams = case useKEM_ of - Just useKem | v >= pqRatchetE2EEncryptVersion -> Just <$> do - ks@(k, _) <- sntrup761Keypair g - case useKem of - ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) - AcceptKEM k' -> do - (ct, shared) <- sntrup761Enc g k' - pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) + Just useKem + | v >= pqRatchetE2EEncryptVersion -> + Just <$> do + ks@(k, _) <- sntrup761Keypair g + case useKem of + ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) + AcceptKEM k' -> do + (ct, shared) <- sntrup761Enc g k' + pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) _ -> pure Nothing -- used by party initiating connection, Bob in double-ratchet spec @@ -456,7 +459,7 @@ pqX3dh (sk1, rk1) dh1 dh2 dh3 kemAccepted = pq = maybe "" (\RatchetKEMAccepted {rcPQRss = KEMSharedKey ss} -> BA.convert ss) kemAccepted (hk, nhk, sk) = let salt = B.replicate 64 '\0' - in hkdf3 salt dhs "SimpleXX3DH" + in hkdf3 salt dhs "SimpleXX3DH" type RatchetX448 = Ratchet 'X448 @@ -698,8 +701,8 @@ data EncMessageHeader = EncMessageHeader -- this encoding depends on version in EncMessageHeader because it is "current" ratchet version instance Encoding EncMessageHeader where - smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} - = smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody + smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} = + smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody smpP = do (ehVersion, ehIV, ehAuthTag) <- smpP ehBody <- largeP @@ -708,8 +711,6 @@ instance Encoding EncMessageHeader where -- the encoder always uses 2-byte lengths for the new version, even for short headers without PQ keys. encodeLarge :: VersionE2E -> ByteString -> ByteString encodeLarge v s - -- the condition for length is not necessary, it's here as a fallback. - -- | v >= pqRatchetE2EEncryptVersion || B.length s > 255 = smpEncode $ Large s | v >= pqRatchetE2EEncryptVersion = smpEncode $ Large s | otherwise = smpEncode s @@ -729,8 +730,8 @@ data EncRatchetMessage = EncRatchetMessage } encodeEncRatchetMessage :: VersionE2E -> EncRatchetMessage -> ByteString -encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} - = encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody) +encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} = + encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody) encRatchetMessageP :: Parser EncRatchetMessage encRatchetMessageP = do diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index 7a3efee54..ddb8880b5 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -117,9 +117,10 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do NtfClientHandshake {ntfVersion = v, keyHash} | keyHash /= kh -> throwError $ TEHandshake IDENTITY - | v `isCompatible` ntfVersionRange -> - pure $ ntfThHandleServer th v pk - | otherwise -> throwE TEVersion + | otherwise -> + case compatibleVRange' ntfVersionRange v of + Just (Compatible vr) -> pure $ ntfThHandleServer th v vr pk + Nothing -> throwE TEVersion -- | Notifcations server client transport handshake. ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient) @@ -128,34 +129,45 @@ ntfClientHandshake c keyHash ntfVRange = do NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th if sessionId /= sessId then throwError TEBadSession - else case ntfVersionRange `compatibleVersion` ntfVRange of - Just (Compatible v) -> do + else case ntfVersionRange `compatibleVRange` ntfVRange of + Just (Compatible vr) -> do ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do serverKey <- getServerVerifyKey c pubKey <- C.verifyX509 serverKey signedKey (,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) + let v = maxVersion vr sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash} - pure $ ntfThHandleClient th v ck_ + pure $ ntfThHandleClient th v vr ck_ Nothing -> throwE TEVersion -ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer -ntfThHandleServer th v pk = +ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> VersionRangeNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer +ntfThHandleServer th v vr pk = let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} - in ntfThHandle_ th v (Just thAuth) + in ntfThHandle_ th v vr (Just thAuth) -ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient -ntfThHandleClient th v ck_ = +ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> VersionRangeNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient +ntfThHandleClient th v vr ck_ = let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = Nothing}) <$> ck_ - in ntfThHandle_ th v thAuth + in ntfThHandle_ th v vr thAuth -ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> Maybe (THandleAuth p) -> THandleNTF c p -ntfThHandle_ th@THandle {params} v thAuth = +ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> VersionRangeNTF -> Maybe (THandleAuth p) -> THandleNTF c p +ntfThHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional let v3 = v >= authBatchCmdsNTFVersion - params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3} + params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v3, batch = v3} in (th :: THandleNTF c p) {params = params'} ntfTHandle :: Transport c => c -> THandleNTF c p ntfTHandle c = THandle {connection = c, params} where - params = THandleParams {sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = VersionNTF 0, thAuth = Nothing, implySessId = False, batch = False} + v = VersionNTF 0 + params = + THandleParams + { sessionId = tlsUnique c, + blockSize = ntfBlockSize, + thVersion = v, + thServerVRange = versionToRange v, + thAuth = Nothing, + implySessId = False, + batch = False + } diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index ffdbf4a20..ad5a29822 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -340,6 +340,8 @@ type THandleSMP c p = THandle SMPVersion c p data THandleParams v p = THandleParams { sessionId :: SessionId, blockSize :: Int, + -- | server protocol version range + thServerVRange :: VersionRange v, -- | agreed server protocol version thVersion :: Version v, -- | peer public key for command authorization and shared secrets for entity ID encryption @@ -493,9 +495,10 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do ClientHandshake {smpVersion = v, keyHash, authPubKey = k'} | keyHash /= kh -> throwE $ TEHandshake IDENTITY - | v `isCompatible` smpVersionRange -> - pure $ smpThHandleServer th v pk k' - | otherwise -> throwE TEVersion + | otherwise -> + case compatibleVRange' smpVersionRange v of + Just (Compatible vr) -> pure $ smpThHandleServer th v vr pk k' + Nothing -> throwE TEVersion -- | Client SMP transport handshake. -- @@ -506,8 +509,8 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th if sessionId /= sessId then throwE TEBadSession - else case smpVersionRange `compatibleVersion` smpVRange of - Just (Compatible v) -> do + else case smpVersionRange `compatibleVRange` smpVRange of + Just (Compatible vr) -> do ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do case cert of @@ -516,24 +519,25 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do serverKey <- getServerVerifyKey c pubKey <- C.verifyX509 serverKey exact (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) + let v = maxVersion vr sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_} - pure $ smpThHandleClient th v (snd <$> ks_) ck_ + pure $ smpThHandleClient th v vr (snd <$> ks_) ck_ Nothing -> throwE TEVersion -smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer -smpThHandleServer th v pk k_ = +smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer +smpThHandleServer th v vr pk k_ = let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_} - in smpThHandle_ th v (Just thAuth) + in smpThHandle_ th v vr (Just thAuth) -smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient -smpThHandleClient th v pk_ ck_ = +smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient +smpThHandleClient th v vr pk_ ck_ = let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_ - in smpThHandle_ th v thAuth + in smpThHandle_ th v vr thAuth -smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> THandleSMP c p -smpThHandle_ th@THandle {params} v thAuth = +smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p +smpThHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional - let params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion} + let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion} in (th :: THandleSMP c p) {params = params'} sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO () @@ -546,7 +550,17 @@ getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP smpTHandle :: Transport c => c -> THandleSMP c p smpTHandle c = THandle {connection = c, params} where - params = THandleParams {sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = VersionSMP 0, thAuth = Nothing, implySessId = False, batch = True} + v = VersionSMP 0 + params = + THandleParams + { sessionId = tlsUnique c, + blockSize = smpBlockSize, + thServerVRange = versionToRange v, + thVersion = v, + thAuth = Nothing, + implySessId = False, + batch = True + } $(J.deriveJSON (sumTypeJSON id) ''HandshakeError) diff --git a/src/Simplex/Messaging/Version.hs b/src/Simplex/Messaging/Version.hs index 25f7368d1..5576cfa9f 100644 --- a/src/Simplex/Messaging/Version.hs +++ b/src/Simplex/Messaging/Version.hs @@ -23,6 +23,8 @@ module Simplex.Messaging.Version isCompatibleRange, proveCompatible, compatibleVersion, + compatibleVRange, + compatibleVRange', ) where @@ -98,6 +100,7 @@ class VersionScope v => VersionI v a | a -> v where class VersionScope v => VersionRangeI v a | a -> v where type VersionT v a versionRange :: a -> VersionRange v + toVersionRange :: a -> VersionRange v -> a toVersionT :: a -> Version v -> VersionT v a instance VersionScope v => VersionI v (Version v) where @@ -108,6 +111,7 @@ instance VersionScope v => VersionI v (Version v) where instance VersionScope v => VersionRangeI v (VersionRange v) where type VersionT v (VersionRange v) = Version v versionRange = id + toVersionRange _ vr = vr toVersionT _ v = v newtype Compatible a = Compatible_ a @@ -135,5 +139,24 @@ compatibleVersion x vr = max1 = maxVersion $ versionRange x max2 = maxVersion vr +-- | intersection of version ranges +compatibleVRange :: VersionRangeI v a => a -> VersionRange v -> Maybe (Compatible a) +compatibleVRange x vr = + compatibleVRange_ x (max min1 min2) (min max1 max2) + where + VRange min1 max1 = versionRange x + VRange min2 max2 = vr + +-- | version range capped by compatible version +compatibleVRange' :: VersionRangeI v a => a -> Version v -> Maybe (Compatible a) +compatibleVRange' x v + | v <= max1 = compatibleVRange_ x min1 v + | otherwise = Nothing + where + VRange min1 max1 = versionRange x + +compatibleVRange_ :: VersionRangeI v a => a -> Version v -> Version v -> Maybe (Compatible a) +compatibleVRange_ x v1 v2 = Compatible_ . toVersionRange x <$> safeVersionRange v1 v2 + mkCompatibleIf :: a -> Bool -> Maybe (Compatible a) x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index 6350baa91..caab0637a 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -325,6 +325,7 @@ testTHandleParams v sessionId = { sessionId, blockSize = smpBlockSize, thVersion = v, + thServerVRange = supportedServerSMPRelayVRange, thAuth = Nothing, implySessId = v >= authCmdsSMPVersion, batch = True diff --git a/tests/CoreTests/VersionRangeTests.hs b/tests/CoreTests/VersionRangeTests.hs index cef556376..ff53cc6ca 100644 --- a/tests/CoreTests/VersionRangeTests.hs +++ b/tests/CoreTests/VersionRangeTests.hs @@ -6,6 +6,7 @@ module CoreTests.VersionRangeTests where +import Data.Word (Word16) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.Messaging.Version @@ -38,6 +39,28 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do (vr 1 3, vr 2 3) `compatible` Just (Version 3) (vr 1 3, vr 2 4) `compatible` Just (Version 3) (vr 1 2, vr 3 4) `compatible` Nothing + it "should choose mutually compatible version range (range intersection)" $ do + (vr 1 1, vr 1 1) `compatibleVR` Just (vr 1 1) + (vr 1 1, vr 1 2) `compatibleVR` Just (vr 1 1) + (vr 1 2, vr 1 2) `compatibleVR` Just (vr 1 2) + (vr 1 2, vr 2 3) `compatibleVR` Just (vr 2 2) + (vr 1 3, vr 2 3) `compatibleVR` Just (vr 2 3) + (vr 1 3, vr 2 4) `compatibleVR` Just (vr 2 3) + (vr 1 2, vr 3 4) `compatibleVR` Nothing + it "should choose compatible version range with changed max version (capped range)" $ do + (vr 1 1, 1) `compatibleVR'` Just (vr 1 1) + (vr 1 1, 2) `compatibleVR'` Nothing + (vr 1 2, 2) `compatibleVR'` Just (vr 1 2) + (vr 1 2, 3) `compatibleVR'` Nothing + (vr 1 3, 2) `compatibleVR'` Just (vr 1 2) + (vr 1 3, 3) `compatibleVR'` Just (vr 1 3) + (vr 1 3, 4) `compatibleVR'` Nothing + (vr 2 3, 1) `compatibleVR'` Nothing + (vr 2 3, 2) `compatibleVR'` Just (vr 2 2) + (vr 2 3, 3) `compatibleVR'` Just (vr 2 3) + (vr 2 4, 1) `compatibleVR'` Nothing + (vr 2 4, 3) `compatibleVR'` Just (vr 2 3) + (vr 2 4, 4) `compatibleVR'` Just (vr 2 4) it "should check if version is compatible" $ do isCompatible @T (Version 1) (vr 1 2) `shouldBe` True isCompatible @T (Version 2) (vr 1 2) `shouldBe` True @@ -63,3 +86,16 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do case compatibleVersion vr1 vr2 of Just (Compatible v') -> Just v' `shouldBe` v Nothing -> Nothing `shouldBe` v + compatibleVR :: (VersionRange T, VersionRange T) -> Maybe (VersionRange T) -> Expectation + (vr1, vr2) `compatibleVR` vr' = do + (vr1, vr2) `checkCompatibleVR` vr' + (vr2, vr1) `checkCompatibleVR` vr' + (vr1, vr2) `checkCompatibleVR` vr' = + case compatibleVRange vr1 vr2 of + Just (Compatible vr'') -> Just vr'' `shouldBe` vr' + Nothing -> Nothing `shouldBe` vr' + compatibleVR' :: (VersionRange T, Word16) -> Maybe (VersionRange T) -> Expectation + (vr1, v2) `compatibleVR'` vr' = + case compatibleVRange' vr1 (Version v2) of + Just (Compatible vr'') -> Just vr'' `shouldBe` vr' + Nothing -> Nothing `shouldBe` vr' diff --git a/tests/XFTPClient.hs b/tests/XFTPClient.hs index 5f38cc639..0152625a8 100644 --- a/tests/XFTPClient.hs +++ b/tests/XFTPClient.hs @@ -14,6 +14,7 @@ import Simplex.FileTransfer.Client import Simplex.FileTransfer.Description import Simplex.FileTransfer.Server (runXFTPServerBlocking) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) import Simplex.Messaging.Protocol (XFTPServer) import Simplex.Messaging.Transport (ALPN) import Simplex.Messaging.Transport.Server @@ -118,6 +119,7 @@ testXFTPServerConfig_ alpn = caCertificateFile = "tests/fixtures/ca.crt", privateKeyFile = "tests/fixtures/server.key", certificateFile = "tests/fixtures/server.crt", + xftpServerVRange = supportedFileServerVRange, logStatsInterval = Nothing, logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", From b48215d341f6b7425fa108b88d1ec11990c66ba5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 9 May 2024 15:36:02 +0100 Subject: [PATCH 10/13] proxy: additional configuration for SOCKS proxy usage in SMP proxy client (#1138) * proxy: additional configuration for SOCKS proxy usage in SMP proxy client * update --- src/Simplex/FileTransfer/Client.hs | 6 ++-- src/Simplex/Messaging/Client.hs | 29 ++++++++++++++---- src/Simplex/Messaging/Server/Main.hs | 45 +++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 2e0585e30..7875542a6 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -99,11 +99,11 @@ defaultXFTPClientConfig = getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient) getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do - let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = clientALPN} - http2Config = xftpHTTP2Config tcConfig config - username = proxyUsername transportSession + let username = proxyUsername transportSession ProtocolServer _ host port keyHash = srv useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host + let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN} + http2Config = xftpHTTP2Config tcConfig config clientVar <- newTVarIO Nothing let usePort = if null port then "443" else port clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 59fbd4c12..c931aefc2 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -69,6 +69,7 @@ module Simplex.Messaging.Client NetworkConfig (..), TransportSessionMode (..), HostMode (..), + SocksMode (..), SMPProxyMode (..), SMPProxyFallback (..), defaultClientConfig, @@ -214,10 +215,20 @@ data HostMode HMPublic deriving (Eq, Show) +data SocksMode + = -- | always use SOCKS proxy when enabled + SMAlways + | -- | use SOCKS proxy only for .onion hosts when no public host is available + -- This mode is used in SMP proxy to minimize SOCKS proxy usage. + SMOnion + deriving (Eq, Show) + -- | network configuration for the client data NetworkConfig = NetworkConfig { -- | use SOCKS5 proxy socksProxy :: Maybe SocksProxy, + -- | when to use SOCKS proxy + socksMode :: SocksMode, -- | determines critera which host is chosen from the list hostMode :: HostMode, -- | if above criteria is not met, if the below setting is True return error, otherwise use the first host @@ -267,6 +278,7 @@ defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig { socksProxy = Nothing, + socksMode = SMAlways, hostMode = HMOnionViaSocks, requiredHostMode = False, sessionMode = TSMUser, @@ -282,9 +294,14 @@ defaultNetworkConfig = logTLSErrors = False } -transportClientConfig :: NetworkConfig -> TransportClientConfig -transportClientConfig NetworkConfig {socksProxy, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} = - TransportClientConfig {socksProxy, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing} +transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig +transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host = + TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing} + where + useSocksProxy SMAlways = socksProxy + useSocksProxy SMOnion = case host of + THOnionHost _ -> socksProxy + _ -> Nothing {-# INLINE transportClientConfig #-} -- | protocol client configuration. @@ -298,7 +315,7 @@ data ProtocolClientConfig v = ProtocolClientConfig clientALPN :: Maybe [ALPN], -- | client-server protocol version range serverVRange :: VersionRange v, - -- | agree shared session secret (used in SMP proxy) + -- | agree shared session secret (used in SMP proxy for additional encryption layer) agreeSecret :: Bool } @@ -411,7 +428,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) runClient (port', ATransport t) useHost c = do cVar <- newEmptyTMVarIO - let tcConfig = (transportClientConfig networkConfig) {alpn = clientALPN} + let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN} username = proxyUsername transportSession action <- async $ @@ -994,6 +1011,8 @@ authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_ $(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SM") ''SocksMode) + $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 39f753af0..418ef36c8 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -14,11 +14,12 @@ import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) import Data.Maybe (fromMaybe) +import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Network.Socket (HostName) import Options.Applicative -import Simplex.Messaging.Client (ProtocolClientConfig (..)) +import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig) import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String @@ -132,7 +133,7 @@ smpServerCLI cfgPath logPath = ) <> "\n\n\ \# control_port_admin_password:\n\ - \# control_port_user_password:\n\ + \# control_port_user_password:\n\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" <> ("host: " <> host <> "\n") @@ -140,6 +141,18 @@ smpServerCLI cfgPath logPath = <> "log_tls_errors: off\n\ \websockets: off\n\ \# control_port: 5224\n\n\ + \[PROXY]\n\ + \# Network configuration for SMP proxy client.\n\ + \# `host_mode` can be 'public' (default) or 'onion'.\n\ + \# It defines prefferred hostname for destination servers with multiple hostnames.\n\ + \# host_mode: public\n\ + \# required_host_mode: off\n\n\ + \# SOCKS proxy port for forwarding messages to destination servers.\n\ + \# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\ + \# socks_proxy: localhost:9050\n\n\ + \# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\ + \# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\ + \# socks_mode: onion\n\n\ \[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" @@ -218,9 +231,33 @@ smpServerCLI cfgPath logPath = alpn = Just supportedSMPHandshakes }, controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, agreeSecret = True}}, - allowSMPProxy = True -- TODO: "get from INI" + smpAgentCfg = + defaultSMPClientAgentConfig + { smpCfg = + (smpCfg defaultSMPClientAgentConfig) + { serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, + agreeSecret = True, + networkConfig = + defaultNetworkConfig + { socksProxy = either error id <$> strDecodeIni "PROXY" "socks_proxy" ini, + socksMode = either (const SMOnion) textToSocksMode $ lookupValue "PROXY" "socks_mode" ini, + hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini, + requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini + } + } + }, + allowSMPProxy = True } + textToSocksMode :: Text -> SocksMode + textToSocksMode = \case + "always" -> SMAlways + "onion" -> SMOnion + s -> error . T.unpack $ "Invalid socks_mode: " <> s + textToHostMode :: Text -> HostMode + textToHostMode = \case + "public" -> HMPublic + "onion" -> HMOnionViaSocks + s -> error . T.unpack $ "Invalid host_mode: " <> s data CliCommand = Init InitOptions From b7afb725fd5cdfde67a82303535aea0367121023 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 10 May 2024 10:55:19 +0100 Subject: [PATCH 11/13] proxy: send MWARN event to user on server version or host more errors (#1140) * proxy: include delivery path in SENT event * send MWARN event to user on server version or host more errors * Revert "proxy: include delivery path in SENT event" This reverts commit 5c476718ec84f4b9f7e2eca6a7ac7c3b260c0275. --- src/Simplex/Messaging/Agent.hs | 6 +++++- src/Simplex/Messaging/Agent/Client.hs | 26 ++++++++++++++----------- src/Simplex/Messaging/Agent/Protocol.hs | 7 +++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3a0e13157..be0d35cb4 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1358,7 +1358,11 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork | temporaryOrHostError e -> do let msgTimeout = if msgType == AM_HELLO_ then helloTimeout else messageTimeout expireTs <- addUTCTime (-msgTimeout) <$> liftIO getCurrentTime - if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RIFast + if internalTs < expireTs + then notifyDelMsgs msgId e expireTs + else do + when (serverHostError e) $ notify $ MWARN (unId msgId) e + retrySndMsg RIFast | otherwise -> notifyDel msgId err where retrySndMsg riMode = do diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 133fae96a..f2c43fc67 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -46,6 +46,7 @@ module Simplex.Messaging.Agent.Client sendInvitation, temporaryAgentError, temporaryOrHostError, + serverHostError, secureQueue, enableQueueNotifications, enableQueuesNtfs, @@ -616,7 +617,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq Left e -> do liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e atomically $ do - unless (persistentProxyError e) $ do + unless (serverHostError e) $ do removeSessVar rv destSrv prs TM.delete destSess smpProxiedRelays putTMVar (sessionVar rv) (Left e) @@ -1060,7 +1061,7 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do case r of Right r' -> pure r' Left e - | persistentProxyError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) + | serverHostError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) | otherwise -> throwE e sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> @@ -1303,17 +1304,20 @@ temporaryAgentError = \case _ -> False temporaryOrHostError :: AgentErrorType -> Bool -temporaryOrHostError = \case - BROKER _ HOST -> True - SMP (SMP.PROXY (SMP.BROKER HOST)) -> True - PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER HOST))) -> True - e -> temporaryAgentError e +temporaryOrHostError e = temporaryAgentError e || serverHostError e +{-# INLINE temporaryOrHostError #-} -persistentProxyError :: AgentErrorType -> Bool -persistentProxyError = \case - BROKER _ (SMP.TRANSPORT TEVersion) -> True - SMP (SMP.PROXY (SMP.BROKER (SMP.TRANSPORT TEVersion))) -> True +serverHostError :: AgentErrorType -> Bool +serverHostError = \case + BROKER _ e -> brokerHostError e + SMP (SMP.PROXY (SMP.BROKER e)) -> brokerHostError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerHostError e _ -> False + where + brokerHostError = \case + HOST -> True + SMP.TRANSPORT TEVersion -> True + _ -> False -- | Subscribe to queues. The list of results can have a different order. subscribeQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 7160863f1..a18f64064 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -396,6 +396,7 @@ data ACommand (p :: AParty) (e :: AEntity) where SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn SENT :: AgentMsgId -> Maybe SMPServer -> ACommand Agent AEConn + MWARN :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn @@ -459,6 +460,7 @@ data ACommandTag (p :: AParty) (e :: AEntity) where SEND_ :: ACommandTag Client AEConn MID_ :: ACommandTag Agent AEConn SENT_ :: ACommandTag Agent AEConn + MWARN_ :: ACommandTag Agent AEConn MERR_ :: ACommandTag Agent AEConn MERRS_ :: ACommandTag Agent AEConn MSG_ :: ACommandTag Agent AEConn @@ -515,6 +517,7 @@ aCommandTag = \case SEND {} -> SEND_ MID {} -> MID_ SENT {} -> SENT_ + MWARN {} -> MWARN_ MERR {} -> MERR_ MERRS {} -> MERRS_ MSG {} -> MSG_ @@ -1668,6 +1671,7 @@ instance StrEncoding ACmdTag where "SEND" -> t SEND_ "MID" -> ct MID_ "SENT" -> ct SENT_ + "MWARN" -> ct MWARN_ "MERR" -> ct MERR_ "MERRS" -> ct MERRS_ "MSG" -> ct MSG_ @@ -1726,6 +1730,7 @@ instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where SEND_ -> "SEND" MID_ -> "MID" SENT_ -> "SENT" + MWARN_ -> "MWARN" MERR_ -> "MERR" MERRS_ -> "MERRS" MSG_ -> "MSG" @@ -1797,6 +1802,7 @@ commandP binaryP = RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP) MID_ -> s (MID <$> A.decimal <*> _strP) SENT_ -> s (SENT <$> A.decimal <*> _strP) + MWARN_ -> s (MWARN <$> A.decimal <* A.space <*> strP) MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP) MERRS_ -> s (MERRS <$> strP_ <*> strP) MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP) @@ -1860,6 +1866,7 @@ serializeCommand = \case SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody] MID mId pqEnc -> s (MID_, mId, pqEnc) SENT mId proxySrv_ -> s (SENT_, mId, proxySrv_) + MWARN mId e -> s (MWARN_, mId, e) MERR mId e -> s (MERR_, mId, e) MERRS mIds e -> s (MERRS_, mIds, e) MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody] From 8516b0dd5b8bdee600fdcfca43d6e15b97bbf1c0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 11 May 2024 17:11:28 +0100 Subject: [PATCH 12/13] proxy: negotiate client-relay version, include it in PFWD commands and in encrypted forwarded transmissions (#1144) * proxy: negotiate client-relay version, include it in PFWD commands and in encrypted forwarded transmissions * rename * inline * comment * use correct server version when encoding forwarded commands --- src/Simplex/Messaging/Client.hs | 17 +++++----- src/Simplex/Messaging/Protocol.hs | 17 +++++----- src/Simplex/Messaging/Server.hs | 25 +++++++------- src/Simplex/Messaging/Server/Env/STM.hs | 6 ++-- src/Simplex/Messaging/Transport.hs | 44 ++++++++++++++++++------- 5 files changed, 65 insertions(+), 44 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index c931aefc2..92dd4047b 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -819,11 +819,10 @@ proxySMPMessage :: MsgFlags -> MsgBody -> ExceptT SMPClientError IO (Either ProxyClientError ()) --- TODO use version -proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId _v serverKey) spKey sId flags msg = do +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams - serverThParams = proxyThParams {sessionId, thAuth = serverThAuth} + serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth} (cmdPubKey, cmdPrivKey) <- liftIO . atomically $ C.generateKeyPair @'C.X25519 g let cmdSecret = C.dh' serverKey cmdPrivKey nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g @@ -837,13 +836,13 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c TBTransmissions s _ _ : _ -> pure s et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength -- proxy interaction errors are wrapped - tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et))) >>= \case + tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case Right r -> case r of PRES (EncResponse er) -> do -- server interaction errors are thrown directly t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er - case tParse proxyThParams t' of - t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of + case tParse serverThParams t' of + t'' :| [] -> case tDecodeParseValidate serverThParams t'' of (_auth, _signed, (_c, _e, cmd)) -> case cmd of Right OK -> pure $ Right () Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay @@ -862,15 +861,15 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c -- sends RFWD :: EncFwdTransmission -> Command Sender -- receives RRES :: EncFwdResponse -> BrokerMsg -- proxy should send PRES to the client with EncResponse -forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse -forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do +forwardSMPMessage :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse +forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of Nothing -> throwError $ PCETransportError TENoServerAuth Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap - let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} + let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission} eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) -- send sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 250c76fcf..1812dce37 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -251,7 +251,7 @@ maxMessageLength v | otherwise = 16088 -- 16064 - always use this size to determine allowed ranges paddedProxiedMsgLength :: Int -paddedProxiedMsgLength = 16244 -- 16241 .. 16245 +paddedProxiedMsgLength = 16242 -- 16241 .. 16243 -- TODO v6.0 change to 16064 type MaxMessageLen = 16088 @@ -399,7 +399,7 @@ data Command (p :: Party) where -- - corrId: also used as a nonce to encrypt transmission to relay, corrId + 1 - from relay -- - key (1st param in the command) is used to agree DH secret for this particular transmission and its response -- Encrypted transmission should include session ID (tlsunique) from proxy-relay connection. - PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command ProxiedClient -- use CorrId as CbNonce, client to proxy + PFWD :: VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> Command ProxiedClient -- use CorrId as CbNonce, client to proxy -- Transmission forwarded to relay: -- - entity ID: empty -- - corrId: unique correlation ID between proxy and relay, also used as a nonce to encrypt forwarded transmission @@ -434,16 +434,17 @@ newtype EncTransmission = EncTransmission ByteString data FwdTransmission = FwdTransmission { fwdCorrId :: CorrId, + fwdVersion :: VersionSMP, fwdKey :: C.PublicKeyX25519, fwdTransmission :: EncTransmission } instance Encoding FwdTransmission where - smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} = - smpEncode (corrId, fwdKey, Tail t) + smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} = + smpEncode (corrId, fwdVersion, fwdKey, Tail t) smpP = do - (corrId, fwdKey, Tail t) <- smpP - pure FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} + (corrId, fwdVersion, fwdKey, Tail t) <- smpP + pure FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} newtype EncFwdTransmission = EncFwdTransmission ByteString deriving (Show) @@ -1278,7 +1279,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where PING -> e PING_ NSUB -> e NSUB_ PRXY host auth_ -> e (PRXY_, ' ', host, auth_) - PFWD pubKey (EncTransmission s) -> e (PFWD_, ' ', pubKey, Tail s) + PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s) RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s) where e :: Encoding a => a -> ByteString @@ -1346,7 +1347,7 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where RFWD_ -> RFWD <$> (EncFwdTransmission . unTail <$> _smpP) CT SProxiedClient tag -> Cmd SProxiedClient <$> case tag of - PFWD_ -> PFWD <$> _smpP <*> (EncTransmission . unTail <$> smpP) + PFWD_ -> PFWD <$> _smpP <*> smpP <*> (EncTransmission . unTail <$> smpP) PRXY_ -> PRXY <$> _smpP <*> smpP CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 156f5da18..85a9073bc 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -89,6 +89,7 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Buffer (trimCR) import Simplex.Messaging.Transport.Server import Simplex.Messaging.Util +import Simplex.Messaging.Version import System.Exit (exitFailure) import System.IO (hPrint, hPutStrLn, hSetNewlineMode, universalNewlineMode) import System.Mem.Weak (deRefWeak) @@ -650,20 +651,21 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi proxyResp = \case Left err -> ERR $ smpProxyError err Right smp -> - let THandleParams {sessionId = srvSessId, thVersion, thAuth} = thParams smp - vr = supportedServerSMPRelayVRange -- TODO this should be destination relay version range - in if thVersion >= sendingProxySMPVersion - then case thAuth of + let THandleParams {sessionId = srvSessId, thVersion, thServerVRange, thAuth} = thParams smp + in case compatibleVRange thServerVRange proxiedSMPRelayVRange of + -- Cap the destination relay version range to prevent client version fingerprinting. + -- See comment for proxiedSMPRelayVersion. + Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey Nothing -> ERR $ transportErr TENoServerAuth - else ERR $ transportErr TEVersion - PFWD pubKey encBlock -> do + _ -> ERR $ transportErr TEVersion + PFWD fwdV pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case Just smp | v >= sendingProxySMPVersion -> liftIO $ either (ERR . smpProxyError) PRES <$> - runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) + runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catchError` (pure . Left . PCEIOError) | otherwise -> pure . ERR $ transportErr TEVersion where THandleParams {thVersion = v} = thParams smp @@ -952,13 +954,14 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s - FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' + FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' let clientSecret = C.dh' fwdKey serverPrivKey clientNonce = C.cbNonce $ bs fwdCorrId b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et + let clntTHParams = smpTHParamsSetVersion fwdVersion thParams' -- only allowing single forwarded transactions - t' <- case tParse thParams' b of - t :| [] -> pure $ tDecodeParseValidate thParams' t + t' <- case tParse clntTHParams b of + t :| [] -> pure $ tDecodeParseValidate clntTHParams t _ -> throwE BLOCK let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} -- process forwarded SEND @@ -972,7 +975,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi _ -> pure (corrId', entId', ERR $ CMD PROHIBITED) -- encode response - r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of + r' <- case batchTransmissions (batch clntTHParams) (blockSize clntTHParams) [Right (Nothing, encodeTransmission clntTHParams r)] of [] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right TBError _ _ : _ -> throwE BLOCK TBTransmission b' _ : _ -> pure b' diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 845d483b1..f23192aeb 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -146,8 +146,7 @@ data Client = Client connected :: TVar Bool, createdAt :: SystemTime, rcvActiveAt :: TVar SystemTime, - sndActiveAt :: TVar SystemTime, - proxyClient_ :: TVar (Maybe C.DhSecretX25519) -- this client is actually an SMP proxy + sndActiveAt :: TVar SystemTime } data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub @@ -179,8 +178,7 @@ newClient nextClientId qSize thVersion sessionId createdAt = do connected <- newTVar True rcvActiveAt <- newTVar createdAt sndActiveAt <- newTVar createdAt - proxyClient_ <- newTVar Nothing - return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, proxyClient_} + return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} newSubscription :: SubscriptionThread -> STM Sub newSubscription subThread = do diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index f12168840..561e9c4de 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -36,6 +36,7 @@ module Simplex.Messaging.Transport supportedSMPHandshakes, supportedClientSMPRelayVRange, supportedServerSMPRelayVRange, + proxiedSMPRelayVRange, legacyServerSMPRelayVRange, currentClientSMPRelayVersion, legacyServerSMPRelayVersion, @@ -77,6 +78,7 @@ module Simplex.Messaging.Transport tGetBlock, sendHandshake, getHandshake, + smpTHParamsSetVersion, ) where @@ -155,13 +157,21 @@ sendingProxySMPVersion :: VersionSMP sendingProxySMPVersion = VersionSMP 8 currentClientSMPRelayVersion :: VersionSMP -currentClientSMPRelayVersion = VersionSMP 7 +currentClientSMPRelayVersion = VersionSMP 8 legacyServerSMPRelayVersion :: VersionSMP legacyServerSMPRelayVersion = VersionSMP 6 currentServerSMPRelayVersion :: VersionSMP -currentServerSMPRelayVersion = VersionSMP 7 +currentServerSMPRelayVersion = VersionSMP 8 + +-- Max SMP protocol version to be used in e2e encrypted +-- connection between client and server, as defined by SMP proxy. +-- 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. +proxiedSMPRelayVersion :: VersionSMP +proxiedSMPRelayVersion = VersionSMP 8 -- minimal supported protocol version is 4 -- TODO remove code that supports sending commands without batching @@ -174,6 +184,10 @@ legacyServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion legacyServerSMPR supportedServerSMPRelayVRange :: VersionRangeSMP supportedServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentServerSMPRelayVersion +-- This range initially allows only version 8 - see the comment above. +proxiedSMPRelayVRange :: VersionRangeSMP +proxiedSMPRelayVRange = mkVersionRange sendingProxySMPVersion proxiedSMPRelayVersion + supportedSMPHandshakes :: [ALPN] supportedSMPHandshakes = ["smp/1"] @@ -497,7 +511,7 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do throwE $ TEHandshake IDENTITY | otherwise -> case compatibleVRange' smpVersionRange v of - Just (Compatible vr) -> pure $ smpThHandleServer th v vr pk k' + Just (Compatible vr) -> pure $ smpTHandleServer th v vr pk k' Nothing -> throwE TEVersion -- | Client SMP transport handshake. @@ -521,25 +535,31 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) let v = maxVersion vr sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_} - pure $ smpThHandleClient th v vr (snd <$> ks_) ck_ + pure $ smpTHandleClient th v vr (snd <$> ks_) ck_ Nothing -> throwE TEVersion -smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer -smpThHandleServer th v vr pk k_ = +smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer +smpTHandleServer th v vr pk k_ = let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_} - in smpThHandle_ th v vr (Just thAuth) + in smpTHandle_ th v vr (Just thAuth) -smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient -smpThHandleClient th v vr pk_ ck_ = +smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient +smpTHandleClient th v vr pk_ ck_ = let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_ - in smpThHandle_ th v vr thAuth + in smpTHandle_ th v vr thAuth -smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p -smpThHandle_ th@THandle {params} v vr thAuth = +smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p +smpTHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion} in (th :: THandleSMP c p) {params = params'} +-- This function is only used with v >= 8, so currently it's a simple record update. +-- It may require some parameters update in the future, to be consistent with smpTHandle_. +smpTHParamsSetVersion :: VersionSMP -> THandleParams SMPVersion p -> THandleParams SMPVersion p +smpTHParamsSetVersion v params = params {thVersion = v} +{-# INLINE smpTHParamsSetVersion #-} + sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO () sendHandshake th = ExceptT . tPutBlock th . smpEncode From 66c916dbb33e625cc40cc43aa2aededab859156b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 12 May 2024 21:12:01 +0100 Subject: [PATCH 13/13] proxy: increase client timeout for proxied commands (#1145) --- src/Simplex/Messaging/Client.hs | 35 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 92dd4047b..62d75c3e2 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -143,6 +143,7 @@ data PClient v err msg = PClient { connected :: TVar Bool, transportSession :: TransportSession msg, transportHost :: TransportHost, + tcpConnectTimeout :: Int, tcpTimeout :: Int, rcvConcurrency :: Int, sendPings :: TVar Bool, @@ -185,6 +186,7 @@ smpClientStub g sessionId thVersion thAuth = do { connected, transportSession = (1, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001", Nothing), transportHost = "localhost", + tcpConnectTimeout = 20_000_000, tcpTimeout = 15_000_000, rcvConcurrency = 8, sendPings, @@ -413,6 +415,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize { connected, transportSession, transportHost, + tcpConnectTimeout, tcpTimeout, sendPings, lastReceived, @@ -735,9 +738,9 @@ deleteSMPQueues = okSMPCommands DEL -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay -connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth +connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout}} relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth | thVersion (thParams c) >= sendingProxySMPVersion = - sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case + sendProtocolCommand_ c Nothing tOut Nothing "" (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case PKEY sId vr (chain, key) -> case supportedClientSMPRelayVRange `compatibleVersion` vr of Nothing -> throwE $ transportErr TEVersion @@ -745,6 +748,7 @@ connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxy r -> throwE . PCEUnexpectedResponse $ bshow r | otherwise = throwE $ PCETransportError TEVersion where + tOut = Just $ tcpConnectTimeout + tcpTimeout transportErr = PCEProtocolError . PROXY . BROKER . TRANSPORT validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 validateRelay (X.CertificateChain cert) exact = do @@ -819,7 +823,7 @@ proxySMPMessage :: MsgFlags -> MsgBody -> ExceptT SMPClientError IO (Either ProxyClientError ()) -proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth} @@ -827,7 +831,7 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c let cmdSecret = C.dh' serverKey cmdPrivKey nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g -- encode - let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg) + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender (SEND flags msg)) auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of [] -> throwE $ PCETransportError TELargeMsg @@ -836,7 +840,8 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c TBTransmissions s _ _ : _ -> pure s et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength -- proxy interaction errors are wrapped - tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case + let tOut = Just $ 2 * tcpTimeout + tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case Right r -> case r of PRES (EncResponse er) -> do -- server interaction errors are thrown directly @@ -872,7 +877,7 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission} eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) -- send - sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case + sendProtocolCommand_ c (Just nonce) Nothing Nothing "" (Cmd SSender (RFWD eft)) >>= \case RRES (EncFwdResponse efr) -> do -- unwrap r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr @@ -936,19 +941,19 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do | n > 0 -> do active <- newTVarIO True atomically $ writeTBQueue sndQ (active, s) - mapConcurrently (getResponse c active) rs + mapConcurrently (getResponse c Nothing active) rs | otherwise -> pure [] TBTransmission s r -> do active <- newTVarIO True atomically $ writeTBQueue sndQ (active, s) - (: []) <$> getResponse c active r + (: []) <$> getResponse c Nothing active r -- | Send Protocol command sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg -sendProtocolCommand c = sendProtocolCommand_ c Nothing +sendProtocolCommand c = sendProtocolCommand_ c Nothing Nothing -sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg -sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ pKey entId cmd = +sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe Int -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg +sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ tOut pKey entId cmd = ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd) where -- two separate "atomically" needed to avoid blocking @@ -960,17 +965,17 @@ sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THan | otherwise -> do active <- newTVarIO True atomically (writeTBQueue sndQ (active, s)) - response <$> getResponse c active r + response <$> getResponse c tOut active r where s | batch = tEncodeBatch1 t | otherwise = tEncode t -- TODO switch to timeout or TimeManager that supports Int64 -getResponse :: ProtocolClient v err msg -> TVar Bool -> Request err msg -> IO (Response err msg) -getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount, sentCommands}} active Request {corrId, entityId, responseVar} = do +getResponse :: ProtocolClient v err msg -> Maybe Int -> TVar Bool -> Request err msg -> IO (Response err msg) +getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount, sentCommands}} tOut active Request {corrId, entityId, responseVar} = do response <- - timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case + fromMaybe tcpTimeout tOut `timeout` atomically (takeTMVar responseVar) >>= \case Just r -> atomically (writeTVar timeoutErrorCount 0) $> r Nothing -> do atomically (writeTVar active False >> TM.delete corrId sentCommands)