packages feed

quic 0.3.13 → 0.3.14

raw patch · 13 files changed

+406/−38 lines, 13 files

Files

ChangeLog.md view
@@ -1,5 +1,52 @@ # ChangeLog +## 0.3.14++A buffer overrun on many streams at once, 0-RTT sent against no limit at+all, and two frames a receiver was taking on trust.++* Count the frame headers when packing many streams into one packet.+  `sendStreamSmall` fills a packet from the send queue up to 1040 octets+  and was counting only the stream data, but every stream whose turn+  comes needs a STREAM frame of its own and nineteen octets of header+  with it.  Hundreds of streams writing a byte or two each therefore+  built a packet far larger than the buffer it is encoded into, and the+  sender died of `BufferOverrun`.  Seen in Cloud Haskell, where one+  connection carries hundreds of processes.+  [#153](https://github.com/kazu-yamamoto/quic/pull/153)++* Hold a client sending 0-RTT to the limits the previous connection gave+  it.  RFC 9000 Sec 7.4.1 holds it to those until the server's own+  arrive.  `sendStreamMany` had a second road for 0-RTT that put the data+  straight on the queue and told the flow control window about it+  afterwards, so a resuming client could spend a connection window it had+  not been given -- and a server that counts answers that with+  FLOW_CONTROL_ERROR before the handshake has finished.  Both roads go+  through the check now, and the connection's own send limit is seeded+  from the remembered parameters so that 0-RTT still carries data.  It is+  seeded beside the two stream counts that were already seeded there and+  were being taken from `defaultParameters` -- 64 streams and ten+  unidirectional, where a server may have allowed fewer, and since these+  limits only ever rise one set too high stayed too high for the rest of+  the connection.+  [#156](https://github.com/kazu-yamamoto/quic/pull/156)++* Refuse a NEW_CONNECTION_ID that contradicts an earlier one.  RFC 9000+  Sec 19.15 leaves to the receiver a connection ID repeated with a+  different stateless reset token or a different sequence number, and a+  sequence number used for a different connection ID.  Ours looked the+  connection ID up, found it, and took the frame for a retransmission+  without comparing what had come with it.  A retransmission says exactly+  what it said before, so it still passes.+  [#157](https://github.com/kazu-yamamoto/quic/pull/157)++* Refuse a RETIRE_CONNECTION_ID for the connection ID the packet carrying+  it arrived on, which RFC 9000 Sec 19.16 also leaves to the receiver.+  An endpoint has to stop using a connection ID before retiring it, so+  the packet that retires one is addressed to another and nothing correct+  is caught by this.+  [#158](https://github.com/kazu-yamamoto/quic/pull/158)+ ## 0.3.13  One line of debug output that could end the connection it described, and
Network/QUIC/Client/Run.hs view
@@ -72,9 +72,20 @@         handshaker <- handshakeClient conf' conn myAuthCIDs         let client = do                 -- For 0-RTT, the following variables should be initialized-                -- in advance.-                setTxMaxStreams conn $ initialMaxStreamsBidi defaultParameters-                setTxUniMaxStreams conn $ initialMaxStreamsUni defaultParameters+                -- in advance -- from the parameters the previous connection+                -- gave, which is what RFC 9000 Sec 7.4.1 holds a client+                -- sending 0-RTT to, and not from 'defaultParameters'.  Those+                -- are 64 bidi streams and 10 uni where a server may have+                -- allowed fewer, and since each of these setters only ever+                -- raises, a limit too high here stayed too high for the rest+                -- of the connection.  The connection's own send limit+                -- belongs with them and was missing altogether: it starts at+                -- zero and nothing raised it before the handshake, so 0-RTT+                -- stream data went out against no connection limit at all.+                params <- getPeerParameters conn+                setTxMaxData conn $ initialMaxData params+                setTxMaxStreams conn $ initialMaxStreamsBidi params+                setTxUniMaxStreams conn $ initialMaxStreamsUni params                 if ccUse0RTT conf                     then wait0RTTReady conn                     else wait1RTTReady conn
Network/QUIC/Connection/Migration.hs view
@@ -17,6 +17,7 @@     retireMyCID,     isMyCIDSeqNumIssued,     addPeerCID,+    isPeerCIDConsistent,     waitPeerCID,     choosePeerCIDForPrivacy,     setPeerStatelessResetToken,@@ -88,6 +89,30 @@     atomicModifyIORef' myCIDDB $ new cid srt  ----------------------------------------------------------------++-- | Does this NEW_CONNECTION_ID agree with what the peer has already said?+--+-- RFC 9000 Sec 19.15: a connection ID repeated with a different stateless+-- reset token or a different sequence number, or a sequence number used for+-- a different connection ID, MAY be treated as a connection error of type+-- PROTOCOL_VIOLATION.+--+-- A retransmission says exactly what it said before and is not caught here,+-- which is the point of comparing the whole 'CIDInfo' rather than noting+-- that we have seen the connection ID.  One that has since been retired is+-- no longer in the table and is not caught either; that is the lenient side+-- of a MAY, and the alternative is remembering every sequence number the+-- peer has ever used.+isPeerCIDConsistent :: Connection -> CIDInfo -> IO Bool+isPeerCIDConsistent Connection{..} cidInfo = agree <$> readTVarIO peerCIDDB+  where+    agree CIDDB{..} = ok bySeqNum && ok byCID+      where+        ok = maybe True (== cidInfo)+        bySeqNum = IntMap.lookup (cidInfoSeq cidInfo) cidInfos+        byCID =+            flip IntMap.lookup cidInfos+                =<< Map.lookup (cidInfoCID cidInfo) revInfos  -- | Receiving NewConnectionID addPeerCID :: Connection -> CIDInfo -> IO Bool
Network/QUIC/Connection/Types.hs view
@@ -18,7 +18,6 @@ import qualified Data.Map.Strict as Map import Data.X509 (CertificateChain) import Foreign.Marshal.Alloc-import Foreign.Ptr (nullPtr) import Network.Control (     Rate,     RxFlow,
Network/QUIC/IO.hs view
@@ -51,19 +51,18 @@ sendStreamMany s dats0 = do     sclosed <- isTxStreamClosed s     when sclosed $ E.throwIO StreamIsClosed-    -- fixme: size check for 0RTT-    let len = totalLen dats0-    ready <- isConnection1RTTReady conn-    if not ready-        then do-            -- 0-RTT-            putSendStreamQ conn $ TxStreamData s dats0 len False-            addTx conn s len-        else flowControl dats0 len False+    flowControl dats0 (totalLen dats0) False   where     conn = streamConnection s+    -- 0-RTT goes through here too.  It used to take the other road: the+    -- data went straight onto the queue and the window was told about it+    -- afterwards, so a resuming client spent a connection window it had not+    -- been given, and a server that counts answers that with+    -- FLOW_CONTROL_ERROR.  RFC 9000 Sec 7.4.1 holds a client sending 0-RTT+    -- to the limits the previous connection gave, and those are in the+    -- peer's parameters by the time anything is sent, so one check serves+    -- both.     flowControl dats len wait = do-        -- 1-RTT         -- FLOW CONTROL: MAX_STREAM_DATA: send: respecting peer's limit         -- FLOW CONTROL: MAX_DATA: send: respecting peer's limit         eblocked <- checkBlocked s len wait@@ -78,8 +77,14 @@                     addTx conn s n                     flowControl dats2 (len - n) False             Left blocked -> do-                -- fixme: RTT0Level?-                sendBlocked conn RTT1Level blocked+                -- Read each time round rather than once: the handshake may+                -- finish while we are waiting for the window, and a BLOCKED+                -- frame at a level we have no keys for goes nowhere.+                ready <- isConnection1RTTReady conn+                let lvl+                        | ready = RTT1Level+                        | otherwise = RTT0Level+                sendBlocked conn lvl blocked                 flowControl dats len True  sendBlocked :: Connection -> EncryptionLevel -> Blocked -> IO ()
Network/QUIC/Receiver.hs view
@@ -192,6 +192,7 @@             when (nkp /= ckp && plainPacketNumber > cpn) $ do                 setCurrentKeyPhase conn nkp plainPacketNumber                 updateCoder1RTT conn ckp -- ckp is now next+            checkRetireOfArrivalCID conn hdr plainFrames             mapM_ (processFrame conn lvl) plainFrames             when ackEli $ do                 case lvl of@@ -288,6 +289,38 @@ deliverStream conn found strm =     when (isNothing found) $ putInput conn $ InpStream strm +-- | Refusing a RETIRE_CONNECTION_ID that retires the connection ID the+--   packet carrying it was addressed to.+--+-- RFC 9000 Sec 19.16: "The sequence number specified in a+-- RETIRE_CONNECTION_ID frame MUST NOT refer to the Destination Connection+-- ID field of the packet in which the frame is contained.  The peer MAY+-- treat this as a connection error of type PROTOCOL_VIOLATION."+--+-- Here rather than in 'processFrame', which is handed the level and one+-- frame and not the header the sequence number has to be read against.+-- Giving all twenty-eight of its equations an argument for the sake of this+-- one is a worse trade than reading the frames twice.+--+-- A peer with nothing wrong with it cannot trip this: it has to stop using+-- a connection ID before retiring it, so the packet that retires one is+-- addressed to another.  A retransmission cannot either -- it goes to a+-- connection ID we still have, and the retired one we no longer answer on+-- at all.+checkRetireOfArrivalCID :: Connection -> Header -> [Frame] -> IO ()+checkRetireOfArrivalCID conn hdr frames = do+    mseq <- myCIDsInclude conn $ headerMyCID hdr+    case mseq of+        Nothing -> return ()+        Just sn ->+            when (sn `elem` retired) $+                closeConnection+                    conn+                    ProtocolViolation+                    "RETIRE_CONNECTION_ID for the CID it arrived on"+  where+    retired = [n | RetireConnectionID n <- frames]+ processFrame :: Connection -> EncryptionLevel -> Frame -> IO () processFrame _ _ Padding{} = return () processFrame conn lvl Ping = do@@ -348,8 +381,8 @@             -- by the tail of every stream it resets, until it has none left.             unarrived <- takeRxUncounted strm             when (unarrived > 0) $ do-                ok <- checkRxMaxData conn unarrived-                unless ok $+                ok' <- checkRxMaxData conn unarrived+                unless ok' $                     closeConnection conn FlowControlError "Flow control error for connection"                 mx <- updateFlowRx conn unarrived                 forM_ mx $ \newMax -> do@@ -565,6 +598,19 @@             seqNum = cidInfoSeq cidInfo         when (cidlen < 1 || 20 < cidlen || retirePriorTo > seqNum) $             closeConnection conn FrameEncodingError "NEW_CONNECTION_ID parameter error"+        -- RFC 9000 Sec 19.15 says:+        -- If an endpoint receives a NEW_CONNECTION_ID frame that repeats a+        -- previously issued connection ID with a different Stateless Reset+        -- Token field value or a different Sequence Number field value, or+        -- if a sequence number is used for different connection IDs, the+        -- endpoint MAY treat that receipt as a connection error of type+        -- PROTOCOL_VIOLATION.+        --+        -- Before the retirement below: a frame that contradicts what the+        -- peer has already said is not one to act on.+        consistent <- isPeerCIDConsistent conn cidInfo+        unless consistent $+            closeConnection conn ProtocolViolation "NEW_CONNECTION_ID repeated differently"         -- Retiring CIDs first then add a new CID.         --         -- RFC 9000 Sec 5.1.1 says:@@ -611,11 +657,9 @@     issued <- isMyCIDSeqNumIssued conn sn     unless issued $         closeConnection conn ProtocolViolation "RETIRE_CONNECTION_ID never issued"-    -- FIXME: CID is necessary here-    -- The sequence number specified in a RETIRE_CONNECTION_ID frame-    -- MUST NOT refer to the Destination Connection ID field of the-    -- packet in which the frame is contained. The peer MAY treat this-    -- as a connection error of type PROTOCOL_VIOLATION.+    -- The sequence number it must not refer to is the one the packet was+    -- addressed to, and the header is not here.  See+    -- 'checkRetireOfArrivalCID'.     mcidInfo <- retireMyCID conn sn     case mcidInfo of         Nothing -> return ()
Network/QUIC/Sender.hs view
@@ -485,6 +485,16 @@ limitation :: Int limitation = 1040 +-- | Upper bound on what a stream frame takes on top of its data+streamFrameMaxOverhead :: Int+streamFrameMaxOverhead+    = sum+    [ 1 -- type+    , 8 -- stream ID+    , 8 -- offset+    , 2 -- length+    ]+ packFin :: Connection -> Stream -> Bool -> IO Bool packFin _ _ True = return True packFin conn s False = do@@ -509,7 +519,7 @@     let sid0 = streamId s0         frame0 = StreamF sid0 off0 dats0 fin0         sb = if fin0 then (s0 :) else id-    (frames, streams) <- loop s0 frame0 len0 id sb+    (frames, streams) <- loop s0 frame0 (len0 + streamFrameMaxOverhead) id sb     ready <- isConnection1RTTReady conn     let lvl             | ready = RTT1Level@@ -536,7 +546,12 @@         case mx of             Nothing -> return (build [frame], sb [])             Just (TxStreamData s1 dats1 len1 fin1) -> do-                let total1 = len1 + total+                -- Entries of the same stream are merged into one frame,+                -- any other stream needs a frame of its own.+                let cost+                        | streamId s1 == streamId s = len1+                        | otherwise = len1 + streamFrameMaxOverhead+                    total1 = cost + total                 if total1 < limitation                     then do                         _ <- takeSendStreamQ conn -- cf tryPeek
Network/QUIC/Server/Reader.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -34,11 +33,6 @@ import qualified Network.Socket.ByteString as NSB import qualified System.IO.Error as E import System.Log.FastLogger-#if MIN_VERSION_random(1,3,0)-import System.Random (getStdRandom, randomRIO, uniformByteString)-#else-import System.Random (getStdRandom, randomRIO, genByteString)-#endif  import Network.QUIC.Common import Network.QUIC.Config
quic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               quic-version:            0.3.13+version:            0.3.14 license:            BSD3 license-file:       LICENSE maintainer:         kazu@iij.ad.jp
test/ErrorSpec.hs view
@@ -5,9 +5,13 @@ import Control.Concurrent import Control.Monad (forever, void) import Data.ByteString ()+import qualified System.Timeout as Timeout+import Test.Hspec+ import Network.QUIC+import qualified Network.QUIC.Client as C+import Network.QUIC.Internal import Network.QUIC.Server-import Test.Hspec  import Config import TransportError@@ -38,5 +42,72 @@ teardown = killThread  spec :: Spec-spec =-    beforeAll setup $ afterAll teardown $ transportErrorSpec testClientConfig 2000 -- 2 seconds+spec = beforeAll setup $ afterAll teardown $ do+    transportErrorSpec testClientConfig 2000 -- 2 seconds+    -- RFC 9000 Sec 19.15 leaves these to the receiver -- "MAY treat that+    -- receipt as a connection error" -- so they live here and not in+    -- TransportError, which is the MUSTs and is run against other+    -- implementations.+    describe "NEW_CONNECTION_ID" $ do+        it "refuses one connection ID offered under two sequence numbers" $ \_ ->+            runQuietly (hooked oneCIDTwoSeqNums) `shouldThrow` protocolViolation+        it "refuses one sequence number offered for two connection IDs" $ \_ ->+            runQuietly (hooked oneSeqNumTwoCIDs) `shouldThrow` protocolViolation+    -- RFC 9000 Sec 19.16, also a MAY.+    describe "RETIRE_CONNECTION_ID" $+        it "refuses one for the connection ID it arrived on" $ \_ ->+            runQuietly (hooked retireArrivalCID)+                `shouldThrow` violationSaying "RETIRE_CONNECTION_ID for the CID it arrived on"++-- | A client that connects, says nothing, and waits to be closed.+runQuietly :: ClientConfig -> IO (Maybe ())+runQuietly cc = Timeout.timeout 2000000 $ C.run cc $ \conn -> do+    waitEstablished conn+    threadDelay 2000000++hooked :: (EncryptionLevel -> Plain -> Plain) -> ClientConfig+hooked f = cc{ccHooks = (ccHooks cc){onPlainCreated = f}}+  where+    cc = testClientConfig++protocolViolation :: QUICException -> Bool+protocolViolation (TransportErrorIsReceived te _) = te == ProtocolViolation+protocolViolation _ = False++-- | The reason phrase matters here: a sequence number that was never issued+--   is a PROTOCOL_VIOLATION too, and that check would answer for this one.+violationSaying :: ReasonPhrase -> QUICException -> Bool+violationSaying want (TransportErrorIsReceived te got) =+    te == ProtocolViolation && got == want+violationSaying _ _ = False++srt1, srt2 :: StatelessResetToken+srt1 = StatelessResetToken "0123456789abcdef"+srt2 = StatelessResetToken "fedcba9876543210"++cid1, cid2 :: CID+cid1 = makeCID "\x01\x01\x01\x01\x01\x01\x01\x01"+cid2 = makeCID "\x02\x02\x02\x02\x02\x02\x02\x02"++-- | Two frames in one packet, the second contradicting the first.  Both go+--   out together so that the peer has no chance to retire the first.+contradict :: CIDInfo -> CIDInfo -> EncryptionLevel -> Plain -> Plain+contradict a b lvl plain+    | lvl == RTT1Level = plain{plainFrames = frames ++ plainFrames plain}+    | otherwise = plain+  where+    frames = [NewConnectionID a 0, NewConnectionID b 0]++-- | Retiring sequence number 0, which is the connection ID the server first+--   gave and the one a client is still using.+retireArrivalCID :: EncryptionLevel -> Plain -> Plain+retireArrivalCID lvl plain+    | lvl == RTT1Level =+        plain{plainFrames = RetireConnectionID 0 : plainFrames plain}+    | otherwise = plain++oneCIDTwoSeqNums :: EncryptionLevel -> Plain -> Plain+oneCIDTwoSeqNums = contradict (newCIDInfo 10 cid1 srt1) (newCIDInfo 11 cid1 srt1)++oneSeqNumTwoCIDs :: EncryptionLevel -> Plain -> Plain+oneSeqNumTwoCIDs = contradict (newCIDInfo 10 cid1 srt1) (newCIDInfo 10 cid2 srt2)
test/HandshakeSpec.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}  module HandshakeSpec where @@ -46,7 +47,9 @@         it "can request and accept a client certificate" $ do             let TLS.Credentials credentials = scCredentials sc0             credential <- case credentials of-                [] -> expectationFailure "test server has no credentials" >> fail "missing credentials"+                [] ->+                    expectationFailure "test server has no credentials"+                        >> fail "missing credentials"                 cred : _ -> pure cred             let clientHooks =                     (ccTlsHooks testClientConfig)@@ -80,6 +83,21 @@             let cc = testClientConfig                 sc = sc0{scUse0RTT = True}             testHandshake2 cc sc waitS (FullHandshake, RTT0) True+        it "keeps 0-RTT within the limits the previous connection gave" $ do+            let cc = testClientConfig+                sc =+                    sc0+                        { scUse0RTT = True+                        , scParameters =+                            (scParameters sc0)+                                { initialMaxData = limit+                                , initialMaxStreamDataBidiRemote = limit+                                }+                        }+            test0RTTFlowControl cc sc waitS+        it "sends 0-RTT data without waiting for the handshake" $ do+            let sc = sc0{scUse0RTT = True}+            test0RTTSendsEarly sc waitS         it "fails with unknown server certificate" $ do             let cc1 =                     testClientConfig@@ -146,6 +164,101 @@     sendStream s content     shutdownStream s     void $ recvStream s 1024++-- | The limit the server gives, which the second connection sends more than.+limit :: Int+limit = 1024++-- | A client sending 0-RTT is held to the limits the previous connection+--   gave it (RFC 9000 Sec 7.4.1).+--+-- 0-RTT stream data used to bypass the flow control check altogether: it+-- went onto the send queue and the window was told about it afterwards, so+-- a resuming client spent a connection window it had not been given.  A+-- server that counts -- ours does -- answers that with FLOW_CONTROL_ERROR+-- before the handshake has even finished.+--+-- The data has to be written before the handshake completes for any of this+-- to be exercised, so this does not go through 'query', which waits for the+-- connection to be established first.+test0RTTFlowControl :: ClientConfig -> ServerConfig -> IO () -> IO ()+test0RTTFlowControl cc1 sc waitS = do+    mvar <- newEmptyMVar+    E.bracket (forkIO $ server mvar) killThread $ \_ -> client mvar+  where+    content = BS.replicate (limit * 4) 97+    client mvar = do+        waitS+        res <- C.run cc1 $ \conn -> do+            query "first" conn+            threadDelay 50000+            getResumptionInfo conn+        threadDelay 50000+        let cc2 = cc1{ccResumption = res, ccUse0RTT = True}+        C.run cc2 $ \conn -> do+            s <- stream conn+            sendStream s content+            shutdownStream s+            void $ recvStream s 1024+        takeMVar mvar+    server mvar = S.run sc serv+      where+        serv conn = do+            s <- acceptStream conn+            bs <- recvAll s id+            sendStream s "bye"+            closeStream s+            when (bs == content) $ putMVar mvar ()+    recvAll s build = do+        bs <- recvStream s 1024+        if BS.null bs+            then return $ BS.concat $ build []+            else recvAll s (build . (bs :))++-- | The remembered limits are what let 0-RTT carry anything at all.+--+-- The connection's own send limit starts at zero and the handshake is what+-- raises it, so a client that checks the connection window before the+-- handshake -- which it has to, see above -- has nothing to spend until the+-- handshake is over, and 0-RTT carries no data.  What it may spend is what+-- the previous connection gave it.+--+-- Every packet from the server is dropped here, so the handshake cannot+-- finish and a send that waits for it waits for good.+test0RTTSendsEarly :: ServerConfig -> IO () -> IO ()+test0RTTSendsEarly sc waitS =+    E.bracket (forkIO server) killThread $ \_ -> do+        waitS+        resVar <- newEmptyMVar+        withPipe (DropServerPacket []) $ do+            res <- C.run testClientConfigR $ \conn -> do+                query "first" conn+                threadDelay 50000+                getResumptionInfo conn+            putMVar resVar res+        res <- takeMVar resVar+        threadDelay 50000+        let cc =+                testClientConfigR+                    { ccResumption = res+                    , ccUse0RTT = True+                    }+        sent <- newEmptyMVar+        withPipe (DropServerPacket [0 .. 50]) $ do+            void $ forkIO $ void $ ignoreQUIC $ C.run cc $ \conn -> do+                s <- stream conn+                sendStream s $ BS.replicate limit 97+                putMVar sent ()+                threadDelay 5000000+            Timeout.timeout 2000000 (takeMVar sent) `shouldReturn` Just ()+  where+    server = S.run sc $ \conn -> do+        s <- acceptStream conn+        void $ recvStream s 1024+        sendStream s "bye"+        closeStream s+    ignoreQUIC :: IO () -> IO ()+    ignoreQUIC act = act `E.catch` \(_ :: E.SomeException) -> return ()  testHandshake2     :: ClientConfig
test/IOSpec.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -156,9 +157,43 @@     describe "concurrency" $ do         it "can handle multiple clients" $ do             withPipe (Randomly 20) $ testMultiSendRecv cc sc waitS 500+    describe "packing" $ do+        it "fits the frames of many streams with tiny writes into a packet" $ do+            withPipe (DropClientPacket []) $ testTinyWrites cc sc waitS     describe "abortConnection" $ do         it "can abort connection" $ do             withPipe (Randomly 20) $ testAbort cc sc waitS++-- | Writing data to many, many streams doesn't cause a buffer overrun+testTinyWrites :: C.ClientConfig -> ServerConfig -> IO () -> IO ()+testTinyWrites cc sc0 waitS = do+    doneVar <- newChan+    withAsync (server doneVar) $ \_ -> client doneVar+  where+    nStreams = 400+    sc =+        sc0+            { scParameters =+                (scParameters sc0){initialMaxStreamsBidi = 2 * nStreams}+            }++    client doneVar = do+        waitS+        C.run cc $ \conn -> do+            strms <- replicateM nStreams (stream conn)+            forM_ strms $ \strm -> sendStream strm "a"+            threadDelay 200_000+            forM_ strms $ \strm -> sendStream strm "b"+            -- Wait for the server to have read both bytes of every stream+            -- before closing the connection.+            mres <- Timeout.timeout 10_000_000 $ replicateM_ nStreams $ readChan doneVar+            mres `shouldBe` Just ()++    server doneVar = run sc $ \conn -> forever $ do+        strm <- acceptStream conn+        void $ forkIO $ do+            consumeBytes strm 2+            writeChan doneVar ()  consumeBytes :: Stream -> Int -> IO () consumeBytes _ 0 = return ()
test/SetupSpec.hs view
@@ -15,7 +15,7 @@ -- hung every CI job on the former. module SetupSpec where -import Control.Concurrent (threadDelay)+import Control.Concurrent import Control.Concurrent.Async import qualified Control.Exception as E import System.Directory@@ -42,10 +42,18 @@                 let qdir = dir </> "qlog"                 createDirectory qdir                 sc0 <- makeTestServerConfig+                -- Waiting for the port to be bound.  Without this the+                -- client can start first, hear nothing, and give up on its+                -- idle timeout having never made the server open a qlog at+                -- all: the directory is then empty and the test fails+                -- saying so rather than saying anything about handles.  One+                -- run in five on a busy machine.+                ready <- newEmptyMVar                 let sc =                         sc0                             { scQLog = Just qdir                             , scDebugLog = Just (dir </> "not-a-directory")+                            , scHooks = (scHooks sc0){onServerReady = putMVar ready ()}                             }                     cc =                         testClientConfig@@ -54,7 +62,8 @@                                     { maxIdleTimeout = Milliseconds 1000                                     }                             }-                withAsync (run sc $ \_ -> return ()) $ \_ ->+                withAsync (run sc $ \_ -> return ()) $ \_ -> do+                    takeMVar ready                     C.run cc (\_ -> return ())                         `shouldThrow` (\(_ :: QUICException) -> True)                 -- If the handle is still open, GHC's own lock on the file