packages feed

quic 0.3.22 → 0.3.23

raw patch · 29 files changed

+647/−120 lines, 29 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Network.QUIC.Internal: setPeerCIDAndRetireCIDs :: Connection -> Int -> IO [Int]
- Network.QUIC.Internal: type DatagramQ = TQueue ByteString
+ Network.QUIC: setMaxBidiStreams :: Connection -> Int -> IO ()
+ Network.QUIC: setMaxUniStreams :: Connection -> Int -> IO ()
+ Network.QUIC.Internal: DatagramQ :: TQueue ByteString -> TVar Int -> DatagramQ
+ Network.QUIC.Internal: RecvBudget :: Int -> TVar Int -> RecvBudget
+ Network.QUIC.Internal: [scMaxConnections] :: ServerConfig -> Int
+ Network.QUIC.Internal: [targetStreams] :: Concurrency -> Int
+ Network.QUIC.Internal: addPeerCIDAndRetire :: Connection -> CIDInfo -> Int -> IO (Maybe [Int])
+ Network.QUIC.Internal: closeRecvQ :: RecvQ -> IO ()
+ Network.QUIC.Internal: congestionRecoveryStartTime :: CC -> Maybe TimeMicrosecond
+ Network.QUIC.Internal: data DatagramQ
+ Network.QUIC.Internal: data RecvBudget
+ Network.QUIC.Internal: datagramQLimit :: Int
+ Network.QUIC.Internal: encodeInitialRefusal :: Version -> CID -> CID -> IO ByteString
+ Network.QUIC.Internal: flushDatagramQSTM :: DatagramQ -> STM [ByteString]
+ Network.QUIC.Internal: genNiteCoder :: Bool -> Version -> Cipher -> TrafficSecrets a -> IO (Coder, Protector)
+ Network.QUIC.Internal: newDatagramQ :: IO DatagramQ
+ Network.QUIC.Internal: newRecvBudget :: Int -> IO RecvBudget
+ Network.QUIC.Internal: onPacketsLost :: LDCC -> Seq SentPacket -> IO ()
+ Network.QUIC.Internal: processFrame :: Connection -> EncryptionLevel -> Frame -> IO ()
+ Network.QUIC.Internal: readDatagramQSTM :: DatagramQ -> STM ByteString
+ Network.QUIC.Internal: recvBudgetLimit :: Int
+ Network.QUIC.Internal: retransmit :: LDCC -> Seq SentPacket -> IO ()
+ Network.QUIC.Internal: setMaxBidiStreams :: Connection -> Int -> IO ()
+ Network.QUIC.Internal: setMaxUniStreams :: Connection -> Int -> IO ()
+ Network.QUIC.Internal: writeDatagramQ :: DatagramQ -> ByteString -> IO ()
+ Network.QUIC.Server: scMaxConnections :: ServerConfig -> Int
- Network.QUIC.Internal: Concurrency :: StreamId -> StreamIdBase -> Int -> Concurrency
+ Network.QUIC.Internal: Concurrency :: StreamId -> StreamIdBase -> Int -> Int -> Concurrency
- Network.QUIC.Internal: RecvQ :: TQueue ReceivedPacket -> TVar Int -> RecvQ
+ Network.QUIC.Internal: RecvQ :: TQueue ReceivedPacket -> TVar Int -> TVar Int -> TVar Bool -> RecvBudget -> RecvQ
- Network.QUIC.Internal: ServerConfig :: [Version] -> [Cipher] -> [Group] -> [[Group]] -> Parameters -> (String -> IO ()) -> Maybe FilePath -> Credentials -> Hooks -> ServerHooks -> Bool -> Int -> [(IP, PortNumber)] -> Maybe (Version -> [ByteString] -> IO ByteString) -> Bool -> Bool -> SessionManager -> Maybe FilePath -> Int -> (ApplicationProtocolError, ReasonPhrase) -> (IO () -> IO ()) -> ServerConfig
+ Network.QUIC.Internal: ServerConfig :: [Version] -> [Cipher] -> [Group] -> [[Group]] -> Parameters -> (String -> IO ()) -> Maybe FilePath -> Credentials -> Hooks -> ServerHooks -> Bool -> Int -> [(IP, PortNumber)] -> Maybe (Version -> [ByteString] -> IO ByteString) -> Bool -> Bool -> Int -> SessionManager -> Maybe FilePath -> Int -> (ApplicationProtocolError, ReasonPhrase) -> (IO () -> IO ()) -> ServerConfig
- Network.QUIC.Internal: getMyStreamId :: Connection -> IO Int
+ Network.QUIC.Internal: getMyStreamId :: Connection -> StreamId -> IO Int
- Network.QUIC.Internal: newRecvQ :: IO RecvQ
+ Network.QUIC.Internal: newRecvQ :: RecvBudget -> IO RecvQ

Files

ChangeLog.md view
@@ -1,5 +1,35 @@ # ChangeLog +## 0.3.23++Adds `scMaxConnections` to `ServerConfig` and `setMaxBidiStreams` and+`setMaxUniStreams` to `Network.QUIC`.  In `Network.QUIC.Internal`,+`newRecvQ` takes a `RecvBudget`, `getMyStreamId` takes a `StreamId`,+`queueTx` and `checkBlocked` are in `STM`, the DATAGRAM queue is a+`DatagramQ`, and `setPeerCIDAndRetireCIDs` is replaced by+`addPeerCIDAndRetire`.++* Take the flow control window in the transaction that finds it+  [#224](https://github.com/kazu-yamamoto/quic/pull/224)+* Add a new peer CID along with the retirement it asks for+  [#225](https://github.com/kazu-yamamoto/quic/pull/225)+* Drop a DATAGRAM when too many wait for the application+  [#227](https://github.com/kazu-yamamoto/quic/pull/227)+* Share one byte budget among a server's receive queues+  [#228](https://github.com/kazu-yamamoto/quic/pull/228)+* Refuse a connection over scMaxConnections with CONNECTION_REFUSED+  [#229](https://github.com/kazu-yamamoto/quic/pull/229)+* Let the application set how many streams the peer may open+  [#230](https://github.com/kazu-yamamoto/quic/pull/230)+* Require a Retry of a client whose token validates nothing+  [#231](https://github.com/kazu-yamamoto/quic/pull/231)+* Check a unidirectional stream against the unidirectional streams we opened+  [#232](https://github.com/kazu-yamamoto/quic/pull/232)+* Answer a frame received in 0-RTT in 1-RTT+  [#233](https://github.com/kazu-yamamoto/quic/pull/233)+* Make no congestion event and send no PING for lost ACK-only packets+  [#234](https://github.com/kazu-yamamoto/quic/pull/234)+ ## 0.3.22  Requires crypto-token 0.2.1.  The public API is unchanged.  In
Network/QUIC.hs view
@@ -36,6 +36,10 @@     resetStream,     stopStream, +    -- ** Limits on the peer's streams+    setMaxBidiStreams,+    setMaxUniStreams,+     -- * IO     recvStream,     resetReceived,
Network/QUIC/Client/Run.hs view
@@ -162,7 +162,7 @@     -- thread is the only one the socket has ever had.     flip E.onException (NS.unsafeClose sock) $ do         when ccSockConnected $ NS.connect sock peersa-        q <- newRecvQ+        q <- newRecvQ =<< newRecvBudget maxBound         retire0 <- newTVarIO False         sref <- newIORef (sock, retire0)         pathInfo <- newPathInfo peersa@@ -191,7 +191,7 @@             let myAuthCIDs = defaultAuthCIDs{initSrcCID = Just myCID}                 peerAuthCIDs = defaultAuthCIDs{initSrcCID = Just peerCID, origDstCID = Just peerCID}             genSRT <- makeGenStatelessReset-            connRecvDatagramQ <- newTQueueIO+            connRecvDatagramQ <- newDatagramQ             conn <-                 clientConnection                     conf
Network/QUIC/Config.hs view
@@ -194,6 +194,11 @@     -- ^ Request a certificate from clients.     , scRequireRetry :: Bool     -- ^ Requiring QUIC retry.+    , scMaxConnections :: Int+    -- ^ How many connections may be open at once.  An Initial packet that+    --   would open one more is answered with CONNECTION_REFUSED.  0 or less,+    --   the default being 0, sets no limit; a server open to the Internet+    --   should set one.     , scSessionManager :: SessionManager     -- ^ A session manager of TLS 1.3.     , scDebugLog :: Maybe FilePath@@ -243,6 +248,7 @@         , scALPN = Nothing         , scWantClientCert = False         , scRequireRetry = False+        , scMaxConnections = 0         , scSessionManager = noSessionManager         , scDebugLog = Nothing         , scTicketLifetime = 7200
Network/QUIC/Connection/Crypto.hs view
@@ -19,6 +19,7 @@     initializeCoder,     initializeCoder1RTT,     updateCoder1RTT,+    genNiteCoder,     getCoder,     getProtector,     --
Network/QUIC/Connection/Migration.hs view
@@ -12,7 +12,7 @@     getNewMyCID,     getMyCIDSeqNum,     setMyCID,-    setPeerCIDAndRetireCIDs,+    addPeerCIDAndRetire,     retirePeerCID,     retireMyCID,     isMyCIDSeqNumIssued,@@ -206,13 +206,26 @@ setPeerRetirePriorTo Connection{..} rpt =     atomically $ modifyTVar' peerCIDDB $ \db -> db{retirePriorTo = rpt} --- | Receiving NewConnectionID-setPeerCIDAndRetireCIDs :: Connection -> Int -> IO [Int]-setPeerCIDAndRetireCIDs Connection{..} rpt = atomically $ do-    db <- readTVar peerCIDDB-    let (db', ns) = arrange rpt db-    writeTVar peerCIDDB db'-    return ns+-- | Receiving NewConnectionID: adding the CID and retiring those below+--   Retire Prior To in one go.  Retired first, nothing might be left to use;+--   added first, the limit is checked only after the retirement+--   (RFC 9000 Sec 5.1.1).  'Nothing' if the limit is exceeded.+addPeerCIDAndRetire :: Connection -> CIDInfo -> Int -> IO (Maybe [Int])+addPeerCIDAndRetire conn@Connection{..} cidInfo rpt = do+    let lim = activeConnectionIdLimit $ getMyParameters conn+    atomically $ do+        db0 <- readTVar peerCIDDB+        let db1+                | Map.member (cidInfoCID cidInfo) (revInfos db0) = db0 -- retransmitted+                | otherwise = add cidInfo db0+            (db2, ns)+                | rpt >= retirePriorTo db0 = arrange rpt db1+                | otherwise = (db1, [])+        if Map.size (revInfos db2) > lim+            then return Nothing+            else do+                writeTVar peerCIDDB db2+                return $ Just ns  arrange :: Int -> CIDDB -> (CIDDB, [Int]) arrange rpt db@CIDDB{..} = (db', dropSeqnums)@@ -220,11 +233,10 @@     (toDrops, cidInfos') = IntMap.partitionWithKey (\k _ -> k < rpt) cidInfos     dropSeqnums = IntMap.foldrWithKey (\k _ ks -> k : ks) [] toDrops     dropCIDs = IntMap.foldr (\c r -> cidInfoCID c : r) [] toDrops-    -- IntMap.findMin is a partial function.-    -- But receiver guarantees that there is at least one cidinfo.+    -- The caller adds a CID at or above rpt first, so one is left.     usedCIDInfo'         | cidInfoSeq usedCIDInfo >= rpt = usedCIDInfo-        | otherwise = snd $ IntMap.findMin cidInfos'+        | otherwise = maybe usedCIDInfo snd $ IntMap.lookupMin cidInfos'     revInfos' = foldr Map.delete revInfos dropCIDs     db' =         db
Network/QUIC/Connection/Stream.hs view
@@ -14,6 +14,8 @@     updatePeerStreamId,     checkStreamIdRoom,     releasePeerStream,+    setMaxBidiStreams,+    setMaxUniStreams, ) where  import Control.Concurrent.STM@@ -24,12 +26,15 @@ import Network.QUIC.Connection.Types import Network.QUIC.Connector import Network.QUIC.Imports-import Network.QUIC.Parameters import Network.QUIC.Types -getMyStreamId :: Connection -> IO Int-getMyStreamId Connection{..} = do-    next <- currentStream <$> readTVarIO myStreamId+-- | The last stream we opened of the kind 'StreamId' is.+getMyStreamId :: Connection -> StreamId -> IO Int+getMyStreamId Connection{..} sid = do+    let tvar+            | isUnidirectional sid = myUniStreamId+            | otherwise = myStreamId+    next <- currentStream <$> readTVarIO tvar     return $ next - 4  possibleMyStreams :: Connection -> IO Int@@ -149,16 +154,12 @@             | otherwise = peerUniStreamId conn     atomicModifyIORef' ref checkConc   where-    params = getMyParameters conn-    initialStreams-        | dir == Bidirectional = initialMaxStreamsBidi params-        | otherwise = initialMaxStreamsUni params     checkConc conc@Concurrency{..} =         let StreamIdBase base = maxStreams             closed = closedStreams + 1-            base' = initialStreams + closed+            base' = targetStreams + closed             conc' = conc{closedStreams = closed}-         in if base' - base >= max 1 (initialStreams `div` 2)+         in if base' - base >= max 1 (targetStreams `div` 2)                 then (conc'{maxStreams = StreamIdBase base'}, Just base')                 else (conc', Nothing) @@ -182,3 +183,34 @@     dir         | isUnidirectional sid = Unidirectional         | otherwise = Bidirectional++-- | Setting how many bidirectional streams the peer may have open at once,+--   'initialMaxStreamsBidi' to begin with.  A larger number is announced+--   at once.  MAX_STREAMS cannot go down (RFC 9000 Sec 4.6), so a smaller+--   one takes effect as the peer's open streams are closed.+setMaxBidiStreams :: Connection -> Int -> IO ()+setMaxBidiStreams conn = setMaxStreams conn Bidirectional++-- | 'setMaxBidiStreams' for unidirectional streams.+setMaxUniStreams :: Connection -> Int -> IO ()+setMaxUniStreams conn = setMaxStreams conn Unidirectional++setMaxStreams :: Connection -> Direction -> Int -> IO ()+setMaxStreams conn dir n0 = do+    mx <- atomicModifyIORef' ref retarget+    case mx of+        Nothing -> return ()+        Just nms -> sendFrames conn RTT1Level [MaxStreams dir nms]+  where+    ref+        | dir == Bidirectional = peerStreamId conn+        | otherwise = peerUniStreamId conn+    -- RFC 9000 Sec 4.6: no more than 2^60 streams.+    n = max 0 $ min n0 (2 ^ (60 :: Int))+    retarget conc@Concurrency{..} =+        let StreamIdBase base = maxStreams+            base' = min (2 ^ (60 :: Int)) (n + closedStreams)+            conc' = conc{targetStreams = n}+         in if base' > base+                then (conc'{maxStreams = StreamIdBase base'}, Just base')+                else (conc', Nothing)
Network/QUIC/Connection/Types.hs view
@@ -195,6 +195,8 @@     , maxStreams :: StreamIdBase     , closedStreams :: Int     -- ^ For the peer's streams: how many of them we are done with+    , targetStreams :: Int+    -- ^ For the peer's streams: how many may be open at once (#86)     }     deriving (Show) @@ -207,6 +209,7 @@         | otherwise = if bidi then 1 else 3     maxStreams = StreamIdBase n     closedStreams = 0+    targetStreams = n  -- | The peer-initiated streams of one type that have been opened so far. --@@ -605,8 +608,6 @@ ----------------------------------------------------------------  type SendStreamQ = TQueue TxStreamData--type DatagramQ = TQueue ByteString  data Shared = Shared     { sharedCloseSent :: IORef Bool
Network/QUIC/IO.hs view
@@ -42,8 +42,8 @@  -- | Counting and queueing at once, so that the offset given is the --   stream's next one and the queue holds a stream's data in order.-queueTx :: Connection -> Stream -> [StreamData] -> Int -> IO ()-queueTx conn s dats len = atomically $ do+queueTx :: Connection -> Stream -> [StreamData] -> Int -> STM ()+queueTx conn s dats len = do     off <- addTxStreamData s len     addTxData conn len     putSendStreamQSTM conn $ TxStreamData s off dats len False@@ -74,16 +74,22 @@     flowControl dats len wait = do         -- FLOW CONTROL: MAX_STREAM_DATA: send: respecting peer's limit         -- FLOW CONTROL: MAX_DATA: send: respecting peer's limit-        eblocked <- checkBlocked s len wait-        case eblocked of-            Right n-                -- Counted before queued, so that a reset in between takes-                -- it into the final size.-                | len == n -> queueTx conn s dats len-                | otherwise -> do-                    let (dats1, dats2) = split n dats+        -- The window is taken in the transaction that finds it.  Two+        -- streams sending at once used to see the same window and send it+        -- twice over.+        eblocked <- atomically $ do+            r <- checkBlocked s len wait+            case r of+                Right n -> do+                    let (dats1, dats2)+                            | len == n = (dats, [])+                            | otherwise = split n dats                     queueTx conn s dats1 n-                    flowControl dats2 (len - n) False+                    return $ Right (n, dats2)+                Left blocked -> return $ Left blocked+        case eblocked of+            Right (n, dats2) ->+                when (n < len) $ flowControl dats2 (len - n) False             Left blocked -> do                 -- Read each time round rather than once: the handshake may                 -- finish while we are waiting for the window, and a BLOCKED@@ -117,8 +123,8 @@       where         len = BS.length bs -checkBlocked :: Stream -> Int -> Bool -> IO (Either Blocked Int)-checkBlocked s len wait = atomically $ do+checkBlocked :: Stream -> Int -> Bool -> STM (Either Blocked Int)+checkBlocked s len wait = do     let conn = streamConnection s     checkPeerClosedSTM conn     strmFlow <- readStreamFlowTx s@@ -343,8 +349,8 @@ -- | Receive all available DATAGRAM frames. --   This function is non-blocking. recvDatagramMany :: Connection -> IO [ByteString]-recvDatagramMany = atomically . flushTQueue . connRecvDatagramQ+recvDatagramMany = atomically . flushDatagramQSTM . connRecvDatagramQ  -- | Receiving a DATAGRAM frame in STM. recvDatagramSTM :: Connection -> STM ByteString-recvDatagramSTM conn = readTQueue (connRecvDatagramQ conn) `orElse` peerClosedSTM conn+recvDatagramSTM conn = readDatagramQSTM (connRecvDatagramQ conn) `orElse` peerClosedSTM conn
Network/QUIC/Internal.hs view
@@ -17,6 +17,7 @@     module Network.QUIC.Client.WatchDog,     module Network.QUIC.Socket,     sendOutput,+    processFrame, ) where  import Network.QUIC.Client.Reader (@@ -35,6 +36,7 @@ import Network.QUIC.Parameters import Network.QUIC.QLogger import Network.QUIC.Qlog+import Network.QUIC.Receiver (processFrame) import Network.QUIC.Recovery import Network.QUIC.Sender (sendOutput) import Network.QUIC.Socket
Network/QUIC/Packet.hs view
@@ -2,6 +2,7 @@     -- * Encode     encodeVersionNegotiationPacket,     encodeRetryPacket,+    encodeInitialRefusal,     encodePlainPacket,     makeStatelessReset, 
Network/QUIC/Packet/Encode.hs view
@@ -5,10 +5,13 @@     encodeVersionNegotiationPacket,     encodeRetryPacket,     encodePlainPacket,+    encodeInitialRefusal,     makeStatelessReset, ) where  import qualified Data.ByteString as BS+import qualified Data.ByteString.Short as Short+import Foreign.Marshal.Alloc (allocaBytes) import System.Random (getStdRandom, randomRIO) #if MIN_VERSION_random(1,3,0) import System.Random (uniformByteString)@@ -54,6 +57,36 @@  ---------------------------------------------------------------- +-- | An Initial packet closing with CONNECTION_REFUSED, made without a+--   connection: the keys are the ones the client's Destination Connection+--   ID gives (RFC 9001 Sec 5.2), and that ID is our Source Connection ID.+encodeInitialRefusal :: Version -> CID -> CID -> IO ByteString+encodeInitialRefusal ver peerCID origCID =+    allocaBytes bufLen $ \buf -> allocaBytes bufLen $ \encBuf -> do+        (coder, protector) <-+            genNiteCoder False ver defaultCipher $ initialSecrets ver origCID+        wbuf <- newWriteBuffer buf bufLen+        (epn, epnLen) <-+            encodeLongHeaderPP' True wbuf InitialPacketType ver peerCID origCID (Flags 0) 0+        encodeInt' wbuf 0 -- token+        (len, _) <-+            protectPayloadHeader'+                encBuf+                defaultCipher+                coder+                protector+                wbuf+                [ConnectionClose ConnectionRefused 0 Short.empty]+                0+                epn+                epnLen+                buf+                Nothing+                InitialLevel+        if len < 0 then return BS.empty else BS.packCStringLen (castPtr buf, len)+  where+    bufLen = 1500+ encodeRetryPacket :: RetryPacket -> IO ByteString encodeRetryPacket (RetryPacket ver dCID sCID token (Left odCID)) = withWriteBuffer maximumQUICHeaderSize $ \wbuf -> do     save wbuf@@ -204,9 +237,22 @@     -> PacketNumber     -> IO (EncodedPacketNumber, Int) encodeLongHeaderPP conn wbuf pkttyp ver dCID sCID flags pn = do+    quicBit <- greaseQuicBit <$> getPeerParameters conn+    encodeLongHeaderPP' quicBit wbuf pkttyp ver dCID sCID flags pn++encodeLongHeaderPP'+    :: Bool+    -> WriteBuffer+    -> LongHeaderPacketType+    -> Version+    -> CID+    -> CID+    -> Flags Raw+    -> PacketNumber+    -> IO (EncodedPacketNumber, Int)+encodeLongHeaderPP' quicBit wbuf pkttyp ver dCID sCID flags pn = do     let el@(_, pnLen) = encodePacketNumber 0 {- dummy -} pn         pp = encodePktNumLength pnLen-    quicBit <- greaseQuicBit <$> getPeerParameters conn     Flags flags' <- encodeLongHeaderFlags ver pkttyp flags pp quicBit     write8 wbuf flags'     encodeLongHeader wbuf ver dCID sCID@@ -227,15 +273,46 @@     -> Bool     -> IO (Int, Int) protectPayloadHeader conn wbuf frames pn epn epnLen headerBeg mlen lvl keyPhase = do+    cipher <- getCipher conn lvl+    coder <- getCoder conn lvl keyPhase+    protector <- getProtector conn lvl+    protectPayloadHeader'+        (encodeBuf conn)+        cipher+        coder+        protector+        wbuf+        frames+        pn+        epn+        epnLen+        headerBeg+        mlen+        lvl++-- | 'protectPayloadHeader' with what it takes from a connection given.+protectPayloadHeader'+    :: Buffer+    -> Cipher+    -> Coder+    -> Protector+    -> WriteBuffer+    -> [Frame]+    -> PacketNumber+    -> EncodedPacketNumber+    -> Int+    -> Buffer+    -> Maybe Int+    -> EncryptionLevel+    -> IO (Int, Int)+protectPayloadHeader' encBuf cipher coder protector wbuf frames pn epn epnLen headerBeg mlen lvl = do     -- Real size is maximumUdpPayloadSize. But smaller is better.-    let encBuf = encodeBuf conn-        encBufLen = 1500 - 20 - 8+    let encBufLen = 1500 - 20 - 8     payloadWithoutPaddingSiz <- encodeFramesWithPadding encBuf encBufLen frames-    cipher <- getCipher conn lvl     -- before length or packer number     lengthOrPNBeg <- currentOffset wbuf     (packetLen, headerLen, plainLen, tagLen, padLen) <--        calcLen cipher lengthOrPNBeg payloadWithoutPaddingSiz+        calcLen lengthOrPNBeg payloadWithoutPaddingSiz     when (lvl /= RTT1Level) $ writeLen (epnLen + plainLen + tagLen)     pnBeg <- currentOffset wbuf     writeEpn epnLen@@ -245,8 +322,6 @@     header <- mkBS headerBeg headerLen     --     let sampleBeg = pnBeg `plusPtr` 4-    coder <- getCoder conn lvl keyPhase-    protector <- getProtector conn lvl     setSample protector sampleBeg     len <- encrypt coder cryptoBeg plaintext (AssDat header) pn     --@@ -259,7 +334,7 @@             ok <- withMask protector $ protectHeader headerBeg pnBeg epnLen             return $ if ok then (packetLen, padLen) else (-1, -1)   where-    calcLen cipher lengthOrPNBeg payloadWithoutPaddingSiz = do+    calcLen lengthOrPNBeg payloadWithoutPaddingSiz = do         let headerLen =                 (lengthOrPNBeg `minusPtr` headerBeg)                     -- length: assuming 2byte length
Network/QUIC/Receiver.hs view
@@ -3,6 +3,7 @@  module Network.QUIC.Receiver (     receiver,+    processFrame, ) where  import qualified Control.Exception as E@@ -11,7 +12,6 @@ import Network.TLS (AlertDescription (..)) import System.Log.FastLogger -import Control.Concurrent.STM import Network.QUIC.Config import Network.QUIC.Connection import Network.QUIC.Connector@@ -259,11 +259,10 @@         ) guardStream _ _ _ = return () --- fixme: what about unidirection stream? streamNotCreatedYet :: Connection -> StreamId -> ReasonPhrase -> IO () streamNotCreatedYet conn sid emsg     | isInitiated conn sid = do-        curSid <- getMyStreamId conn+        curSid <- getMyStreamId conn sid         when (sid > curSid) $             closeConnection conn StreamStateError emsg streamNotCreatedYet _ _ _ = return ()@@ -464,11 +463,18 @@             Reassembled _ -> return ()         deliverStream conn found strm +-- | The level to answer a frame received at: 0-RTT goes from client to+--   server only, so a server answers 0-RTT in 1-RTT (RFC 9000 Sec 17.2.3).+replyLevel :: EncryptionLevel -> EncryptionLevel+replyLevel RTT0Level = RTT1Level+replyLevel lvl = lvl+ processFrame :: Connection -> EncryptionLevel -> Frame -> IO () processFrame _ _ Padding{} = return () processFrame conn lvl Ping = do     -- see ackEli above-    when (lvl /= InitialLevel && lvl /= RTT1Level) $ sendFrames conn lvl []+    when (lvl /= InitialLevel && lvl /= RTT1Level) $+        sendFrames conn (replyLevel lvl) [] processFrame conn lvl (Ack ackInfo@(AckInfo largestAcked _ _) ackDelay) = do     when (lvl == RTT0Level) $ closeConnection conn ProtocolViolation "ACK"     -- RFC 9000 Sec 19.3.1: walking the ranges down from the largest@@ -576,7 +582,7 @@                 -- No more STREAM frames may go out on it.  Without this,                 -- 'sendStream' went on working after the RESET_STREAM.                 finalSize <- resetTxStream strm-                sendFrames conn lvl [ResetStream sid err finalSize]+                sendFrames conn (replyLevel lvl) [ResetStream sid err finalSize] processFrame _ _ (CryptoF _ "") = return () processFrame conn lvl (CryptoF off cdat) = do     when (lvl == RTT0Level) $@@ -704,29 +710,7 @@     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:-    -- An endpoint MAY send connection IDs that temporarily exceed a-    -- peer's limit if the NEW_CONNECTION_ID frame also requires the-    -- retirement of any excess, by including a sufficiently large-    -- value in the Retire Prior To field.     prevRetirePriorTo <- getPeerRetirePriorTo conn-    -- RFC 900 Sec 19.15 says:-    -- Once a sender indicates a Retire Prior To value, smaller values-    -- sent in subsequent NEW_CONNECTION_ID frames have no effect. A-    -- receiver MUST ignore any Retire Prior To fields that do not-    -- increase the largest received Retire Prior To value.-    when (retirePriorTo >= prevRetirePriorTo) $ do-        -- RFC 9000 Sec 5.1.2 says:-        -- Upon receipt of an increased Retire Prior To field, the-        -- peer MUST stop using the corresponding connection IDs and-        -- retire them with RETIRE_CONNECTION_ID frames before adding-        -- the newly provided connection ID to the set of active-        -- connection IDs.-        seqNums <- setPeerCIDAndRetireCIDs conn retirePriorTo -- upadting RPT-        sendFrames conn RTT1Level $ map RetireConnectionID seqNums-    -- Adding a new CID     if seqNum < prevRetirePriorTo         then             -- RFC 9000 Sec 19.15 says:@@ -736,11 +720,27 @@             -- MUST send a corresponding RETIRE_CONNECTION_ID frame             -- that retires the newly received connection ID, unless             -- it has already done so for that sequence number.+            --+            -- Retire Prior To is not above seqNum here, so it does not+            -- increase and is ignored (RFC 9000 Sec 19.15).             sendFrames conn RTT1Level [RetireConnectionID seqNum]         else do-            ng <- not <$> addPeerCID conn cidInfo-            when ng $-                closeConnection conn ConnectionIdLimitError "NEW_CONNECTION_ID limit error"+            -- RFC 9000 Sec 5.1.1 says:+            -- An endpoint MAY send connection IDs that temporarily exceed a+            -- peer's limit if the NEW_CONNECTION_ID frame also requires the+            -- retirement of any excess, by including a sufficiently large+            -- value in the Retire Prior To field.+            --+            -- The new CID is added along with the retirement, so that one+            -- retiring all the others leaves it to use (#223).+            mseqNums <- addPeerCIDAndRetire conn cidInfo retirePriorTo+            case mseqNums of+                Nothing ->+                    closeConnection conn ConnectionIdLimitError "NEW_CONNECTION_ID limit error"+                Just seqNums ->+                    unless (null seqNums) $+                        sendFrames conn RTT1Level $+                            map RetireConnectionID seqNums processFrame conn RTT1Level (RetireConnectionID sn) = do     -- RFC 9000 Sec 19.16: "Receipt of a RETIRE_CONNECTION_ID frame     -- containing a sequence number greater than any previously sent to the@@ -805,7 +805,7 @@         closeConnection conn ProtocolViolation "DATAGRAM not supported"     when (1 + lenSize + BS.length dat > limitBytes) $         closeConnection conn ProtocolViolation "DATAGRAM size violation"-    atomically $ writeTQueue (connRecvDatagramQ conn) dat+    writeDatagramQ (connRecvDatagramQ conn) dat processFrame conn _ _ = closeConnection conn ProtocolViolation "Frame is not allowed"  -- Return value indicates duplication.
Network/QUIC/Recovery.hs view
@@ -31,6 +31,8 @@     -- Release     releaseByRetry,     releaseOldest,+    onPacketsLost,+    retransmit,     -- Timer     beforeAntiAmp,     ldccTimer,@@ -48,6 +50,9 @@     LDCC,     newLDCC,     lostCandidates,+    recoveryCC,+    ptoPing,+    congestionRecoveryStartTime,     SentPackets (..),     qlogSent, ) where@@ -61,4 +66,4 @@ import Network.QUIC.Recovery.Release import Network.QUIC.Recovery.Timer import Network.QUIC.Recovery.Types-import Network.QUIC.Recovery.Utils (sendPing)+import Network.QUIC.Recovery.Utils (retransmit, sendPing)
Network/QUIC/Recovery/Interface.hs view
@@ -46,6 +46,8 @@     unless (null packets) $ do         onPacketsLost ldcc packets         retransmit ldcc packets+        -- Nothing to resend: a PING, to hurry the peer along all the same.+        unless (any spAckEliciting packets) $ sendPing ldcc lvl         setLossDetectionTimer ldcc  resender :: LDCC -> IO ()
Network/QUIC/Recovery/Release.hs view
@@ -63,16 +63,20 @@ ----------------------------------------------------------------  onPacketsLost :: LDCC -> Seq SentPacket -> IO ()-onPacketsLost ldcc@LDCC{..} lostPackets = case Seq.viewr lostPackets of-    EmptyR -> return ()-    _ :> lastPkt -> do-        decreaseCC ldcc lostPackets-        isRecovery <--            inCongestionRecovery (spTimeSent lastPkt) . congestionRecoveryStartTime-                <$> readTVarIO recoveryCC-        onCongestionEvent ldcc lostPackets isRecovery-        mapM_ (qlogPacketLost ldcc . LostPacket) lostPackets+onPacketsLost ldcc@LDCC{..} lostPackets = do+    decreaseCC ldcc lostPackets+    -- RFC 9002 B.8: only packets in flight make a congestion event.  An+    -- ACK-only packet lost used to halve the window.+    case Seq.viewr inflight of+        EmptyR -> return ()+        _ :> lastPkt -> do+            isRecovery <-+                inCongestionRecovery (spTimeSent lastPkt) . congestionRecoveryStartTime+                    <$> readTVarIO recoveryCC+            onCongestionEvent ldcc inflight isRecovery+    mapM_ (qlogPacketLost ldcc . LostPacket) lostPackets   where+    inflight = Seq.filter ((> 0) . inFlightBytes) lostPackets     onCongestionEvent = updateCC  ----------------------------------------------------------------
Network/QUIC/Recovery/Utils.hs view
@@ -25,10 +25,11 @@  ---------------------------------------------------------------- +-- | Resending the ack-eliciting packets lost.  Lost ACK-only packets need+--   nothing: a PING for them used to send a probe outside the congestion+--   window and resend a packet that was not lost. retransmit :: LDCC -> Seq SentPacket -> IO ()-retransmit ldcc lostPackets-    | null packetsToBeResent = getEncryptionLevel ldcc >>= sendPing ldcc-    | otherwise = mapM_ put packetsToBeResent+retransmit ldcc lostPackets = mapM_ put packetsToBeResent   where     packetsToBeResent = Seq.filter spAckEliciting lostPackets     put = putRetrans ldcc . spPlainPacket
Network/QUIC/Server.hs view
@@ -12,6 +12,7 @@     scALPN,     scWantClientCert,     scRequireRetry,+    scMaxConnections,     scUse0RTT,     scMaxDatagramFrameSize,     scCiphers,
Network/QUIC/Server/Reader.hs view
@@ -9,6 +9,7 @@     liveConnections,     spawnConnection,     waitNoConnection,+    releaseConnection,     spawnDispatcher,     waitNoDispatcher,     tokenMgr,@@ -70,6 +71,8 @@     , stopServerNow :: IO ()     -- ^ Stopping this server: what the shutdown handler is handed, and     -- what a connection's 'stop' reaches it by.+    , connectionCount :: TVar Int+    -- ^ Connections forked and not yet ended, for 'scMaxConnections'.     }  newDispatch :: ServerConfig -> IO () -> IO Dispatch@@ -83,6 +86,7 @@         <*> T.newThreadManager         <*> T.newThreadManager         <*> pure stopNow+        <*> newTVarIO 0   where     conf =         CT.defaultConfig@@ -128,6 +132,18 @@ spawnConnection :: Dispatch -> IO () -> IO () spawnConnection Dispatch{..} = T.forkManaged connectionThreads "QUIC connection" +-- | Counting a connection in, unless there are 'scMaxConnections' already.+reserveConnection :: Dispatch -> Int -> IO Bool+reserveConnection Dispatch{..} lim = atomically $ do+    n <- readTVar connectionCount+    if lim <= 0 || n < lim+        then writeTVar connectionCount (n + 1) >> return True+        else return False++-- | Counting a connection out.+releaseConnection :: Dispatch -> IO ()+releaseConnection Dispatch{..} = atomically $ modifyTVar' connectionCount (subtract 1)+ -- | Waiting until no connection is running. waitNoConnection :: Dispatch -> IO () waitNoConnection Dispatch{..} = T.waitUntilAllGone connectionThreads@@ -146,20 +162,24 @@  -- Source CID -> RecvQ -- Initials and RTT0 are queued before Conneciton is created.-newtype RecvQDict = RecvQDict (LRUCacheRef CID RecvQ)+data RecvQDict = RecvQDict RecvBudget (LRUCacheRef CID RecvQ)  recvQDictSize :: Int recvQDictSize = 100 +-- | The queues share one budget, the endpoint's (#170). newRecvQDict :: IO RecvQDict-newRecvQDict = RecvQDict <$> LRUCache.newLRUCacheRef recvQDictSize+newRecvQDict =+    RecvQDict+        <$> newRecvBudget recvBudgetLimit+        <*> LRUCache.newLRUCacheRef recvQDictSize  -- Looking up and insert a new RecvQ if not exist. lookupInsertRecvQDict :: RecvQDict -> CID -> IO (RecvQ, Bool)-lookupInsertRecvQDict (RecvQDict ref) dcid = LRUCache.cached ref dcid newRecvQ+lookupInsertRecvQDict (RecvQDict budget ref) dcid = LRUCache.cached ref dcid $ newRecvQ budget  lookupRecvQDict :: RecvQDict -> CID -> IO (Maybe RecvQ)-lookupRecvQDict (RecvQDict ref) dcid = LRUCache.cached' ref dcid+lookupRecvQDict (RecvQDict _ ref) dcid = LRUCache.cached' ref dcid  ---------------------------------------------------------------- @@ -277,7 +297,7 @@     -> (CryptPacket, EncryptionLevel, Int)     -> IO () dispatch-    Dispatch{..}+    d@Dispatch{..}     ServerConfig{..}     forkConnection     logAction@@ -330,7 +350,9 @@                                 -- Retry -- but the address it arrives from has                                 -- proved nothing.                                 fresh <- isTokenFresh ct-                                pushToAcceptFirst $ fresh && isTokenAddress ct peersa+                                if fresh && isTokenAddress ct peersa+                                    then pushToAcceptFirst True+                                    else unvalidated                         -- A token we cannot decrypt is not a token.  RFC 9000                         -- section 8.1.3: "If the token is invalid, then the                         -- server SHOULD proceed as if the client did not have@@ -339,14 +361,35 @@                         -- rubbish than for sending nothing, and turned off                         -- the anti-amplification limit for an address that                         -- had proved nothing.-                        Nothing -> pushToAcceptFirst False+                        Nothing -> unvalidated                 Just conn -> writeRecvQ (connRecvQ conn) $ mkReceivedPacket cpkt tim siz lvl       where         myVersions = scVersions+        -- An address no token has validated: a Retry if one is required,+        -- as for an Initial with no token at all.+        unvalidated+            | scRequireRetry = sendRetry+            | otherwise = pushToAcceptFirst False         pushToAcceptQ myAuthCIDs peerAuthCIDs addrValid = do             let key = nonZeroLengthCID sCID peersa+            mq <- lookupRecvQDict srcTable key+            case mq of+                -- A retransmission of an Initial already accepted.+                Just q -> writeRecvQ q $ mkReceivedPacket cpkt tim siz lvl+                Nothing -> do+                    ok <- reserveConnection d scMaxConnections+                    if ok+                        then accept myAuthCIDs peerAuthCIDs addrValid key+                        else -- RFC 9000 Sec 5.2.2: "If a server refuses to+                        -- accept a new connection, it SHOULD send an Initial+                        -- packet containing a CONNECTION_CLOSE frame with+                        -- error code CONNECTION_REFUSED."+                            encodeInitialRefusal peerVer sCID dCID >>= send'+        accept myAuthCIDs peerAuthCIDs addrValid key = do             (q, exist) <- lookupInsertRecvQDict srcTable key             writeRecvQ q $ mkReceivedPacket cpkt tim siz lvl+            -- Another dispatcher got there first.+            when exist $ releaseConnection d             unless exist $ do                 let reg = registerConnectionDict dstTable                     unreg cid =
Network/QUIC/Server/Run.hs view
@@ -234,7 +234,7 @@     -> Dispatch     -> Accept     -> IO ()-runServer conf server0 dispatch acc = do+runServer conf server0 dispatch acc = (`E.finally` done) $ do     labelMe "QUIC runServer"     E.bracket open clse $ \(ConnRes conn myAuthCIDs _reader) ->         handleLogUnit (debugLog conn) $ do@@ -316,6 +316,9 @@             setConnectionClosed conn             closure conn ldcc ex   where+    done = do+        closeRecvQ $ accRecvQ acc+        releaseConnection dispatch     open = createServerConnection conf dispatch acc     -- 'killReaders' stops this connection's own threads and sees them out,     -- which has to happen before the buffers they use are freed.  Nothing@@ -367,7 +370,7 @@     (debugLog, dclean) <- dirDebugLogger scDebugLog ocid `E.onException` qclean     flip E.onException (dclean >> qclean) $ do         debugLog $ "Original CID: " <> bhow ocid-        connRecvDatagramQ <- newTQueueIO+        connRecvDatagramQ <- newDatagramQ         conn <-             serverConnection                 conf
Network/QUIC/Types/Queue.hs view
@@ -1,7 +1,8 @@ module Network.QUIC.Types.Queue where  import Control.Concurrent.STM-import Control.Monad (when)+import Control.Monad (void, when)+import Data.ByteString (ByteString)  import Network.QUIC.Types.Packet @@ -23,18 +24,44 @@ recvQLimit :: Int recvQLimit = 4096 -data RecvQ = RecvQ (TQueue ReceivedPacket) (TVar Int)+-- | How many bytes the packets waiting in all of a server's 'RecvQ's may+--   take together.  'recvQLimit' bounds one connection, and a server has as+--   many connections as it is asked for, so this is what bounds the server+--   (#170).  Some ten connections' worth of full queues.+recvBudgetLimit :: Int+recvBudgetLimit = 64 * 1024 * 1024 -newRecvQ :: IO RecvQ-newRecvQ = RecvQ <$> newTQueueIO <*> newTVarIO 0+-- | Bytes in use, shared by the queues of an endpoint, and its limit.+data RecvBudget = RecvBudget Int (TVar Int) +newRecvBudget :: Int -> IO RecvBudget+newRecvBudget lim = RecvBudget lim <$> newTVarIO 0++-- | Packets, how many, their bytes, whether closed, and the budget.+data RecvQ+    = RecvQ (TQueue ReceivedPacket) (TVar Int) (TVar Int) (TVar Bool) RecvBudget++newRecvQ :: RecvBudget -> IO RecvQ+newRecvQ budget =+    RecvQ+        <$> newTQueueIO+        <*> newTVarIO 0+        <*> newTVarIO 0+        <*> newTVarIO False+        <*> pure budget+ readRecvQ :: RecvQ -> IO ReceivedPacket-readRecvQ (RecvQ q qlen) = atomically $ do+readRecvQ (RecvQ q qlen qbytes _ (RecvBudget _ used)) = atomically $ do     x <- readTQueue q+    let n = rpReceivedBytes x     modifyTVar' qlen (subtract 1)+    modifyTVar' qbytes (subtract n)+    modifyTVar' used (subtract n)     return x --- | Queue a packet, or drop it if 'recvQLimit' are already waiting.+-- | Queue a packet, or drop it if 'recvQLimit' are already waiting, if the+--   budget it shares with the other queues has no room for it, or if the+--   queue has been closed. -- -- What is dropped is the packet that has just arrived rather than one -- already queued, so that what the connection has taken on is kept and only@@ -45,18 +72,72 @@ -- is one sending faster than the receiver can refuse what it sends, and a -- line of log for each of those packets is the same attack by another road. writeRecvQ :: RecvQ -> ReceivedPacket -> IO ()-writeRecvQ (RecvQ q qlen) x = atomically $ do+writeRecvQ (RecvQ q qlen qbytes closed (RecvBudget lim used)) x = atomically $ do+    c <- readTVar closed     n <- readTVar qlen-    when (n < recvQLimit) $ do+    u <- readTVar used+    let b = rpReceivedBytes x+    when (not c && n < recvQLimit && u + b <= lim) $ do         writeTQueue q x         writeTVar qlen $! n + 1+        modifyTVar' qbytes (+ b)+        writeTVar used $! u + b  -- | Put back a packet this connection had already taken. ----- Not subject to the limit: these come from the packets put off for want of--- the keys to read them, which the handshake bounds, and dropping one would--- lose a packet the connection has already accounted for.+-- Not subject to the limits: these come from the packets put off for want+-- of the keys to read them, which the handshake bounds, and dropping one+-- would lose a packet the connection has already accounted for. prependRecvQ :: RecvQ -> ReceivedPacket -> STM ()-prependRecvQ (RecvQ q qlen) x = do+prependRecvQ (RecvQ q qlen qbytes _ (RecvBudget _ used)) x = do+    let b = rpReceivedBytes x     unGetTQueue q x     modifyTVar' qlen (+ 1)+    modifyTVar' qbytes (+ b)+    modifyTVar' used (+ b)++-- | Closing a queue nobody will read again: what it holds goes back to the+--   budget, and what is written to it from now on is dropped.  A queue the+--   dispatcher can still find, for a connection that has ended, would+--   otherwise hold its share of the budget for good.+closeRecvQ :: RecvQ -> IO ()+closeRecvQ (RecvQ q qlen qbytes closed (RecvBudget _ used)) = atomically $ do+    writeTVar closed True+    b <- readTVar qbytes+    modifyTVar' used (subtract b)+    writeTVar qbytes 0+    writeTVar qlen 0+    void $ flushTQueue q++----------------------------------------------------------------++-- | How many DATAGRAM frames a connection may have waiting for the+--   application.  Each fits in a packet, so this is a megabyte and a half+--   or so (#226).+datagramQLimit :: Int+datagramQLimit = 1024++data DatagramQ = DatagramQ (TQueue ByteString) (TVar Int)++newDatagramQ :: IO DatagramQ+newDatagramQ = DatagramQ <$> newTQueueIO <*> newTVarIO 0++readDatagramQSTM :: DatagramQ -> STM ByteString+readDatagramQSTM (DatagramQ q qlen) = do+    x <- readTQueue q+    modifyTVar' qlen (subtract 1)+    return x++flushDatagramQSTM :: DatagramQ -> STM [ByteString]+flushDatagramQSTM (DatagramQ q qlen) = do+    writeTVar qlen 0+    flushTQueue q++-- | Queue a DATAGRAM, or drop it if 'datagramQLimit' are already waiting.+--   RFC 9221 Sec 5 lets a receiver drop DATAGRAM frames.+writeDatagramQ :: DatagramQ -> ByteString -> IO ()+writeDatagramQ (DatagramQ q qlen) x = atomically $ do+    n <- readTVar qlen+    when (n < datagramQLimit) $ do+        writeTQueue q x+        writeTVar qlen $! n + 1
quic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               quic-version:            0.3.22+version:            0.3.23 license:            BSD3 license-file:       LICENSE maintainer:         kazu@iij.ad.jp
test/DatagramSpec.hs view
@@ -3,6 +3,7 @@ module DatagramSpec where  import Control.Concurrent+import Control.Concurrent.STM (atomically) import qualified Control.Exception as E import Control.Monad import Data.ByteString ()@@ -138,6 +139,16 @@                         sendDatagram conn (BS.replicate 50 0x41)                         threadDelay 500 +    -- #226: the queue grew for as long as the peer sent faster than the+    -- application read.+    describe "the DATAGRAM receive queue" $+        it "drops what arrives past its limit" $ do+            q <- newDatagramQ+            forM_ [1 .. datagramQLimit + 10] $ \_ -> writeDatagramQ q "x"+            length <$> atomically (flushDatagramQSTM q)+                `shouldReturn` datagramQLimit+            writeDatagramQ q "y"+            atomically (flushDatagramQSTM q) `shouldReturn` ["y"]     describe "Datagram SHOULD requirements" $ do         -- "This frame SHOULD be sent as soon as possible... and MAY be coalesced with other frames."         it "Can send and receive DATAGRAM in 1-RTT" $ do
test/HandshakeSpec.hs view
@@ -4,6 +4,7 @@ module HandshakeSpec where  import Control.Concurrent+import Control.Concurrent.Async (wait, withAsync) import qualified Control.Exception as E import Control.Monad import qualified Data.ByteString as BS@@ -78,6 +79,21 @@             let cc = testClientConfig                 sc = sc0{scRequireRetry = True}             testHandshake cc sc waitS FullHandshake+        -- A token that proves nothing does not stand in for the Retry.+        it "requires a Retry of a client with an invalid token" $ do+            let cc =+                    testClientConfig+                        { ccResumption =+                            (ccResumption testClientConfig)+                                { resumptionToken = "invalid token"+                                , resumptionVersion = ccVersion testClientConfig+                                }+                        }+                sc = sc0{scRequireRetry = True}+            E.bracket (forkIO $ S.run sc $ \_ -> threadDelay 2000000) killThread $ \_ -> do+                waitS+                C.run cc (\conn -> waitEstablished conn >> retry <$> getConnectionInfo conn)+                    `shouldReturn` True         it "can handshake in the case of resumption" $ do             let cc = testClientConfig                 sc = sc0@@ -152,6 +168,31 @@                         te == cryptoError TLS.HandshakeFailure                     | otherwise = False             testHandshake3 cc1 cc2 sc waitS handshakeFailure+        -- RFC 9000 Sec 5.2.2: a connection over the limit is refused with+        -- CONNECTION_REFUSED, and one ending makes room for another (#170).+        it "refuses a connection over scMaxConnections" $ do+            let sc = sc0{scMaxConnections = 1}+            first <- newEmptyMVar+            done <- newEmptyMVar+            let holder = C.run testClientConfig $ \conn -> do+                    waitEstablished conn+                    putMVar first ()+                    takeMVar done+                refused (TransportErrorIsReceived ConnectionRefused _) = True+                refused _ = False+                establish = C.run testClientConfig waitEstablished+                again 0 = establish+                again n =+                    establish `E.catch` \(_ :: QUICException) ->+                        threadDelay 200000 >> again (n - 1 :: Int)+            E.bracket (forkIO $ S.run sc $ \_ -> threadDelay 10000000) killThread $ \_ -> do+                waitS+                withAsync holder $ \h -> do+                    takeMVar first+                    establish `shouldThrow` refused+                    putMVar done ()+                    wait h+                again 10         it "reconnects with the version a Version Negotiation offers" $ do             let cc = testClientConfig{ccVersion = Version2, ccVersions = [Version2, Version1]}                 sc = sc0{scVersions = [Version1]}
test/IOSpec.hs view
@@ -6,7 +6,7 @@  import Control.Concurrent import Control.Concurrent.Async-import Control.Concurrent.STM (readTVarIO)+import Control.Concurrent.STM (atomically, readTVarIO) import qualified Control.Exception as E import Control.Monad import Data.Bits (shiftR)@@ -15,6 +15,7 @@ import Data.IORef import qualified Data.IntMap.Strict as IntMap import GHC.Conc (getUncaughtExceptionHandler, setUncaughtExceptionHandler)+import Network.Control (txfSent) import qualified Network.Socket as NS import qualified Network.Socket.ByteString as NSB import System.Directory (listDirectory)@@ -79,6 +80,24 @@             -- The first one taken and lost: the second keeps its offset.             _ <- takeSendStreamQ clientConn             offset <$> takeSendStreamQ clientConn `shouldReturn` (6, 16)+    -- RFC 9000 Sec 4.1: two streams sending at once used to both see the+    -- same connection window and queue twice what MAX_DATA allowed.+    describe "the connection window" $+        it "is not given to two streams sending at once" $ do+            scw <- makeTestServerConfig+            (clientConn, _) <- makeConnections scw Version1+            overs <- forM [0 .. 999] $ \i -> do+                s1 <- addStream clientConn (8 * i)+                s2 <- addStream clientConn (8 * i + 4)+                setTxMaxStreamData s1 100+                setTxMaxStreamData s2 100+                base <- txfSent <$> atomically (readConnectionFlowTx clientConn)+                setTxMaxData clientConn $ base + 100+                let send s = Timeout.timeout 1000 $ sendStream s $ BS.replicate 100 0+                _ <- concurrently (send s1) (send s2)+                sent <- txfSent <$> atomically (readConnectionFlowTx clientConn)+                return $ sent - base+            filter (> 100) overs `shouldBe` []     describe "a stateless reset to the client" $         it "ends the connection with ConnectionIsReset" $             testStatelessResetReceived cc sc waitS@@ -173,11 +192,48 @@             withPipe (DelayServerPacket 300) $ testLateCopy False cc sc waitS         it "ignores a late copy of the data it received on a stream the peer opened" $ do             withPipe (DelayClientPacket 300) $ testLateCopy True cc sc waitS+    -- RFC 9000 Sec 17.2.3: 0-RTT goes from client to server only.+    describe "a frame received in 0-RTT" $ do+        let replied serverConn = do+                out <- atomically $ takeOutputSTM serverConn+                case out of+                    OutControl lvl frames -> return (lvl, frames)+                    _ -> fail "no control frames"+        it "has its ACK sent in 1-RTT" $ do+            scz <- makeTestServerConfig+            (_, serverConn) <- makeConnections scz Version1+            processFrame serverConn RTT0Level Ping+            fst <$> replied serverConn `shouldReturn` RTT1Level+        it "has its RESET_STREAM sent in 1-RTT" $ do+            scz <- makeTestServerConfig+            (_, serverConn) <- makeConnections scz Version1+            _ <- addStream serverConn 0+            processFrame serverConn RTT0Level $ StopSending 0 $ ApplicationProtocolError 0+            (lvl, frames) <- replied serverConn+            lvl `shouldBe` RTT1Level+            frames `shouldSatisfy` any isResetStream     describe "stream limits" $ do+        -- #86: the number the peer may have open is no longer fixed.+        it "announces a raised limit at once and holds a lowered one" $ do+            scl <- makeTestServerConfig+            (_, serverConn) <- makeConnections scl Version1+            let initial = initialMaxStreamsBidi $ getMyParameters serverConn+            setMaxBidiStreams serverConn $ initial + 10+            out <- atomically $ takeOutputSTM serverConn+            case out of+                OutControl RTT1Level [MaxStreams Bidirectional n] -> n `shouldBe` initial + 10+                _ -> expectationFailure "no MAX_STREAMS"+            -- MAX_STREAMS cannot go down, and closing one stream of many+            -- under a lower target gives the peer no new one.+            setMaxBidiStreams serverConn 1+            atomically (isEmptyOutputSTM serverConn) `shouldReturn` True+            checkStreamIdRoom serverConn Bidirectional `shouldReturn` Nothing         it "keeps the peer's open bidirectional streams within the limit" $ do             withPipe (DropClientPacket []) $ testOpenStreams cc sc waitS         it "raises the limit on unidirectional streams by their own number" $ do             withPipe (DropClientPacket []) $ testUniStreams cc sc waitS+        it "ignores STOP_SENDING for a unidirectional stream it has closed" $ do+            withPipe (DropClientPacket []) $ testStopClosedUni cc sc waitS     describe "a stream closed before the peer finishes it" $ do         it "is forgotten once the peer answers STOP_SENDING" $ do             withPipe (DropClientPacket []) $ testDepartedForgotten cc sc waitS@@ -659,6 +715,48 @@         forever $ do             strm <- acceptStream conn             recvStream strm 1++isResetStream :: Frame -> Bool+isResetStream ResetStream{} = True+isResetStream _ = False++-- | A unidirectional stream used to be checked against the counter of+--   bidirectional ones, so a client that had opened none of those closed the+--   connection with STREAM_STATE_ERROR on a STOP_SENDING for stream 2.+testStopClosedUni :: C.ClientConfig -> ServerConfig -> IO () -> IO ()+testStopClosedUni cc sc waitS = do+    closed <- newEmptyMVar+    stopped <- newEmptyMVar+    withAsync (server closed stopped) $ \_ -> client closed stopped+  where+    client closed stopped = do+        waitS+        C.run cc $ \conn -> do+            uni <- unidirectionalStream conn+            sendStream uni "x"+            closeStream uni+            putMVar closed ()+            () <- takeMVar stopped+            threadDelay 200000+            strm <- stream conn+            sendStream strm "ping"+            shutdownStream strm+            recvStream strm 4 `shouldReturn` "pong"+    server closed stopped = run sc $ \conn -> do+        uni <- acceptStream conn+        recvStream uni 1 `shouldReturn` "x"+        () <- takeMVar closed+        -- Sent as is: 'stopStream' sends nothing for a stream whose FIN+        -- has arrived, and a peer's may cross it.+        sendFrames+            conn+            RTT1Level+            [StopSending (streamId uni) $ ApplicationProtocolError 0]+        putMVar stopped ()+        strm <- acceptStream conn+        _ <- recvStream strm 4+        sendStream strm "pong"+        shutdownStream strm  testRecvStreamClientStopFirst     :: C.ClientConfig -> ServerConfig -> IO () -> IO ()
test/MigrationSpec.hs view
@@ -5,7 +5,7 @@ import Control.Concurrent import Control.Concurrent.Async (concurrently, mapConcurrently, wait, withAsync) import Control.Concurrent.STM-import Control.Monad (forM, void)+import Control.Monad (forM, forM_, void) import qualified Data.ByteString as BS import qualified Data.ByteString.Short as Short import Network.Control (txfLimit)@@ -51,6 +51,27 @@     -- CID of its own on the new path, while the old path keeps the old     -- one; the old one was given up at once, and the old path's challenge     -- went out with the new.+    -- #223: a NEW_CONNECTION_ID retiring every CID it does not bring+    -- left none to use, and the connection ended with INTERNAL_ERROR.+    describe "addPeerCIDAndRetire" $ do+        let cid :: Int -> CID+            cid i = toCID $ BS.replicate 8 $ fromIntegral i+            srt i = StatelessResetToken $ Short.toShort $ BS.replicate 16 $ fromIntegral i+            info i = newCIDInfo i (cid i) (srt i)+        it "moves to the new CID when all the others are retired" $ do+            (_, serverConn) <- makeConnections serverConf Version1+            addPeerCIDAndRetire serverConn (info 1) 1 `shouldReturn` Just [0]+            getPeerCID serverConn `shouldReturn` cid 1+        it "lets a CID exceed the limit when the excess is retired" $ do+            (_, serverConn) <- makeConnections serverConf Version1+            let lim = activeConnectionIdLimit $ getMyParameters serverConn+            forM_ [1 .. lim - 1] $ \i ->+                addPeerCIDAndRetire serverConn (info i) 0 `shouldReturn` Just []+            addPeerCIDAndRetire serverConn (info lim) 0 `shouldReturn` Nothing+            addPeerCIDAndRetire serverConn (info lim) lim+                `shouldReturn` Just [0 .. lim - 1]+            getPeerCID serverConn `shouldReturn` cid lim+     describe "validateMigration" $ do         let next conn = do                 out <- T.timeout 4000000 $ atomically $ takeOutputSTM conn
test/PacketSpec.hs view
@@ -180,7 +180,7 @@     -- dummy     let clientConf = testClientConfig     (sock, peersa) <- clientSocket "127.0.0.1" "2000"-    q <- newRecvQ+    q <- newRecvQ =<< newRecvBudget maxBound     retire <- newTVarIO False     sref <- newIORef (sock, retire)     pathInfo <- newPathInfo peersa@@ -188,7 +188,7 @@     let ver = v         verInfo = VersionInfo ver [ver]     genSRT <- makeGenStatelessReset-    dq <- newTQueueIO+    dq <- newDatagramQ     ----     clientConn <-         clientConnection
test/RecoverySpec.hs view
@@ -4,6 +4,7 @@  import Control.Concurrent.STM import Data.ByteString.Short ()+import Data.Maybe (isJust) import qualified Data.Sequence as Seq import Data.UnixTime import Test.Hspec@@ -51,6 +52,28 @@             atomically $ writeTVar (encryptionLevel cs) RTT1Level             onAckReceived ldcc HandshakeLevel (AckInfo 0 0 []) 0             getPtoTimeAndSpace ldcc `shouldReturn` Nothing+    -- RFC 9002 B.8: only packets in flight make a congestion event.+    describe "a lost packet" $ do+        let lostWith frames = do+                cs <- newConnState Client+                ldcc <- newLDCC cs (\_ -> return ()) (\_ -> return ())+                now <- getTimeMicrosecond+                let lost =+                        (sp 1 (any ackEliciting frames) now)+                            { spEncryptionLevel = RTT1Level+                            , spSentBytes = 1200+                            , spPlainPacket = PlainPacket (Short $ toCID "") (Plain (Flags 0) 1 frames 0)+                            }+                onPacketsLost ldcc $ Seq.singleton lost+                retransmit ldcc $ Seq.singleton lost+                -- A congestion event starts a recovery period.+                event <- isJust . congestionRecoveryStartTime <$> readTVarIO (recoveryCC ldcc)+                ping <- readTVarIO $ ptoPing ldcc+                return (event, ping)+        it "is no congestion event and needs no PING if it carried only an ACK" $+            lostWith [Ack (AckInfo 0 0 []) noDelay] `shouldReturn` (False, Nothing)+        it "is a congestion event if it was in flight" $+            fst <$> lostWith [Ping] `shouldReturn` True     describe "persistent congestion" $ do         it "does not find a pair" $ do             findDuration (Seq.fromList [sp0]) 0 `shouldBe` Nothing
test/TypesSpec.hs view
@@ -43,7 +43,7 @@     -- anyone who can see a connection ID can fill it.  See #167.     describe "RecvQ" $ do         it "keeps what it has taken and drops what is past the limit" $ do-            q <- newRecvQ+            q <- newRecvQ =<< newRecvBudget maxBound             mapM_ (writeRecvQ q . numbered) [1 .. recvQLimit + 100]             ns <- replicateM recvQLimit (number <$> readRecvQ q)             -- The packets kept are the ones that arrived first: what is@@ -52,7 +52,7 @@             ns `shouldBe` [1 .. recvQLimit]             Timeout.timeout 100000 (readRecvQ q) `shouldReturn` Nothing         it "takes back a packet it had already taken, full or not" $ do-            q <- newRecvQ+            q <- newRecvQ =<< newRecvBudget maxBound             mapM_ (writeRecvQ q . numbered) [1 .. recvQLimit]             -- Putting back what was put off for want of keys is not the             -- same as taking something new on, and the limit does not apply@@ -60,6 +60,29 @@             -- for.             atomically $ prependRecvQ q $ numbered 0             number <$> readRecvQ q `shouldReturn` 0+        -- #170: what one connection may queue times the number of+        -- connections was a limit on nothing.+        it "shares a budget with the other queues of the endpoint" $ do+            budget <- newRecvBudget 3600 -- three packets of 1200+            q1 <- newRecvQ budget+            q2 <- newRecvQ budget+            mapM_ (writeRecvQ q1 . numbered) [1, 2]+            mapM_ (writeRecvQ q2 . numbered) [3, 4]+            number <$> readRecvQ q1 `shouldReturn` 1+            writeRecvQ q2 $ numbered 5+            number <$> readRecvQ q2 `shouldReturn` 3+            number <$> readRecvQ q2 `shouldReturn` 5+        it "gives back what a closed queue holds and takes no more" $ do+            budget <- newRecvBudget 2400+            q1 <- newRecvQ budget+            q2 <- newRecvQ budget+            mapM_ (writeRecvQ q1 . numbered) [1, 2]+            closeRecvQ q1+            writeRecvQ q1 $ numbered 3+            mapM_ (writeRecvQ q2 . numbered) [4, 5]+            number <$> readRecvQ q2 `shouldReturn` 4+            number <$> readRecvQ q2 `shouldReturn` 5+            Timeout.timeout 100000 (readRecvQ q1) `shouldReturn` Nothing     -- RFC 9000 Sec 19.3: the field counts units of 2 ^ ack_delay_exponent     -- microseconds, and it is a varint, so up to 2 ^ 62 - 1 of them.     describe "ackDelayMicro" $ do