/ test / Network / Cardano / Mux / TcpSpec.hs
TcpSpec.hs
  1  {-# LANGUAGE OverloadedStrings #-}
  2  {-# LANGUAGE ScopedTypeVariables #-}
  3  
  4  module Network.Cardano.Mux.TcpSpec (spec) where
  5  
  6  import Test.Hspec
  7  
  8  import Control.Concurrent (forkIO)
  9  import Control.Concurrent.MVar
 10  
 11  import Network.Cardano.Mux.Transport
 12  import Network.Cardano.Mux.Transport.Tcp
 13  import Network.Cardano.Protocol.Handshake
 14  import Network.Cardano.Protocol.Handshake.Codec
 15  import Network.Cardano.Protocol.Handshake.Types
 16  
 17  spec :: Spec
 18  spec = describe "TCP Transport Integration" $ do
 19    describe "Basic Transport" $ do
 20      it "can send and receive data between client and server" $ do
 21        withServer 0 $ \listenSock port -> do
 22          -- Start a thread to accept connection
 23          serverResult <- newEmptyMVar
 24          _ <- forkIO $ do
 25            serverT <- acceptConnection listenSock
 26            -- Read what client sends
 27            mData <- transportRead serverT 13
 28            putMVar serverResult mData
 29            transportClose serverT
 30  
 31          -- Connect as client and send data
 32          clientT <- connectToServer "127.0.0.1" port
 33          transportWrite clientT "Hello, Server"
 34          transportClose clientT
 35  
 36          -- Check server received it
 37          result <- takeMVar serverResult
 38          result `shouldBe` Just "Hello, Server"
 39  
 40      it "handles bidirectional communication" $ do
 41        withServer 0 $ \listenSock port -> do
 42          serverResult <- newEmptyMVar
 43          clientResult <- newEmptyMVar
 44  
 45          -- Server thread
 46          _ <- forkIO $ do
 47            serverT <- acceptConnection listenSock
 48            -- Read from client
 49            mData <- transportRead serverT 5
 50            -- Send response
 51            transportWrite serverT "PONG"
 52            putMVar serverResult mData
 53            transportClose serverT
 54  
 55          -- Client
 56          clientT <- connectToServer "127.0.0.1" port
 57          transportWrite clientT "PING!"
 58          -- Read response
 59          mResp <- transportRead clientT 4
 60          putMVar clientResult mResp
 61          transportClose clientT
 62  
 63          takeMVar serverResult `shouldReturn` Just "PING!"
 64          takeMVar clientResult `shouldReturn` Just "PONG"
 65  
 66    describe "Handshake Protocol over TCP" $ do
 67      it "server-initiator and client-responder complete handshake" $ do
 68        testHandshake ServerInitiator
 69  
 70      it "server-responder and client-initiator complete handshake" $ do
 71        testHandshake ServerResponder
 72  
 73      it "client-initiator and server-responder complete handshake" $ do
 74        -- Same as server-responder, but emphasizing client perspective
 75        testHandshake ClientInitiator
 76  
 77      it "client-responder and server-initiator complete handshake" $ do
 78        -- Same as server-initiator, but emphasizing client perspective
 79        testHandshake ClientResponder
 80  
 81      it "handshake fails when no common version" $ do
 82        testHandshakeNoCommonVersion
 83  
 84  -- | Configuration for which side plays which role
 85  data TestRole
 86    = ServerInitiator  -- Server sends ProposeVersions, Client responds
 87    | ServerResponder  -- Client sends ProposeVersions, Server responds
 88    | ClientInitiator  -- Same as ServerResponder (client initiates)
 89    | ClientResponder  -- Same as ServerInitiator (server initiates)
 90  
 91  -- | Run a handshake test with specified role configuration.
 92  testHandshake :: TestRole -> IO ()
 93  testHandshake role = do
 94    let initiatorConfig = HandshakeConfig
 95          { hcNetworkMagic = 764824073
 96          , hcSupportedVersions = [V10, V11, V12]
 97          , hcInitiatorOnly = False
 98          , hcPeerSharing = PeerSharingDisabled
 99          }
100        responderConfig = HandshakeConfig
101          { hcNetworkMagic = 764824073
102          , hcSupportedVersions = [V10, V11]
103          , hcInitiatorOnly = False
104          , hcPeerSharing = PeerSharingDisabled
105          }
106  
107    withServer 0 $ \listenSock port -> do
108      serverResult <- newEmptyMVar
109      clientResult <- newEmptyMVar
110  
111      case role of
112        ServerInitiator -> do
113          -- Server thread: initiator
114          _ <- forkIO $ do
115            serverT <- acceptConnection listenSock
116            result <- runInitiator serverT initiatorConfig
117            putMVar serverResult result
118            transportClose serverT
119  
120          -- Client: responder
121          clientT <- connectToServer "127.0.0.1" port
122          result <- runResponder clientT responderConfig
123          putMVar clientResult result
124          transportClose clientT
125  
126        ServerResponder -> do
127          -- Server thread: responder
128          _ <- forkIO $ do
129            serverT <- acceptConnection listenSock
130            result <- runResponder serverT responderConfig
131            putMVar serverResult result
132            transportClose serverT
133  
134          -- Client: initiator
135          clientT <- connectToServer "127.0.0.1" port
136          result <- runInitiator clientT initiatorConfig
137          putMVar clientResult result
138          transportClose clientT
139  
140        ClientInitiator -> do
141          -- Same as ServerResponder
142          _ <- forkIO $ do
143            serverT <- acceptConnection listenSock
144            result <- runResponder serverT responderConfig
145            putMVar serverResult result
146            transportClose serverT
147  
148          clientT <- connectToServer "127.0.0.1" port
149          result <- runInitiator clientT initiatorConfig
150          putMVar clientResult result
151          transportClose clientT
152  
153        ClientResponder -> do
154          -- Same as ServerInitiator
155          _ <- forkIO $ do
156            serverT <- acceptConnection listenSock
157            result <- runInitiator serverT initiatorConfig
158            putMVar serverResult result
159            transportClose serverT
160  
161          clientT <- connectToServer "127.0.0.1" port
162          result <- runResponder clientT responderConfig
163          putMVar clientResult result
164          transportClose clientT
165  
166      sRes <- takeMVar serverResult
167      cRes <- takeMVar clientResult
168  
169      -- Both should succeed with V11 (highest common version)
170      case (sRes, cRes) of
171        (Right sHr, Right cHr) -> do
172          hrVersion sHr `shouldBe` V11
173          hrVersion cHr `shouldBe` V11
174        (Left err, _) -> expectationFailure $ "Server failed: " ++ show err
175        (_, Left err) -> expectationFailure $ "Client failed: " ++ show err
176  
177  -- | Test that handshake fails when there's no common version.
178  testHandshakeNoCommonVersion :: IO ()
179  testHandshakeNoCommonVersion = do
180    let initiatorConfig = HandshakeConfig
181          { hcNetworkMagic = 764824073
182          , hcSupportedVersions = [V7, V8]
183          , hcInitiatorOnly = False
184          , hcPeerSharing = PeerSharingDisabled
185          }
186        responderConfig = HandshakeConfig
187          { hcNetworkMagic = 764824073
188          , hcSupportedVersions = [V11, V12, V13]
189          , hcInitiatorOnly = False
190          , hcPeerSharing = PeerSharingDisabled
191          }
192  
193    withServer 0 $ \listenSock port -> do
194      serverResult <- newEmptyMVar
195      clientResult <- newEmptyMVar
196  
197      -- Server: responder
198      _ <- forkIO $ do
199        serverT <- acceptConnection listenSock
200        result <- runResponder serverT responderConfig
201        putMVar serverResult result
202        transportClose serverT
203  
204      -- Client: initiator
205      clientT <- connectToServer "127.0.0.1" port
206      result <- runInitiator clientT initiatorConfig
207      putMVar clientResult result
208      transportClose clientT
209  
210      sRes <- takeMVar serverResult
211      cRes <- takeMVar clientResult
212  
213      -- Initiator should get refused, responder reports no common version
214      case (cRes, sRes) of
215        (Left (HandshakeRefused (RefuseVersionMismatch _)), Left HandshakeNoCommonVersion) ->
216          pure ()  -- Expected
217        (cR, sR) ->
218          expectationFailure $ "Expected version mismatch, got client: " ++
219            show cR ++ ", server: " ++ show sR
220  
221  -- | Run the initiator side of the handshake over a transport.
222  runInitiator :: TcpTransport -> HandshakeConfig -> IO (Either HandshakeError HandshakeResult)
223  runInitiator t config = do
224    -- Send ProposeVersions
225    let versions = buildVersionTable config
226        msg = MsgProposeVersions versions
227    transportWrite t (encodeHandshake msg)
228  
229    -- Receive response
230    mResponse <- transportRead t 4096
231    case mResponse of
232      Nothing -> pure $ Left $ HandshakeUnexpectedMessage "Connection closed"
233      Just bs -> do
234        case decodeHandshake bs of
235          Left err -> pure $ Left $ HandshakeUnexpectedMessage err
236          Right (MsgAcceptVersion ver vdata) ->
237            pure $ Right $ HandshakeResult ver vdata
238          Right (MsgRefuse reason) ->
239            pure $ Left $ HandshakeRefused reason
240          Right other ->
241            pure $ Left $ HandshakeUnexpectedMessage $ "Unexpected: " ++ show other
242  
243  -- | Run the responder side of the handshake over a transport.
244  runResponder :: TcpTransport -> HandshakeConfig -> IO (Either HandshakeError HandshakeResult)
245  runResponder t config = do
246    -- Receive ProposeVersions
247    mPropose <- transportRead t 4096
248    case mPropose of
249      Nothing -> pure $ Left $ HandshakeUnexpectedMessage "Connection closed"
250      Just bs -> do
251        case decodeHandshake bs of
252          Left err -> pure $ Left $ HandshakeUnexpectedMessage err
253          Right (MsgProposeVersions theirVersions) -> do
254            -- Find compatible version
255            case findCompatibleVersion config theirVersions of
256              Nothing -> do
257                -- Send refuse
258                let refuse = MsgRefuse $ RefuseVersionMismatch (hcSupportedVersions config)
259                transportWrite t (encodeHandshake refuse)
260                pure $ Left HandshakeNoCommonVersion
261              Just (ver, _theirData) -> do
262                -- Accept with our version data
263                let ourData = buildVersionData config ver
264                    accept = MsgAcceptVersion ver ourData
265                transportWrite t (encodeHandshake accept)
266                pure $ Right $ HandshakeResult ver ourData
267          Right other ->
268            pure $ Left $ HandshakeUnexpectedMessage $ "Expected ProposeVersions, got: " ++ show other