Files
simplexmq/tests/RemoteControl.hs
Evgeny af9ca59e51 smp server: optimize concurrency and memory usage, refactor (#1544)
* smp server: optimize concurrency and memory usage, refactor

* hide clients IntMap

* reduce STM contention

* comment

* version

* correct stats for subscriptions

* version

* comment

* remove subscribed clients from map

* version

* optimze, refactor

* version

* debug test

* enable all tests

* remove test logs

* retry failed tests with debug logging

* increase test timeout

* sync between tests
2025-05-23 12:52:18 +01:00

212 lines
8.3 KiB
Haskell

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module RemoteControl where
import AgentTests.FunctionalAPITests (runRight)
import Control.Logger.Simple
import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson as J
import Data.ByteString.Lazy.Char8 as LB
import Data.List.NonEmpty (NonEmpty (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Transport (TSbChainKeys (..))
import qualified Simplex.RemoteControl.Client as HC (RCHostClient (action))
import qualified Simplex.RemoteControl.Client as RC
import Simplex.RemoteControl.Discovery (mkLastLocalHost, preferAddress)
import Simplex.RemoteControl.Invitation (RCSignedInvitation, verifySignedInvitation)
import Simplex.RemoteControl.Types
import Test.Hspec hiding (fit, it)
import UnliftIO
import UnliftIO.Concurrent
import Util
remoteControlTests :: Spec
remoteControlTests = do
describe "preferred bindings should go first" testPreferAddress
describe "New controller/host pairing" $ do
it "should connect to new pairing" testNewPairing
it "should connect to existing pairing" testExistingPairing
describe "Multicast discovery" $ do
it "should find paired host and connect" testMulticast
testPreferAddress :: Spec
testPreferAddress = do
it "suppresses localhost" $
mkLastLocalHost addrs
`shouldBe` [ "10.20.30.40" `on` "eth0",
"10.20.30.42" `on` "wlan0",
"127.0.0.1" `on` "lo"
]
it "finds by address" $ do
preferAddress ("127.0.0.1" `on` "lo23") addrs' `shouldBe` addrs -- localhost is back on top
preferAddress ("10.20.30.42" `on` "wlp2s0") addrs'
`shouldBe` [ "10.20.30.42" `on` "wlan0",
"10.20.30.40" `on` "eth0",
"127.0.0.1" `on` "lo"
]
it "finds by interface" $ do
preferAddress ("127.1.2.3" `on` "lo") addrs' `shouldBe` addrs
preferAddress ("0.0.0.0" `on` "eth0") addrs' `shouldBe` addrs'
it "survives duplicates" $ do
preferAddress ("0.0.0.0" `on` "eth1") addrsDups `shouldBe` addrsDups
preferAddress ("0.0.0.0" `on` "eth0") ifaceDups `shouldBe` ifaceDups
where
on th interface = RCCtrlAddress {address = either error id $ strDecode th, interface}
addrs =
[ "127.0.0.1" `on` "lo", -- localhost may go first and break things
"10.20.30.40" `on` "eth0",
"10.20.30.42" `on` "wlan0"
]
addrs' = mkLastLocalHost addrs
addrsDups = "10.20.30.40" `on` "eth1" : addrs'
ifaceDups = "10.20.30.41" `on` "eth0" : addrs'
testNewPairing :: IO ()
testNewPairing = do
drg <- C.newRandom
hp <- RC.newRCHostPairing drg
invVar <- newEmptyMVar
ctrlSessId <- async . runRight $ do
logNote "c 1"
(_found, inv, hc, r) <- RC.connectRCHost drg hp (J.String "app") False Nothing Nothing
logNote "c 2"
putMVar invVar (inv, hc)
logNote "c 3"
Right (sessId, _tls, r') <- atomically $ takeTMVar r
logNote "c 4"
Right (rcHostSession, _rcHelloBody, _hp') <- atomically $ takeTMVar r'
let RCHostSession {tls, sessionKeys = HostSessKeys {chainKeys}} = rcHostSession
TSbChainKeys {rcvKey, sndKey} = chainKeys
sndKeyNonce <- atomically $ stateTVar sndKey C.sbcHkdf
encCmd <- RC.rcEncryptBody sndKeyNonce "command message"
RC.sendRCPacket tls $ LB.toStrict encCmd
encResp <- RC.receiveRCPacket tls
rcvKeyNonce <- atomically $ stateTVar rcvKey C.sbcHkdf
resp <- RC.rcDecryptBody rcvKeyNonce $ LB.fromStrict encResp
liftIO $ resp `shouldBe` "response message"
logNote "c 5"
threadDelay 250000
logNote "ctrl: ciao"
liftIO $ RC.cancelHostClient hc
pure sessId
(signedInv, hc) <- takeMVar invVar
-- logNote $ decodeUtf8 $ strEncode inv
inv <- maybe (fail "bad invite") pure $ verifySignedInvitation signedInv
hostSessId <- async . runRight $ do
logNote "h 1"
(rcCtrlClient, r) <- RC.connectRCCtrl drg inv Nothing (J.String "app")
logNote "h 2"
Right (sessId', _tls, r') <- atomically $ takeTMVar r
logNote "h 3"
liftIO $ RC.confirmCtrlSession rcCtrlClient True
logNote "h 4"
Right (rcCtrlSession, _rcCtrlPairing) <- atomically $ takeTMVar r'
let RCCtrlSession {tls, sessionKeys = CtrlSessKeys {chainKeys}} = rcCtrlSession
TSbChainKeys {rcvKey, sndKey} = chainKeys
encCmd <- RC.receiveRCPacket tls
rcvKeyNonce <- atomically $ stateTVar rcvKey C.sbcHkdf
cmd <- RC.rcDecryptBody rcvKeyNonce $ LB.fromStrict encCmd
liftIO $ cmd `shouldBe` "command message"
sndKeyNonce <- atomically $ stateTVar sndKey C.sbcHkdf
encResp <- RC.rcEncryptBody sndKeyNonce "response message"
RC.sendRCPacket tls $ LB.toStrict encResp
logNote "h 5"
threadDelay 250000
logNote "ctrl: adios"
pure sessId'
waitCatch (HC.action hc) >>= \case
Left err -> fromException err `shouldBe` Just AsyncCancelled
Right () -> fail "Unexpected controller finish"
timeout 5000000 (waitBoth ctrlSessId hostSessId) >>= \case
Just (sessId, sessId') -> sessId `shouldBe` sessId'
_ -> fail "timeout"
testExistingPairing :: IO ()
testExistingPairing = do
drg <- C.newRandom
invVar <- newEmptyMVar
hp <- RC.newRCHostPairing drg
ctrl <- runCtrl drg False hp invVar
inv <- takeMVar invVar
let cp_ = Nothing
host <- runHostURI drg cp_ inv
timeout 5000000 (waitBoth ctrl host) >>= \case
Nothing -> fail "timeout"
Just (hp', cp') -> do
ctrl' <- runCtrl drg False hp' invVar
inv' <- takeMVar invVar
host' <- runHostURI drg (Just cp') inv'
timeout 5000000 (waitBoth ctrl' host') >>= \case
Nothing -> fail "timeout"
Just (_hp2, cp2) -> do
ctrl2 <- runCtrl drg False hp' invVar -- old host pairing used to test controller not updating state
inv2 <- takeMVar invVar
host2 <- runHostURI drg (Just cp2) inv2
timeout 5000000 (waitBoth ctrl2 host2) >>= \case
Nothing -> fail "timeout"
Just (hp3, cp3) -> do
ctrl3 <- runCtrl drg False hp3 invVar
inv3 <- takeMVar invVar
host3 <- runHostURI drg (Just cp3) inv3
timeout 5000000 (waitBoth ctrl3 host3) >>= \case
Nothing -> fail "timeout"
Just _ -> pure ()
testMulticast :: IO ()
testMulticast = do
drg <- C.newRandom
subscribers <- newTMVarIO 0
invVar <- newEmptyMVar
hp <- RC.newRCHostPairing drg
ctrl <- runCtrl drg False hp invVar
inv <- takeMVar invVar
let cp_ = Nothing
host <- runHostURI drg cp_ inv
timeout 5000000 (waitBoth ctrl host) >>= \case
Nothing -> fail "timeout"
Just (hp', cp') -> do
ctrl' <- runCtrl drg True hp' invVar
_inv <- takeMVar invVar
host' <- runHostMulticast drg subscribers cp'
timeout 5000000 (waitBoth ctrl' host') >>= \case
Nothing -> fail "timeout"
Just _ -> pure ()
runCtrl :: TVar ChaChaDRG -> Bool -> RCHostPairing -> MVar RCSignedInvitation -> IO (Async RCHostPairing)
runCtrl drg multicast hp invVar = async . runRight $ do
(_found, inv, hc, r) <- RC.connectRCHost drg hp (J.String "app") multicast Nothing Nothing
putMVar invVar inv
Right (_sessId, _tls, r') <- atomically $ takeTMVar r
Right (_rcHostSession, _rcHelloBody, hp') <- atomically $ takeTMVar r'
threadDelay 250000
liftIO $ RC.cancelHostClient hc
pure hp'
runHostURI :: TVar ChaChaDRG -> Maybe RCCtrlPairing -> RCSignedInvitation -> IO (Async RCCtrlPairing)
runHostURI drg cp_ signedInv = async . runRight $ do
inv <- maybe (fail "bad invite") pure $ verifySignedInvitation signedInv
(rcCtrlClient, r) <- RC.connectRCCtrl drg inv cp_ (J.String "app")
Right (_sessId', _tls, r') <- atomically $ takeTMVar r
liftIO $ RC.confirmCtrlSession rcCtrlClient True
Right (_rcCtrlSession, cp') <- atomically $ takeTMVar r'
threadDelay 250000
pure cp'
runHostMulticast :: TVar ChaChaDRG -> TMVar Int -> RCCtrlPairing -> IO (Async RCCtrlPairing)
runHostMulticast drg subscribers cp = async . runRight $ do
(pairing, inv) <- RC.discoverRCCtrl subscribers (cp :| [])
(rcCtrlClient, r) <- RC.connectRCCtrl drg inv (Just pairing) (J.String "app")
Right (_sessId', _tls, r') <- atomically $ takeTMVar r
liftIO $ RC.confirmCtrlSession rcCtrlClient True
Right (_rcCtrlSession, cp') <- atomically $ takeTMVar r'
threadDelay 250000
pure cp'