packages feed

quic 0.3.20 → 0.3.21

raw patch · 29 files changed

+655/−139 lines, 29 filesdep ~asyncdep ~tlsPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: async, tls

API changes (from Hackage documentation)

+ Network.QUIC.Internal: MigrationAbandoned :: MigrationState
+ Network.QUIC.Internal: OutPathProbe :: PathInfo -> [Frame] -> Output
+ Network.QUIC.Internal: SentPackets :: Seq SentPacket -> SentPackets
+ Network.QUIC.Internal: [pathAbandoned] :: PathInfo -> TVar Bool
+ Network.QUIC.Internal: abandonMigration :: Connection -> IO ()
+ Network.QUIC.Internal: getPtoTimeAndSpace :: LDCC -> IO (Maybe (TimeMicrosecond, EncryptionLevel))
+ Network.QUIC.Internal: newtype SentPackets
+ Network.QUIC.Internal: requestEnd :: Exception e => Connection -> e -> IO ()
+ Network.QUIC.Internal: resetTxMaxData :: Connection -> Int -> IO ()
+ Network.QUIC.Internal: resetTxMaxStreamData :: Stream -> Int -> IO ()
+ Network.QUIC.Internal: resetTxMaxStreams :: Connection -> Int -> IO ()
+ Network.QUIC.Internal: resetTxStreamLimits :: Connection -> Parameters -> IO ()
+ Network.QUIC.Internal: resetTxUniMaxStreams :: Connection -> Int -> IO ()
+ Network.QUIC.Internal: tableStreams :: StreamTable -> [Stream]
- Network.QUIC.Internal: Connection :: ConnState -> DebugLogger -> QLogger -> Hooks -> ~Send -> ~Recv -> RecvQ -> DatagramQ -> IORef (Socket, TVar Bool) -> IORef [Socket] -> (CID -> StatelessResetToken) -> ThreadManager -> ThreadId -> Rate -> IORef RoleInfo -> IORef VersionInfo -> VersionInfo -> Parameters -> IORef CIDDB -> IORef Parameters -> TVar CIDDB -> IORef PeerInfo -> InputQ -> CryptoQ -> OutputQ -> Rate -> TVar Int -> Shared -> IORef Int -> IORef (IO ()) -> Array EncryptionLevel (IORef PacketNumber) -> TVar StreamTable -> TVar (IntMap Departed) -> TVar Concurrency -> TVar Concurrency -> IORef Concurrency -> IORef Concurrency -> IORef OpenedStreams -> IORef OpenedStreams -> TVar TxFlow -> IORef RxFlow -> TVar MigrationState -> IORef Bool -> IORef Microseconds -> IORef Int -> IORef Int -> TVar Bool -> TVar (Maybe (ApplicationProtocolError, ReasonPhrase)) -> Array EncryptionLevel (TVar [ReceivedPacket]) -> IOArray EncryptionLevel Cipher -> IOArray EncryptionLevel Coder -> IOArray Bool Coder1RTT -> IOArray EncryptionLevel Protector -> IORef (Maybe (Coder, Protector)) -> IORef (Bool, PacketNumber) -> IORef Negotiated -> IORef AuthCIDs -> IORef AuthCIDs -> Buffer -> SizedBuffer -> Buffer -> IORef (IO ()) -> IORef Bool -> IORef Bool -> IORef Int -> IORef Int -> LDCC -> Connection
+ Network.QUIC.Internal: Connection :: ConnState -> DebugLogger -> QLogger -> Hooks -> ~Send -> ~Recv -> RecvQ -> DatagramQ -> IORef (Socket, TVar Bool) -> IORef [Socket] -> (CID -> StatelessResetToken) -> ThreadManager -> ThreadId -> Rate -> IORef RoleInfo -> IORef VersionInfo -> VersionInfo -> Parameters -> IORef CIDDB -> IORef Parameters -> TVar CIDDB -> IORef PeerInfo -> InputQ -> CryptoQ -> OutputQ -> Rate -> TVar Int -> Shared -> IORef Int -> IORef (IO ()) -> Array EncryptionLevel (IORef PacketNumber) -> TVar StreamTable -> TVar (IntMap Departed) -> TVar Concurrency -> TVar Concurrency -> IORef Concurrency -> IORef Concurrency -> IORef OpenedStreams -> IORef OpenedStreams -> TVar TxFlow -> IORef RxFlow -> TVar MigrationState -> IORef Bool -> IORef Microseconds -> IORef Int -> IORef Int -> TVar Bool -> TVar (Maybe SomeException) -> Array EncryptionLevel (TVar [ReceivedPacket]) -> IOArray EncryptionLevel Cipher -> IOArray EncryptionLevel Coder -> IOArray Bool Coder1RTT -> IOArray EncryptionLevel Protector -> IORef (Maybe (Coder, Protector)) -> IORef (Bool, PacketNumber) -> IORef Negotiated -> IORef AuthCIDs -> IORef AuthCIDs -> Buffer -> SizedBuffer -> Buffer -> IORef (IO ()) -> IORef Bool -> IORef Bool -> IORef Int -> IORef Int -> LDCC -> Connection
- Network.QUIC.Internal: PathInfo :: SockAddr -> TVar Int -> TVar Int -> TVar Bool -> PathInfo
+ Network.QUIC.Internal: PathInfo :: SockAddr -> TVar Int -> TVar Int -> TVar Bool -> TVar Bool -> PathInfo
- Network.QUIC.Internal: SendChallenge :: PathInfo -> PathData -> MigrationState
+ Network.QUIC.Internal: SendChallenge :: PathInfo -> [PathData] -> MigrationState
- Network.QUIC.Internal: [connAbortRequest] :: Connection -> TVar (Maybe (ApplicationProtocolError, ReasonPhrase))
+ Network.QUIC.Internal: [connAbortRequest] :: Connection -> TVar (Maybe SomeException)

Files

ChangeLog.md view
@@ -1,5 +1,42 @@ # ChangeLog +## 0.3.21++Requires tls 2.5.1 and async 2.2, and allows iproute 1.8 and unix-time+0.6.  A server now keeps the limits 0-RTT is held to in the session+(`sessionAppData`, which it overwrites) and refuses 0-RTT if it allows less+now; a session saved without them, e.g. by tls 2.5.0, gets no 0-RTT.  A+client whose 0-RTT is not accepted drops the limits it remembered, but+what it already sent in 0-RTT is still resent as 1-RTT.  In+`Network.QUIC.Internal`, `connAbortRequest` holds an exception, `Output`+has `OutPathProbe`, and `SendChallenge` keeps a list of payloads.++* Release lost candidates only on a 1-RTT ACK+  [#204](https://github.com/kazu-yamamoto/quic/pull/204)+* Follow RFC 9002 A.8 in choosing the PTO space+  [#205](https://github.com/kazu-yamamoto/quic/pull/205)+* Decode ACK_ECN as ACK+  [#206](https://github.com/kazu-yamamoto/quic/pull/206)+* Refuse an empty NEW_TOKEN+  [#207](https://github.com/kazu-yamamoto/quic/pull/207)+* Do not check the QUIC bit of Version Negotiation+  [#208](https://github.com/kazu-yamamoto/quic/pull/208)+* Go back to the old path as soon as the peer is heard there+  [#209](https://github.com/kazu-yamamoto/quic/pull/209)+* Challenge the old path too on a migration+  [#210](https://github.com/kazu-yamamoto/quic/pull/210)+* Ask the protocol threads to end instead of throwing to the main thread+  [#211](https://github.com/kazu-yamamoto/quic/pull/211)+* Reset the remembered limits when 0-RTT is rejected+  [#212](https://github.com/kazu-yamamoto/quic/pull/212)+* Refuse 0-RTT when the server allows less than it did+  [#213](https://github.com/kazu-yamamoto/quic/pull/213)+* Require async 2.2+  [#214](https://github.com/kazu-yamamoto/quic/pull/214)+* Reset the remembered limits whenever 0-RTT is not accepted+  [#215](https://github.com/kazu-yamamoto/quic/pull/215)+* Send the path challenge again with a new payload until answered+  [#216](https://github.com/kazu-yamamoto/quic/pull/216)  ## 0.3.20 
Network/QUIC/Client/Reader.hs view
@@ -12,10 +12,10 @@  import Control.Concurrent import Control.Concurrent.STM-import Data.Functor (($>))-import qualified Control.Monad.STM as STM import qualified Control.Exception as E+import qualified Control.Monad.STM as STM import qualified Data.ByteString as BS+import Data.Functor (($>)) import Data.List (intersect) import Network.Socket (Socket, getSocketName) import qualified Network.Socket.ByteString as NSB@@ -120,7 +120,7 @@                 nextVerInfo = case myVers `intersect` peerVers of                     vers@(ver : _) -> VersionInfo ver vers                     _ -> brokenVersionInfo-            E.throwTo (mainThreadId conn) $ VerNego nextVerInfo+            requestEnd conn $ VerNego nextVerInfo     putQ t (PacketIC pkt@(CryptPacket hdr crypt) lvl siz) = do         let cid = headerMyCID hdr         included <- myCIDsInclude conn cid@@ -137,7 +137,7 @@                     when isStatelessReset $ do                         qlogReceived conn StatelessReset t                         connDebugLog conn "debug: connection is reset statelessly"-                        E.throwTo (mainThreadId conn) ConnectionIsReset+                        requestEnd conn ConnectionIsReset                 _ -> return () -- really invalid, just ignore     putQ t (PacketIR pkt@(RetryPacket ver dCID sCID token ex)) = do         qlogReceived conn pkt t
Network/QUIC/Connection/Migration.hs view
@@ -23,6 +23,7 @@     setPeerStatelessResetToken,     isStatelessRestTokenValid,     claimMigration,+    abandonMigration,     checkResponse,     validatePath,     getMyRetirePriorTo,@@ -355,8 +356,8 @@ validatePath conn pathInfo Nothing = do     pdat <- newPathData     setChallenges conn pathInfo pdat-    putOutput conn $ OutControl RTT1Level [PathChallenge pdat]-    waitResponse conn+    putOutput conn $ challengeOutput conn pathInfo [PathChallenge pdat]+    waitResponse conn pathInfo validatePath conn pathInfo (Just cidInfo) = do     pdat <- newPathData     setChallenges conn pathInfo pdat@@ -375,12 +376,20 @@             else                 return []     let frames = extra ++ [PathChallenge pdat, RetireConnectionID retiredSeqNum]-    putOutput conn $ OutControl RTT1Level frames-    waitResponse conn+    putOutput conn $ challengeOutput conn pathInfo frames+    waitResponse conn pathInfo +-- | A server sends the challenge to the path it is for, which may not be+--   the one in use by the time it leaves.  A client's socket may be+--   connected, and so cannot be given an address.+challengeOutput :: Connection -> PathInfo -> [Frame] -> Output+challengeOutput conn pathInfo frames+    | isServer conn = OutPathProbe pathInfo frames+    | otherwise = OutControl RTT1Level frames+ setChallenges :: Connection -> PathInfo -> PathData -> IO () setChallenges Connection{..} pathInfo pdat =-    atomically $ writeTVar migrationState $ SendChallenge pathInfo pdat+    atomically $ writeTVar migrationState $ SendChallenge pathInfo [pdat]  -- | Take on the migration, if no one has it. --@@ -406,24 +415,61 @@ -- | Waiting for the PATH_RESPONSE, for three times the larger of the PTO --   and the PTO of a new path (RFC 9000 Sec 8.2.4).  It used to wait for --   ever, and no other migration could start meanwhile.-waitResponse :: Connection -> IO Bool-waitResponse Connection{..} = do+--+-- A challenge is sent again, with a new payload, every one of those+--   periods (Sec 8.2.1): a lost one is not resent, and with only one a+--   single loss failed the validation.+waitResponse :: Connection -> PathInfo -> IO Bool+waitResponse conn@Connection{..} pathInfo = do     pto <- getPTO connLDCC-    let Microseconds us = 3 * max pto (calcPTO initialRTT Nothing)-    expired <- registerDelay us-    atomically $ do-        state <- readTVar migrationState-        timedOut <- readTVar expired-        check (state == RecvResponse || timedOut)-        writeTVar migrationState NonMigration-        return (state == RecvResponse)+    let period = max pto (calcPTO initialRTT Nothing)+        Microseconds total = 3 * period+        Microseconds every = period+    expired <- registerDelay total+    let loop = do+            again <- registerDelay every+            r <- atomically $ do+                state <- readTVar migrationState+                timedOut <- readTVar expired+                resend <- readTVar again+                if state == RecvResponse || state == MigrationAbandoned || timedOut+                    then do+                        writeTVar migrationState NonMigration+                        return $ Just (state == RecvResponse)+                    else if resend then return Nothing else retry+            case r of+                Just ok -> return ok+                Nothing -> do+                    pdat <- newPathData+                    sent <- atomically $ do+                        state <- readTVar migrationState+                        case state of+                            SendChallenge p pdats -> do+                                writeTVar migrationState $ SendChallenge p (pdat : pdats)+                                return True+                            _ -> return False+                    when sent $+                        putOutput conn $+                            challengeOutput conn pathInfo [PathChallenge pdat]+                    loop+    loop +-- | RFC 9000 Sec 9.3.2: a newer packet from the last validated address+--   ends the validation of the new one, which then fails at once and+--   sends us back there.+abandonMigration :: Connection -> IO ()+abandonMigration Connection{..} = atomically $ do+    s <- readTVar migrationState+    case s of+        SendChallenge{} -> writeTVar migrationState MigrationAbandoned+        _ -> return ()+ checkResponse :: Connection -> PathData -> IO () checkResponse Connection{..} pdat = do     state <- readTVarIO migrationState     case state of-        SendChallenge pathInfo pdat'-            | pdat == pdat' -> atomically $ do+        SendChallenge pathInfo pdats+            | pdat `elem` pdats -> atomically $ do                 writeTVar migrationState RecvResponse                 writeTVar (addressValidated pathInfo) True         _ -> return ()
Network/QUIC/Connection/Misc.hs view
@@ -38,6 +38,7 @@     sendFrames,     closeConnection,     abortConnection,+    requestEnd,     watchAbortRequest,     abortRequested, ) where@@ -141,7 +142,10 @@  -- | Going back to a path, forgetting the one in use. restorePathInfo :: Connection -> PathInfo -> IO ()-restorePathInfo Connection{..} oldpi = writeIORef peerInfo $ PeerInfo oldpi Nothing+restorePathInfo Connection{..} oldpi = do+    PeerInfo failed _ <- readIORef peerInfo+    writeIORef peerInfo $ PeerInfo oldpi Nothing+    atomically $ writeTVar (pathAbandoned failed) True  findPathInfo :: Connection -> SockAddr -> IO (Maybe PathInfo) findPathInfo Connection{..} sa = do@@ -314,10 +318,16 @@ -- just never looks.  The first request is the one acted on. abortConnection     :: Connection -> ApplicationProtocolError -> ReasonPhrase -> IO ()-abortConnection Connection{..} err desc =-    atomically $ modifyTVar' connAbortRequest $ \r -> r <|> Just (err, desc)+abortConnection conn err desc = requestEnd conn $ Abort err desc --- | A connection's ending, as 'abortConnection' asked for it if it did.+-- | Asking the connection's protocol threads to end it with this+--   exception, as 'abortConnection' does.  For threads outside them, such+--   as a reader, which used to throw to the connection's main thread.+requestEnd :: E.Exception e => Connection -> e -> IO ()+requestEnd Connection{..} e =+    atomically $ modifyTVar' connAbortRequest $ \r -> r <|> Just (E.toException e)++-- | A connection's ending, as 'requestEnd' asked for it if it did. --   An application that asks and returns at once can see the protocol --   threads end before they have acted on it; it is what it asked for all --   the same.@@ -325,15 +335,13 @@     :: Connection -> Either E.SomeException a -> IO (Either E.SomeException a) abortRequested Connection{..} ex@(Right _) = do     req <- readTVarIO connAbortRequest-    return $ case req of-        Nothing -> ex-        Just (err, desc) -> Left $ E.toException $ Abort err desc+    return $ maybe ex Left req abortRequested _ ex = return ex --- | Waiting for 'abortConnection' and raising 'Abort' when it is asked:+-- | Waiting for 'requestEnd' and raising what it was asked with: --   one of a connection's protocol threads, so that it ends the connection --   as any of them failing does. watchAbortRequest :: Connection -> IO () watchAbortRequest Connection{..} = do-    (err, desc) <- atomically $ readTVar connAbortRequest >>= maybe retry return-    E.throwIO $ Abort err desc+    e <- atomically $ readTVar connAbortRequest >>= maybe retry return+    E.throwIO e
Network/QUIC/Connection/State.hs view
@@ -15,6 +15,7 @@     readConnectionFlowTx,     addTxData,     setTxMaxData,+    resetTxMaxData,     getRxMaxData,     updateFlowRx,     checkRxMaxData,@@ -120,6 +121,11 @@         | txfLimit flow < n = flow{txfLimit = n}         | otherwise = flow +-- | 'setTxMaxData' that may lower: only for 0-RTT rejected.+resetTxMaxData :: Connection -> Int -> IO ()+resetTxMaxData Connection{..} n =+    atomically $ modifyTVar' flowTx $ \flow -> flow{txfLimit = n}+ ----------------------------------------------------------------  getRxMaxData :: Connection -> IO Int@@ -171,7 +177,8 @@ checkAntiAmplificationFreeSTM :: PathInfo -> Int -> STM Bool checkAntiAmplificationFreeSTM PathInfo{..} siz = do     validated <- readTVar addressValidated-    if validated+    abandoned <- readTVar pathAbandoned+    if validated || abandoned         then return True         else do             tx <- readTVar pathBytesTx
Network/QUIC/Connection/Stream.hs view
@@ -8,6 +8,8 @@     waitMyNewUniStreamId,     setTxMaxStreams,     setTxUniMaxStreams,+    resetTxMaxStreams,+    resetTxUniMaxStreams,     checkRxMaxStreams,     updatePeerStreamId,     checkStreamIdRoom,@@ -60,6 +62,17 @@  setTxUniMaxStreams :: Connection -> Int -> IO () setTxUniMaxStreams Connection{..} = set myUniStreamId++-- | Setting the limits on the streams I may open even lower: only for+--   0-RTT rejected.+resetTxMaxStreams :: Connection -> Int -> IO ()+resetTxMaxStreams Connection{..} = reset myStreamId++resetTxUniMaxStreams :: Connection -> Int -> IO ()+resetTxUniMaxStreams Connection{..} = reset myUniStreamId++reset :: TVar Concurrency -> Int -> IO ()+reset tvar mx = atomically $ modifyTVar' tvar $ \conc -> conc{maxStreams = StreamIdBase mx}  -- | Raising the limit on the streams I may open. --
Network/QUIC/Connection/StreamTable.hs view
@@ -6,6 +6,7 @@     claimPeerStream,     findStream,     addStream,+    resetTxStreamLimits,     delStream,     departStream,     Arrival (..),@@ -76,6 +77,17 @@                 newStream conn sid txLim rxLim     atomically $ modifyTVar' streamTable $ insertStream sid strm     return strm++-- | 0-RTT rejected (RFC 9001 Sec 4.6.2): the streams the client opened+--   under the remembered limits are held to what the server gives now.+resetTxStreamLimits :: Connection -> Parameters -> IO ()+resetTxStreamLimits Connection{..} serverParams = do+    strms <- tableStreams <$> readTVarIO streamTable+    forM_ strms $ \strm -> do+        let sid = streamId strm+        when (sid >= 0 && isClientInitiated sid) $+            resetTxMaxStreamData strm $+                serverInitial sid serverParams  delStream :: Connection -> Stream -> IO () delStream Connection{..} strm =
Network/QUIC/Connection/Types.hs view
@@ -7,6 +7,7 @@  import Control.Concurrent import Control.Concurrent.STM+import qualified Control.Exception as E import qualified Crypto.Token as CT import Data.Array.IO import Data.ByteString.Internal@@ -114,14 +115,17 @@ data MigrationState     = NonMigration     | MigrationStarted-    | SendChallenge PathInfo PathData+    | -- | The challenges sent so far, the latest first.+      SendChallenge PathInfo [PathData]     | RecvResponse+    | MigrationAbandoned  {- FOURMOLU_DISABLE -} instance Eq MigrationState where     NonMigration     == NonMigration     = True     MigrationStarted == MigrationStarted = True     RecvResponse     == RecvResponse     = True+    MigrationAbandoned == MigrationAbandoned = True     _                == _                = False {- FOURMOLU_ENABLE -} @@ -237,6 +241,9 @@     , pathBytesTx :: TVar Int -- TVar for anti amplification     , pathBytesRx :: TVar Int -- TVar for anti amplification     , addressValidated :: TVar Bool+    , pathAbandoned :: TVar Bool+    -- ^ Gone back from: a send waiting for its anti-amplification+    --   credit goes to the path now in use instead.     }  newPathInfo :: SockAddr -> IO PathInfo@@ -244,6 +251,7 @@     pathBytesTx <- newTVarIO 0     pathBytesRx <- newTVarIO 0     addressValidated <- newTVarIO False+    pathAbandoned <- newTVarIO False     return PathInfo{..}  ----------------------------------------------------------------@@ -319,9 +327,9 @@     , bytesTx           :: IORef Int     , bytesRx           :: IORef Int     , connDone          :: TVar Bool-    , connAbortRequest  :: TVar (Maybe (ApplicationProtocolError, ReasonPhrase))-    -- ^ What 'Network.QUIC.Connection.Misc.abortConnection' asked to close-    --   the connection with, for its protocol threads to act on.+    , connAbortRequest  :: TVar (Maybe E.SomeException)+    -- ^ How 'Network.QUIC.Connection.Misc.requestEnd' asked the connection+    --   to end, for its protocol threads to act on.     , -- TLS       pendingQ          :: Array EncryptionLevel (TVar [ReceivedPacket])     , ciphers           :: IOArray EncryptionLevel Cipher@@ -555,6 +563,8 @@     = OutControl EncryptionLevel [Frame]     | OutHandshake [(EncryptionLevel, ByteString)]     | OutRetrans PlainPacket+    | -- | 1-RTT frames for a path other than the one in use.+      OutPathProbe PathInfo [Frame]  -- | The control frames of an output, which 'queuedControls' counts. --   Not data, which flow control bounds, nor what is never resent.@@ -562,6 +572,7 @@ countControls (OutControl _ frames) = length $ filter isControl frames countControls (OutRetrans (PlainPacket _ plain)) = length $ filter isControl $ plainFrames plain countControls OutHandshake{} = 0+countControls (OutPathProbe _ frames) = length $ filter isControl frames  isControl :: Frame -> Bool isControl Padding{} = False
Network/QUIC/Handshake.hs view
@@ -321,25 +321,19 @@                 _ -> sendCCVNError      setParams params = do-        when (isClient conn) $ do-            is0RTT <- isConnection0RTTReady conn-            when is0RTT $ do-                storedParams <- getPeerParameters conn-                let storedMaxDatagram = maxDatagramFrameSize storedParams-                    newMaxDatagram = maxDatagramFrameSize params-                when (storedMaxDatagram > 0 && newMaxDatagram < storedMaxDatagram) $-                    E.throwIO WrongDatagramSizeParameter-                -- RFC 9000 Sec 7.4.1: "If 0-RTT data is accepted by the-                -- server, the server MUST NOT reduce any limits".  The limits-                -- only ever rise here, so one lowered all the same was kept-                -- at the old value, and the server closed the connection over-                -- what it had itself allowed before.-                minfo <- TLS.contextGetInformation ctx-                let accepted = (minfo >>= TLS.infoTLS13HandshakeMode) == Just TLS.RTT0-                when (accepted && lowered storedParams params) $-                    E.throwIO Wrong0RTTLimitParameter-+        -- Also without 0-RTT sent: the limits remembered for it were set+        -- all the same, and tls may not send it, e.g. with a ticket expired.+        rejected <- if isClient conn then check0RTT params else return False         setPeerParameters conn params+        -- RFC 9001 Sec 4.6.2: a client whose 0-RTT is rejected resets what+        -- it set up for it.  What it sent is resent as 1-RTT; the limits it+        -- remembered go, lower or not, for those the server gives now.+        -- Nothing has raised them before these parameters.+        when rejected $ do+            resetTxMaxData conn $ initialMaxData params+            resetTxMaxStreams conn $ initialMaxStreamsBidi params+            resetTxUniMaxStreams conn $ initialMaxStreamsUni params+            resetTxStreamLimits conn params         case statelessResetToken params of             Nothing -> return ()             Just srt -> setPeerStatelessResetToken conn srt@@ -350,6 +344,23 @@         setMaxAckDaley (connLDCC conn) $ milliToMicro $ maxAckDelay params         setTxMaxStreams conn $ initialMaxStreamsBidi params         setTxUniMaxStreams conn $ initialMaxStreamsUni params++    -- True unless 0-RTT was accepted.+    check0RTT params = do+        minfo <- TLS.contextGetInformation ctx+        let accepted = (minfo >>= TLS.infoTLS13HandshakeMode) == Just TLS.RTT0+        when accepted $ do+            storedParams <- getPeerParameters conn+            -- RFC 9221 Sec 3, for 0-RTT accepted only.+            let storedMaxDatagram = maxDatagramFrameSize storedParams+                newMaxDatagram = maxDatagramFrameSize params+            when (storedMaxDatagram > 0 && newMaxDatagram < storedMaxDatagram) $+                E.throwIO WrongDatagramSizeParameter+            -- RFC 9000 Sec 7.4.1: "If 0-RTT data is accepted by the+            -- server, the server MUST NOT reduce any limits".+            when (lowered storedParams params) $+                E.throwIO Wrong0RTTLimitParameter+        return $ not accepted      lowered old new =         or
Network/QUIC/Packet/Decode.hs view
@@ -45,7 +45,7 @@ decodePacket bs checkQuicBit = E.handle handler $ withReadBuffer bs $ \rbuf -> do     save rbuf     proFlags <- Flags <$> read8 rbuf-    if checkQuicBit && not (isQuic proFlags)+    if checkQuicBit && not (isQuic proFlags) && not isVN         then             return (PacketIB BrokenPacket len, "")         else do@@ -87,6 +87,9 @@                     return $ PacketIC crypt HandshakeLevel siz     handler BufferOverrun = return (PacketIB BrokenPacket len, "")     len = BS.length bs+    -- RFC 9000 Sec 17.2.1: all but the first bit of a Version Negotiation+    -- packet are arbitrary, so it has no QUIC bit to check.+    isVN = isLong (BS.head bs) && BS.take 4 (BS.drop 1 bs) == "\0\0\0\0"  makeShortCrypt :: ByteString -> ReadBuffer -> IO Crypt makeShortCrypt bs rbuf = do
Network/QUIC/Packet/Frame.hs view
@@ -183,8 +183,8 @@     case ftyp :: FrameType of         0x00 -> decodePadding rbuf         0x01 -> return Ping-        0x02 -> decodeAck rbuf-        -- 0x03 -> Ack with ECN Counts+        0x02 -> decodeAck rbuf False+        0x03 -> decodeAck rbuf True         0x04 -> decodeResetStream rbuf         0x05 -> decodeStopSending rbuf         0x06 -> decodeCrypto rbuf@@ -264,13 +264,16 @@     cdata <- extractByteString rbuf len     return $ CryptoF off cdata -decodeAck :: ReadBuffer -> IO Frame-decodeAck rbuf = do+-- | ACK, or ACK with ECN counts (type 0x03).  The counts are skipped:+--   no packet is sent ECN-capable, so there is nothing to validate.+decodeAck :: ReadBuffer -> Bool -> IO Frame+decodeAck rbuf ecn = do     largest <- fromIntegral <$> decodeInt' rbuf     delay <- decodeDelay rbuf     count <- fromIntegral <$> decodeInt' rbuf     range1 <- fromIntegral <$> decodeInt' rbuf     ranges <- getRanges count id+    when ecn $ replicateM_ 3 $ void $ decodeInt' rbuf     return $ Ack (AckInfo largest range1 ranges) delay   where     getRanges 0 build = return $ build []@@ -384,7 +387,8 @@  decodeDatagram :: ReadBuffer -> Bool -> IO Frame decodeDatagram rbuf hasLen = do-    len <- if hasLen-             then fromIntegral <$> decodeInt' rbuf-             else remainingSize rbuf+    len <-+        if hasLen+            then fromIntegral <$> decodeInt' rbuf+            else remainingSize rbuf     Datagram hasLen <$> extractByteString rbuf len
Network/QUIC/Receiver.hs view
@@ -316,7 +316,10 @@ checkMigration :: Connection -> CID -> SockAddr -> Bool -> IO () checkMigration conn dCID peersa cidChanged = do     mPathInfo <- findPathInfo conn peersa+    current <- peerSockAddr <$> getPathInfo conn     case mPathInfo of+        -- The previous path: the peer is still there.+        Just _ | peersa /= current -> abandonMigration conn         Just _ -> return ()         Nothing -> do             -- Claimed before the thread rather than inside it, so that a@@ -350,6 +353,10 @@     pathInfo <- newPathInfo peersa     -- assumed that this PathInfo is not stored in PeerInfo     addPathInfo conn pathInfo+    -- RFC 9000 Sec 9.3.3: a challenge on the old path too.  A peer still+    -- there answers from it and so ends a spurious migration at once.+    oldChallenge <- newPathData+    putOutput conn $ OutPathProbe oldPathInfo [PathChallenge oldChallenge]     ok <- validatePath conn pathInfo mcidinfo     -- RFC 9000 Sec 9.3.2: back to the last validated address.  The one     -- that failed is forgotten, so that a packet from it starts afresh.@@ -606,6 +613,9 @@ processFrame conn lvl (NewToken token) = do     when (isServer conn || lvl /= RTT1Level) $         closeConnection conn ProtocolViolation "NEW_TOKEN for server or in 1-RTT"+    -- RFC 9000 Sec 19.7: an empty token is a FRAME_ENCODING_ERROR.+    when (BS.null token) $+        closeConnection conn FrameEncodingError "empty NEW_TOKEN"     when (isClient conn) $ setNewToken conn token processFrame conn RTT0Level (StreamF sid off (dat : _) fin) = do     when (off == 0) $ updatePeerStreamId conn sid
Network/QUIC/Recovery.hs view
@@ -26,6 +26,7 @@     -- Persistent     findDuration,     getPTO,+    getPtoTimeAndSpace,     -- Release     releaseByRetry,     releaseOldest,@@ -45,6 +46,8 @@     fixSentPacket,     LDCC,     newLDCC,+    lostCandidates,+    SentPackets (..),     qlogSent, ) where 
Network/QUIC/Recovery/LossRecovery.hs view
@@ -136,14 +136,18 @@  releaseLostCandidates     :: LDCC -> EncryptionLevel -> (SentPacket -> Bool) -> IO (Seq SentPacket)-releaseLostCandidates ldcc@LDCC{..} lvl predicate = do-    packets <- atomically $ do-        SentPackets db <- readTVar lostCandidates-        let (pkts, db') = Seq.partition predicate db-        writeTVar lostCandidates $ SentPackets db'-        return pkts-    removePacketNumbers ldcc lvl packets-    return packets+releaseLostCandidates ldcc@LDCC{..} lvl predicate+    -- Lost candidates are 1-RTT packets; an ACK in another space+    -- shares their packet numbers but does not acknowledge them.+    | lvl /= RTT1Level = return Seq.empty+    | otherwise = do+        packets <- atomically $ do+            SentPackets db <- readTVar lostCandidates+            let (pkts, db') = Seq.partition predicate db+            writeTVar lostCandidates $ SentPackets db'+            return pkts+        removePacketNumbers ldcc lvl packets+        return packets  onPacketsAcked :: LDCC -> Seq SentPacket -> IO () onPacketsAcked ldcc@LDCC{..} ackedPackets = metricsUpdated ldcc $ do
Network/QUIC/Recovery/Timer.hs view
@@ -64,7 +64,7 @@                     return Nothing                 else do                     rtt <- readIORef recoveryRTT-                    lvl <- getEncryptionLevel ldcc+                    lvl <- antiDeadlockLevel ldcc                     let pto = backOff (calcPTO rtt $ Just lvl) (ptoCount rtt)                     ptoTime <- getFutureTimeMicrosecond pto                     return $ Just (ptoTime, lvl)@@ -73,24 +73,31 @@             let lvls                     | completed = [InitialLevel, HandshakeLevel, RTT1Level]                     | otherwise = [InitialLevel, HandshakeLevel]-            loop lvls+            loop lvls Nothing   where-    loop :: [EncryptionLevel] -> IO (Maybe (TimeMicrosecond, EncryptionLevel))-    loop [] = return Nothing-    loop (l : ls) = do+    -- RFC 9002 A.8: the space whose PTO expires first, not the first+    -- space with packets in flight.+    loop+        :: [EncryptionLevel]+        -> Maybe (TimeMicrosecond, EncryptionLevel)+        -> IO (Maybe (TimeMicrosecond, EncryptionLevel))+    loop [] r = return r+    loop (l : ls) r = do         notInFlight <- noInFlightPacket ldcc l         if notInFlight-            then loop ls+            then loop ls r             else do                 LossDetection{..} <- readIORef (lossDetection ! l)                 if timeOfLastAckElicitingPacket == timeMicrosecond0-                    then loop ls+                    then loop ls r                     else do                         rtt <- readIORef recoveryRTT                         let pto0 = backOff (calcPTO rtt $ Just l) (ptoCount rtt)                             pto = max pto0 kGranularity                             ptoTime = timeOfLastAckElicitingPacket `addMicroseconds` pto-                        return $ Just (ptoTime, l)+                        case r of+                            Just (t0, _) | t0 <= ptoTime -> loop ls r+                            _ -> loop ls $ Just (ptoTime, l)  ---------------------------------------------------------------- @@ -235,8 +242,8 @@                         -- to earn more anti-amplification credit,                         -- a Handshake packet proves address ownership.                         validated <- peerCompletedAddressValidation ldcc-                        when validated $ qlogDebug ldcc $ Debug "onLossDetectionTimeout: RTT1"-                        lvl' <- getEncryptionLevel ldcc -- fixme+                        when validated $ qlogDebug ldcc $ Debug "onLossDetectionTimeout: validated"+                        lvl' <- antiDeadlockLevel ldcc                         sendPing ldcc lvl'                  metricsUpdated ldcc $
Network/QUIC/Recovery/Utils.hs view
@@ -6,6 +6,7 @@     mergeLostCandidates,     mergeLostCandidatesAndClear,     peerCompletedAddressValidation,+    antiDeadlockLevel,     countAckEli,     inFlightBytes,     inCongestionRecovery,@@ -77,9 +78,19 @@ -- For servers: assume clients validate the server's address implicitly. peerCompletedAddressValidation ldcc     | isServer ldcc = return True--- For clients: servers complete address validation when a protected--- packet is received.-peerCompletedAddressValidation ldcc = isConnectionEstablished ldcc+-- For clients: a Handshake ACK or the confirmed handshake (RFC 9002 A.8).+peerCompletedAddressValidation ldcc@LDCC{..} = do+    confirmed <- isConnectionEstablished ldcc+    if confirmed+        then return True+        else (>= 0) . largestAckedPacket <$> readIORef (lossDetection ! HandshakeLevel)++-- | The level of an anti-deadlock probe: Handshake if its keys are+-- available, Initial otherwise (RFC 9002 A.8).+antiDeadlockLevel :: LDCC -> IO EncryptionLevel+antiDeadlockLevel ldcc = do+    lvl <- getEncryptionLevel ldcc+    return $ if lvl >= HandshakeLevel then HandshakeLevel else InitialLevel  ---------------------------------------------------------------- 
Network/QUIC/Sender.hs view
@@ -11,6 +11,7 @@ import qualified Control.Exception as E import qualified Data.ByteString as BS import Foreign.Ptr (plusPtr)+import qualified Network.Socket as NS  import Network.QUIC.Config import Network.QUIC.Connection@@ -39,9 +40,18 @@ ----------------------------------------------------------------  sendPacket :: Connection -> [SentPacket] -> IO ()-sendPacket _ [] = return ()-sendPacket conn spkts0 = getMaxPacketSize conn >>= go+sendPacket conn = sendPacketTo conn Nothing++-- | Sending to the path in use, or to the one given.+sendPacketTo :: Connection -> Maybe PathInfo -> [SentPacket] -> IO ()+sendPacketTo _ _ [] = return ()+sendPacketTo conn mdst spkts0 = getMaxPacketSize conn >>= go   where+    send pathInfo buf bytes = case mdst of+        Nothing -> connSend conn buf bytes+        Just _ -> do+            (s, _) <- readIORef $ connSocket conn+            void $ NS.sendBufTo s buf bytes $ peerSockAddr pathInfo     SizedBuffer buf0 bufsiz0 = encryptRes conn     ldcc = connLDCC conn     -- RFC 9002 section 7: "packets containing only ACK frames do not count@@ -98,14 +108,14 @@                 when (isJust mx) $ qlogDebug conn $ Debug "probe new"                 (sentPackets, leftsiz) <- buildPackets buf0 bufsiz0 maxSiz spkts0 id                 let bytes = bufsiz0 - leftsiz-                pathInfo <- getPathInfo conn+                pathInfo <- maybe (getPathInfo conn) return mdst                 when (isServer conn) $                     waitAntiAmplificationFree conn pathInfo bytes                 -- If the secret of this level (e.g. HandshakeLevel)                 -- is already dropped, bytes is 0.                 when (bytes > 0) $ do                     now <- getTimeMicrosecond-                    connSend conn buf0 bytes+                    send pathInfo buf0 bytes                     addTxBytes conn bytes                     addPathTxBytes pathInfo bytes                     forM_ sentPackets $ \sentPacket0 -> do@@ -414,6 +424,8 @@     sendCryptoFragments conn lcs     when (any (\(l, _) -> l == HandshakeLevel) lcs) $         discardClientInitialPacketNumberSpace conn+sendOutput conn (OutPathProbe pathInfo frames) =+    construct conn RTT1Level frames False >>= sendPacketTo conn (Just pathInfo) sendOutput conn (OutRetrans (PlainPacket hdr0 plain0)) = do     frames <- adjustForRetransmit conn $ plainFrames plain0     let lvl = levelFromHeader hdr0@@ -449,6 +461,9 @@ -- frames are sent just once."  The peer sends a new PATH_CHALLENGE if -- it needs one.  Resent, a PATH_RESPONSE flood would never leave us. adjustForRetransmit conn (PathResponse{} : xs) = adjustForRetransmit conn xs+-- RFC 9000 Sec 13.3: a new payload each time, so never resent.  Resent, it+-- also went to the path in use, whichever path it was for.+adjustForRetransmit conn (PathChallenge{} : xs) = adjustForRetransmit conn xs adjustForRetransmit conn (x : xs) = do     rs <- adjustForRetransmit conn xs     return (x : rs)@@ -506,13 +521,13 @@  -- | 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-    ]+streamFrameMaxOverhead =+    sum+        [ 1 -- type+        , 8 -- stream ID+        , 8 -- offset+        , 2 -- length+        ]  packFin :: Connection -> Stream -> Bool -> IO Bool packFin _ _ True = return True
Network/QUIC/Stream.hs view
@@ -39,6 +39,7 @@     readStreamFlowTx,     addTxStreamData,     setTxMaxStreamData,+    resetTxMaxStreamData,     getRxMaxStreamData,     updateStreamFlowRx, @@ -53,6 +54,7 @@     StreamTable,     emptyStreamTable,     lookupStream,+    tableStreams,     insertStream,     deleteStream,     insertCryptoStreams,
Network/QUIC/Stream/Misc.hs view
@@ -27,6 +27,7 @@     readStreamFlowTx,     addTxStreamData,     setTxMaxStreamData,+    resetTxMaxStreamData,     --     getRxMaxStreamData,     addRxStreamData,@@ -127,6 +128,12 @@     set flow         | txfLimit flow < n = flow{txfLimit = n}         | otherwise = flow++-- | Setting the limit even lower: only for 0-RTT rejected, when the+--   remembered limit goes (RFC 9001 Sec 4.6.2).+resetTxMaxStreamData :: Stream -> Int -> IO ()+resetTxMaxStreamData Stream{..} n =+    atomically $ modifyTVar' streamFlowTx $ \flow -> flow{txfLimit = n}  ---------------------------------------------------------------- 
Network/QUIC/Stream/Table.hs view
@@ -2,6 +2,7 @@     StreamTable,     emptyStreamTable,     lookupStream,+    tableStreams,     insertStream,     deleteStream,     insertCryptoStreams,@@ -27,6 +28,10 @@  lookupStream :: StreamId -> StreamTable -> Maybe Stream lookupStream sid (StreamTable tbl) = Map.lookup sid tbl++-- | The streams in the table, the crypto streams included.+tableStreams :: StreamTable -> [Stream]+tableStreams (StreamTable tbl) = Map.elems tbl  insertStream :: StreamId -> Stream -> StreamTable -> StreamTable insertStream sid strm (StreamTable tbl) = StreamTable $ Map.insert sid strm tbl
Network/QUIC/TLS.hs view
@@ -75,6 +75,38 @@             { debugKeyLogger = ccKeyLog             } +-- | The parameters a server accepting 0-RTT must not lower (RFC 9000+--   Sec 7.4.1, and RFC 9221 Sec 3 for max_datagram_frame_size).+zeroRTTLimits :: Parameters -> Parameters+zeroRTTLimits p =+    baseParameters+        { activeConnectionIdLimit = activeConnectionIdLimit p+        , initialMaxData = initialMaxData p+        , initialMaxStreamDataBidiLocal = initialMaxStreamDataBidiLocal p+        , initialMaxStreamDataBidiRemote = initialMaxStreamDataBidiRemote p+        , initialMaxStreamDataUni = initialMaxStreamDataUni p+        , initialMaxStreamsBidi = initialMaxStreamsBidi p+        , initialMaxStreamsUni = initialMaxStreamsUni p+        , maxDatagramFrameSize = maxDatagramFrameSize p+        }++-- | Whether the parameters now keep to those remembered.+keeps0RTTLimits :: Parameters -> Parameters -> Bool+keeps0RTTLimits remembered now =+    and+        [ f now >= f remembered+        | f <-+            [ activeConnectionIdLimit+            , initialMaxData+            , initialMaxStreamDataBidiLocal+            , initialMaxStreamDataBidiRemote+            , initialMaxStreamDataUni+            , initialMaxStreamsBidi+            , initialMaxStreamsUni+            , maxDatagramFrameSize+            ]+        ]+ parametersToExtensionRaw :: Version -> Parameters -> [ExtensionRaw] parametersToExtensionRaw ver params = [ExtensionRaw tpId eParams]   where@@ -105,11 +137,27 @@     sshared =         defaultShared             { sharedCredentials = scCredentials-            , sharedSessionManager = scSessionManager+            , sharedSessionManager = mgr             }+    -- RFC 9000 Sec 7.4.1: what 0-RTT is held to is kept with the session,+    -- and 0-RTT is refused if the server now allows less.+    mgr =+        scSessionManager+            { sessionEstablish = \sid sdata -> do+                remembered <- zeroRTTLimits <$> getParams+                sessionEstablish scSessionManager sid $+                    sdata{sessionAppData = Just $ encodeParameters remembered}+            }+    accept0RTT sdata = do+        ok <- onAccept0RTT scTlsHooks sdata+        now <- getParams+        return $ case sessionAppData sdata >>= decodeParameters of+            Just remembered -> ok && keeps0RTTLimits remembered now+            Nothing -> False     hook =         scTlsHooks-            { onALPNClientSuggest = case scALPN of+            { onAccept0RTT = accept0RTT+            , onALPNClientSuggest = case scALPN of                 Nothing -> onALPNClientSuggest scTlsHooks                 Just io -> Just $ io ver             , onEncryptedExtensionsCreating = \exts0 -> do
quic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               quic-version:            0.3.20+version:            0.3.21 license:            BSD3 license-file:       LICENSE maintainer:         kazu@iij.ad.jp@@ -127,7 +127,7 @@     build-depends:         base >=4.9 && <5,         array >=0.5 && <0.6,-        async,+        async >=2.2,         base16-bytestring >=1.0 && <1.1,         bytestring >=0.10,         containers,@@ -139,7 +139,7 @@         crypton-x509-validation >=1.9.0 && <1.11,         fast-logger >=3.2.8 && <3.3,         filepath,-        iproute >=1.7.12 && <1.8,+        iproute >=1.7.12 && <1.9,         network >=3.2.12,         network-byte-order >=0.1.7 && <0.2,         network-control >=0.1.5 && <0.2,@@ -148,8 +148,8 @@         serialise,         stm >=2.5 && <2.6,         time-manager >=0.5 && <0.6,-        tls >=2.4.0 && <2.6,-        unix-time >=0.4.12 && <0.6+        tls >=2.5.1 && <2.6,+        unix-time >=0.4.12 && <0.7      if os(linux)         cc-options: -DOS_Linux
test/DatagramSpec.hs view
@@ -49,10 +49,18 @@         -- "If a client stores the value of the max_datagram_frame_size transport parameter with their 0-RTT state, they MUST validate that the new value of the max_datagram_frame_size transport parameter sent by the server in the handshake is greater than or equal to the stored value; if not, the client MUST terminate the connection with error PROTOCOL_VIOLATION."         it "Client MUST reject 0-RTT downgrade server parameters" $ do             let scInit = sc0 { scMaxDatagramFrameSize = 1024 }-                scDowngrade = sc0 { scMaxDatagramFrameSize = 500 }+                scDowngrade = lowerSentDatagramSize sc0 { scMaxDatagramFrameSize = 1024 }                 cc0RTT = testClientConfig { ccMaxDatagramFrameSize = 1024 }             testDatagram0RTTDowngrade cc0RTT scInit scDowngrade +        -- The check above is for 0-RTT accepted only.  Rejected, the+        -- client takes the value the server gives now (RFC 9001 Sec 4.6.2).+        it "Client takes a lower max_datagram_frame_size when 0-RTT is rejected" $ do+            let scInit = sc0 { scMaxDatagramFrameSize = 1024 }+                scLower = sc0 { scMaxDatagramFrameSize = 500, scUse0RTT = False }+                cc0RTT = testClientConfig { ccMaxDatagramFrameSize = 1024 }+            testDatagram0RTTRejected cc0RTT scInit scLower+         -- "An endpoint MUST NOT send DATAGRAM frames until it has received the max_datagram_frame_size transport parameter with a non-zero value"         it "Endpoint MUST NOT send DATAGRAM if not negotiated" $ do             let scDtgm = sc0 { scMaxDatagramFrameSize = 0 }@@ -88,7 +96,7 @@         -- "When servers decide to accept 0-RTT data, they MUST send a max_datagram_frame_size transport parameter greater than or equal to the value they sent to the client..."         it "Server MUST NOT downgrade max_datagram_frame_size on 0-RTT" $ do             let scInit = sc0 { scMaxDatagramFrameSize = 1024 }-                scDowngrade = sc0 { scMaxDatagramFrameSize = 500 }+                scDowngrade = lowerSentDatagramSize sc0 { scMaxDatagramFrameSize = 1024 }                 cc0RTT = testClientConfig { ccMaxDatagramFrameSize = 1024 }              mvar <- newEmptyMVar@@ -280,6 +288,33 @@         when (handshakeMode info == RTT0) $ do             recv <- recvDatagram conn             recv `shouldBe` "0rtt_client_data"++-- | A server that keeps max_datagram_frame_size and so accepts 0-RTT, but+--   sends a lower one: what the client is to catch (RFC 9221 Sec 3).+lowerSentDatagramSize :: ServerConfig -> ServerConfig+lowerSentDatagramSize sc =+    sc { scHooks = (scHooks sc) { onTransportParametersCreated = \p -> p { maxDatagramFrameSize = 500 } } }++testDatagram0RTTRejected :: ClientConfig -> ServerConfig -> ServerConfig -> IO ()+testDatagram0RTTRejected cc1 scInit scLower = do+    mvar <- newEmptyMVar+    smgr <- newSessionManager+    let scInit' = scInit { scSessionManager = smgr, scUse0RTT = True, scHooks = (scHooks scInit) { onServerReady = putMVar mvar () } }+    res <- E.bracket (forkIO $ S.run scInit' (\c -> waitEstablished c >> threadDelay 100000)) killThread $ \_ -> do+        takeMVar mvar+        C.run cc1 $ \conn -> do+            waitEstablished conn+            threadDelay 50000+            getResumptionInfo conn+    let cc2 = cc1 { ccResumption = res, ccUse0RTT = True }+    mvar2 <- newEmptyMVar+    let scLower' = scLower { scSessionManager = smgr, scHooks = (scHooks scLower) { onServerReady = putMVar mvar2 () } }+    E.bracket (forkIO $ E.handle (\(E.SomeException _) -> return ()) $ S.run scLower' (\c -> waitEstablished c >> threadDelay 2000000)) killThread $ \_ -> do+        takeMVar mvar2+        C.run cc2 (\conn -> do+            waitEstablished conn+            sendDatagram conn (BS.replicate 400 0x41)+            handshakeMode <$> getConnectionInfo conn) `shouldReturn` PreSharedKey  testDatagram0RTTDowngrade :: ClientConfig -> ServerConfig -> ServerConfig -> IO () testDatagram0RTTDowngrade cc1 scInit scDowngrade = do
test/FrameSpec.hs view
@@ -1,6 +1,7 @@ module FrameSpec where  import qualified Control.Exception as E+import qualified Data.ByteString as BS import Data.Word import Foreign.Marshal.Alloc import Foreign.Marshal.Utils (fillBytes)@@ -12,6 +13,14 @@  spec :: Spec spec = do+    describe "ACK_ECN" $ do+        it "is decoded as ACK with its ECN counts skipped" $ do+            let ack = Ack (AckInfo 5 2 [(1, 0)]) noDelay+            bs <- encodeFrames [ack, Ping]+            -- Type 0x03, then the three ECN counts after the ranges.+            let (body, ping) = BS.splitAt (BS.length bs - 2) $ BS.drop 1 bs+                ecn = BS.concat [BS.singleton 0x03, body, BS.pack [1, 2, 0x40, 3], ping]+            decodeFramesBS ecn `shouldReturn` Just [ack, Ping]     describe "countZero" $ do         it "counts 0 correctly" $ do             let siz = 512
test/HandshakeSpec.hs view
@@ -101,9 +101,33 @@         it "keeps a resumed connection to the limits the server gives now" $ do             testLoweredLimits testClientConfig sc0 waitS                 `shouldReturn` (PreSharedKey, 3)+        it "drops the remembered limits when 0-RTT is rejected" $ do+            testRejected0RTTLimits testClientConfig sc0 waitS+                `shouldReturn` (PreSharedKey, 3)+        it "drops the remembered limits when no 0-RTT is sent" $ do+            testExpired0RTTLimits testClientConfig sc0 waitS+                `shouldReturn` (FullHandshake, 3)+        it "refuses 0-RTT when the server allows less than it did" $ do+            let lower sc = sc{scParameters = (scParameters sc){initialMaxStreamsBidi = 50}}+            testLowered0RTT testClientConfig sc0{scUse0RTT = True} waitS lower+                `shouldReturn` PreSharedKey         it "refuses 0-RTT accepted with the limits lowered" $ do-            testLowered0RTT testClientConfig sc0{scUse0RTT = True} waitS+            -- Lowered in what is sent only, so that the server accepts.+            let lower sc =+                    sc+                        { scHooks =+                            (scHooks sc)+                                { onTransportParametersCreated = \p -> p{initialMaxStreamsBidi = 50}+                                }+                        }+            testLowered0RTT testClientConfig sc0{scUse0RTT = True} waitS lower                 `shouldThrow` protocolViolation+        it "MUST send FRAME_ENCODING_ERROR on an empty NEW_TOKEN [Transport 19.7]" $ do+            let sc = sc0{scHooks = (scHooks sc0){onPlainCreated = emptyNewToken}}+            E.bracket (forkIO $ S.run sc $ \_ -> threadDelay 2000000) killThread $ \_ -> do+                waitS+                C.run testClientConfig (\_ -> threadDelay 1000000)+                    `shouldThrow` frameEncodingErrorSent         it "sends 0-RTT data without waiting for the handshake" $ do             let sc = sc0{scUse0RTT = True}             test0RTTSendsEarly sc waitS@@ -128,6 +152,13 @@                         te == cryptoError TLS.HandshakeFailure                     | otherwise = False             testHandshake3 cc1 cc2 sc waitS handshakeFailure+        it "reconnects with the version a Version Negotiation offers" $ do+            let cc = testClientConfig{ccVersion = Version2, ccVersions = [Version2, Version1]}+                sc = sc0{scVersions = [Version1]}+            E.bracket (forkIO $ S.run sc $ \_ -> threadDelay 2000000) killThread $ \_ -> do+                waitS+                C.run cc (\conn -> waitEstablished conn >> getVersion conn)+                    `shouldReturn` Version1         it "fails with no version in common" $ do             let cc1 = testClientConfig{ccVersion = Version1, ccVersions = [Version1]}                 cc2 = testClientConfig@@ -412,12 +443,70 @@     sc2 = sc{scParameters = (scParameters sc){initialMaxStreamsBidi = 3}}     serv conn = forever $ void $ acceptStream conn --- | As above, but with 0-RTT, which the server accepts with fifty streams---   where it gave a hundred before: RFC 9000 Sec 7.4.1 says it MUST NOT.---   The client opens one stream, within either, so that it is the client---   that notices.-testLowered0RTT :: ClientConfig -> ServerConfig -> IO () -> IO ()-testLowered0RTT cc1 sc waitS = do+-- | As above, but the client sends 0-RTT, which the server rejects.  RFC+--   9001 Sec 4.6.2: the client then resets what it set up for 0-RTT.  It+--   kept the hundred streams it remembered and the server closed it with+--   STREAM_LIMIT_ERROR.+testRejected0RTTLimits+    :: ClientConfig -> ServerConfig -> IO () -> IO (HandshakeMode13, Int)+testRejected0RTTLimits cc1 sc waitS = do+    res <- resumeFrom cc1 sc{scUse0RTT = True} waitS+    threadDelay 100000+    E.bracket (forkIO $ S.run sc2 serv) killThread $ \_ -> do+        waitS+        C.run cc1{ccResumption = res, ccUse0RTT = True} $ \conn -> do+            waitEstablished conn+            opened <- newIORef (0 :: Int)+            _ <- Timeout.timeout 1500000 $ forever $ do+                s <- stream conn+                sendStream s "x"+                atomicModifyIORef' opened $ \x -> (x + 1, ())+            (,) <$> (handshakeMode <$> getConnectionInfo conn) <*> readIORef opened+  where+    sc2 =+        sc+            { scUse0RTT = False+            , scParameters = (scParameters sc){initialMaxStreamsBidi = 3}+            }+    serv conn = forever $ void $ acceptStream conn++-- | As above, but the ticket has expired, so tls sends no 0-RTT while the+--   client has set the limits it remembered for 0-RTT all the same.+testExpired0RTTLimits+    :: ClientConfig -> ServerConfig -> IO () -> IO (HandshakeMode13, Int)+testExpired0RTTLimits cc1 sc waitS = do+    res <- resumeFrom cc1 sc{scUse0RTT = True, scTicketLifetime = 1} waitS+    threadDelay 2000000+    E.bracket (forkIO $ S.run sc2 serv) killThread $ \_ -> do+        waitS+        C.run cc1{ccResumption = res, ccUse0RTT = True} $ \conn -> do+            waitEstablished conn+            opened <- newIORef (0 :: Int)+            _ <- Timeout.timeout 1500000 $ forever $ do+                s <- stream conn+                sendStream s "x"+                atomicModifyIORef' opened $ \x -> (x + 1, ())+            (,) <$> (handshakeMode <$> getConnectionInfo conn) <*> readIORef opened+  where+    sc2 =+        sc+            { scUse0RTT = True+            , scParameters = (scParameters sc){initialMaxStreamsBidi = 3}+            }+    serv conn = forever $ void $ acceptStream conn++-- | As above, but with 0-RTT and fifty streams where the server gave a+--   hundred before, lowered as 'lower' says.  RFC 9000 Sec 7.4.1: a+--   server that lowers them MUST NOT accept 0-RTT, and a client MAY close+--   on one that does.  The client opens one stream, within either.+--   Answers how the connection went.+testLowered0RTT+    :: ClientConfig+    -> ServerConfig+    -> IO ()+    -> (ServerConfig -> ServerConfig)+    -> IO HandshakeMode13+testLowered0RTT cc1 sc waitS lower = do     res <- resumeFrom cc1 sc waitS     threadDelay 100000     E.bracket (forkIO $ S.run sc2 serv) killThread $ \_ -> do@@ -426,9 +515,21 @@             s <- stream conn             sendStream s "x"             threadDelay 1000000+            handshakeMode <$> getConnectionInfo conn   where-    sc2 = sc{scParameters = (scParameters sc){initialMaxStreamsBidi = 50}}+    -- The hundred streams 'resumeFrom' allows, lowered.+    sc2 = lower sc{scParameters = (scParameters sc){initialMaxStreamsBidi = 100}}     serv conn = forever $ void $ acceptStream conn++emptyNewToken :: EncryptionLevel -> Plain -> Plain+emptyNewToken _ plain = plain{plainFrames = map empty $ plainFrames plain}+  where+    empty (NewToken _) = NewToken ""+    empty f = f++frameEncodingErrorSent :: QUICException -> Bool+frameEncodingErrorSent (TransportErrorIsSent FrameEncodingError _) = True+frameEncodingErrorSent _ = False  protocolViolation :: QUICException -> Bool protocolViolation (TransportErrorIsSent ProtocolViolation _) = True
test/IOSpec.hs view
@@ -17,6 +17,8 @@ import GHC.Conc (getUncaughtExceptionHandler, setUncaughtExceptionHandler) import qualified Network.Socket as NS import qualified Network.Socket.ByteString as NSB+import System.Directory (listDirectory)+import System.FilePath ((</>)) import qualified System.Timeout as Timeout import Test.Hspec @@ -59,6 +61,9 @@             testInjected cc sc waitS $ \dcid scid ->                 encodeRetryPacket $                     RetryPacket Version2 dcid (toCID "forged") "token" (Left scid)+    describe "a stateless reset to the client" $+        it "ends the connection with ConnectionIsReset" $+            testStatelessResetReceived cc sc waitS     describe "max_udp_payload_size" $ do         it "bounds the datagrams each side sends" $ do             let small p = p{maxUdpPayloadSize = 1200}@@ -180,7 +185,14 @@                         ]         it "follows a client that rebinds" $ testRebindPath cc sc waitS         it "goes back when the new one is not validated" $-            testSpuriousMigration cc sc waitS+            testSpuriousMigration cc sc waitS 1500000+        -- RFC 9000 Sec 9.3.3.  The new path answers nothing, so a+        -- PATH_CHALLENGE in the client's qlog came on the old one.+        it "challenges the old path too" $+            withTempDir "quic-old-path-qlog" $ \dir -> do+                testSpuriousMigration cc{ccQLog = Just dir} sc waitS 1500000+                logs <- listDirectory dir >>= mapM (BS.readFile . (dir </>))+                any ("path_challenge" `BS.isInfixOf`) logs `shouldBe` True     describe "a stateless reset" $ do         it "is smaller than the packet it answers" $ do             withAsync (run sc $ \_ -> return ()) $ \_ -> do@@ -520,12 +532,12 @@             _ <- recvStream strm 1             closeStream strm         let departed = IntMap.size <$> readTVarIO (departedStreams conn)-            wait :: Int -> IO Int-            wait 0 = departed-            wait k = do+            settle :: Int -> IO Int+            settle 0 = departed+            settle k = do                 d <- departed-                if d == 0 then return 0 else threadDelay 10000 >> wait (k - 1)-        wait 300 >>= putMVar left+                if d == 0 then return 0 else threadDelay 10000 >> settle (k - 1)+        settle 300 >>= putMVar left  -- | As above, but the client does not hear the STOP_SENDING.  The streams --   it leaves unfinished are not given back to its limit, so the server@@ -590,12 +602,12 @@             _ <- recvStream strm 1             closeStream strm         let departed = IntMap.size <$> readTVarIO (departedStreams conn)-            wait :: Int -> IO Int-            wait 0 = departed-            wait k = do+            settle :: Int -> IO Int+            settle 0 = departed+            settle k = do                 d <- departed-                if d == 0 then return 0 else threadDelay 10000 >> wait (k - 1)-        wait 300 >>= putMVar left+                if d == 0 then return 0 else threadDelay 10000 >> settle (k - 1)+        settle 300 >>= putMVar left  -- | The server closes the first unidirectional stream and keeps the rest. -- The client may then have initial_max_streams_uni plus one.  The limit was@@ -756,6 +768,32 @@         assertEndOfStream strm         putMVar mvar () +-- | A stateless reset with the token the server gave.  The client's reader+--   threw 'ConnectionIsReset' to the connection's main thread; it now asks+--   the protocol threads to end with it.+testStatelessResetReceived :: C.ClientConfig -> ServerConfig -> IO () -> IO ()+testStatelessResetReceived cc sc waitS = do+    trigger <- newEmptyMVar+    tokenVar <- newEmptyMVar+    let mk _ _ = readMVar tokenVar >>= makeStatelessReset 64+    withPipe (InjectToClient trigger mk) $+        withAsync (run sc $ \_ -> threadDelay 5_000_000) $ \_ -> do+            waitS+            noUncaught $+                C.run cc (go trigger tokenVar) `shouldThrow` isReset+  where+    go trigger tokenVar conn = do+        waitEstablished conn+        mtoken <- statelessResetToken <$> getPeerParameters conn+        case mtoken of+            Nothing -> expectationFailure "no stateless_reset_token"+            Just token -> do+                putMVar tokenVar token+                putMVar trigger ()+                threadDelay 3_000_000+    isReset ConnectionIsReset = True+    isReset _ = False+ -- | The server asks the client to stop sending on @n@ streams at once. testStopMany :: C.ClientConfig -> ServerConfig -> IO () -> Int -> IO () testStopMany cc sc waitS n = do@@ -1420,7 +1458,7 @@             C.run cc $ \conn -> do                 strm <- stream conn                 echo strm "ping1" `shouldReturn` Just "ping1"-                before <- takeMVar paths+                original <- takeMVar paths                 ver <- getVersion conn                 dcid <- getPeerCID conn                 addr : _ <-@@ -1432,7 +1470,7 @@                     void $ NSB.sendTo s (forge ver dcid) $ NS.addrAddress addr                 threadDelay 300000                 echo strm "ping2" `shouldReturn` Just "ping2"-                takeMVar paths `shouldReturn` before+                takeMVar paths `shouldReturn` original  -- | A client that moves to a new port is still followed, now that the move --   is taken from decrypted packets.@@ -1446,25 +1484,27 @@         C.run cc' $ \conn -> do             strm <- stream conn             echo strm "ping1" `shouldReturn` Just "ping1"-            before <- takeMVar paths+            original <- takeMVar paths             void $ controlConnection conn NATRebinding             -- The first echo may still go out on the old path, while the             -- new one is being validated.-            let wait (0 :: Int) = expectationFailure "the server never moved"-                wait n = do+            let untilMoved (0 :: Int) = expectationFailure "the server never moved"+                untilMoved n = do                     echo strm "ping" `shouldReturn` Just "ping"                     p <- takeMVar paths-                    when (p == before) $ do+                    when (p == original) $ do                         threadDelay 100000-                        wait (n - 1)-            wait 20+                        untilMoved (n - 1)+            untilMoved 20  -- | RFC 9000 Sec 9.3.2: a packet of the client's, forwarded from another --   address, moves the server there, and the validation of that address --   fails.  The server waited for the PATH_RESPONSE for ever and went on---   sending to the address that never answered.-testSpuriousMigration :: C.ClientConfig -> ServerConfig -> IO () -> IO ()-testSpuriousMigration cc sc waitS = do+--   sending to the address that never answered.  Then it waited for the+--   validation to time out (about 3 s), though the client's next packets+--   came from the old address; they now send it back at once.+testSpuriousMigration :: C.ClientConfig -> ServerConfig -> IO () -> Int -> IO ()+testSpuriousMigration cc sc waitS within = do     paths <- newEmptyMVar     trigger <- newEmptyMVar     withPipe (ForwardAside trigger) $@@ -1473,13 +1513,13 @@             C.run cc $ \conn -> do                 strm <- stream conn                 echo strm "ping1" `shouldReturn` Just "ping1"-                before <- takeMVar paths+                original <- takeMVar paths                 putMVar trigger ()                 -- Answered on the spurious path, and so not heard until-                -- the server has given up on it.+                -- the server is back on the old one.                 sendStream strm "ping2"-                Timeout.timeout 8000000 (recvStream strm 1024)+                Timeout.timeout within (recvStream strm 1024)                     `shouldReturn` Just "ping2"                 _ <- takeMVar paths                 echo strm "ping3" `shouldReturn` Just "ping3"-                takeMVar paths `shouldReturn` before+                takeMVar paths `shouldReturn` original
test/MigrationSpec.hs view
@@ -1,10 +1,11 @@ module MigrationSpec where  import Control.Concurrent-import Control.Concurrent.Async (mapConcurrently)+import Control.Concurrent.Async (mapConcurrently, wait, withAsync) import Control.Concurrent.STM import Control.Monad (void) import Network.Socket (connect, getSocketName, withFdSocket)+import qualified System.Timeout as T import Test.Hspec  import Network.QUIC.Internal@@ -22,6 +23,25 @@     -- -- which asks no spoofing of it -- is therefore unknown on every     -- packet it sends.  One of those packets is a migration; the rest are     -- nothing.  See #167.+    -- RFC 9000 Sec 8.2.1: a challenge lost was not sent again, so one+    -- loss failed the validation.  It is sent again, each time with a new+    -- payload, and a late answer to an earlier one still counts.+    describe "validatePath" $+        it "sends the challenge again with a new payload until answered" $ do+            (_, serverConn) <- makeConnections serverConf Version1+            pathInfo <- getPathInfo serverConn >>= newPathInfo . peerSockAddr+            withAsync (validatePath serverConn pathInfo Nothing) $ \validation -> do+                let challenge = do+                        out <- T.timeout 3000000 $ atomically $ takeOutputSTM serverConn+                        case out of+                            Just (OutPathProbe _ [PathChallenge pdat]) -> return pdat+                            _ -> fail "no challenge"+                p1 <- challenge+                p2 <- challenge+                p2 `shouldNotBe` p1+                checkResponse serverConn p1+                wait validation `shouldReturn` True+     describe "claimMigration" $ do         it "gives the migration to one caller and refuses the rest" $ do             (_, serverConn) <- makeConnections serverConf Version1
test/PacketSpec.hs view
@@ -12,14 +12,21 @@ import Foreign.Ptr (plusPtr) import Test.Hspec -import Network.QUIC.Internal import Control.Concurrent.STM+import Network.QUIC.Internal  import Config  spec :: Spec spec = do     serverConf <- runIO makeTestServerConfig+    describe "Version Negotiation" $ do+        it "is decoded without its QUIC bit even when the bit is checked" $ do+            let vn = VersionNegotiationPacket (toCID "dcid") (toCID "scid") [Version1]+            bs0 <- encodeVersionNegotiationPacket vn+            let bs = BS.cons (BS.head bs0 .&. 0xbf) $ BS.tail bs0+            pkts <- decodePackets bs True+            [p | PacketIV p <- pkts] `shouldBe` [vn]     describe "test vector" $ do         it "describes example of Client Initial version 1" $ do             conns <- makeConnections serverConf Version1
test/RecoverySpec.hs view
@@ -2,6 +2,7 @@  module RecoverySpec where +import Control.Concurrent.STM import Data.ByteString.Short () import qualified Data.Sequence as Seq import Data.UnixTime@@ -11,6 +12,45 @@  spec :: Spec spec = do+    describe "lost candidates" $ do+        it "are released only by an ACK in the 1-RTT space" $ do+            cs <- newConnState Client+            ldcc <- newLDCC cs (\_ -> return ()) (\_ -> return ())+            let lost = (sp 1 True $ UnixTime 0 0){spEncryptionLevel = RTT1Level}+                candidates = (\(SentPackets db) -> db) <$> readTVarIO (lostCandidates ldcc)+            atomically $ writeTVar (lostCandidates ldcc) $ SentPackets $ Seq.singleton lost+            -- A Handshake ACK for the same packet number is not for it.+            onAckReceived ldcc HandshakeLevel (AckInfo 1 0 []) 0+            candidates `shouldReturn` Seq.singleton lost+            onAckReceived ldcc RTT1Level (AckInfo 1 0 []) 0+            candidates `shouldReturn` Seq.empty+    describe "PTO" $ do+        it "is armed for the space that expires first" $ do+            cs <- newConnState Server+            ldcc <- newLDCC cs (\_ -> return ()) (\_ -> return ())+            -- Far in the future so that the timer does not fire.+            now <- getTimeMicrosecond+            let at lvl pn s =+                    (sp pn True $ now `addMicroseconds` Microseconds (s * 1000000))+                        { spEncryptionLevel = lvl+                        , spSentBytes = 1200+                        , spPlainPacket = PlainPacket (Short $ toCID "") (Plain (Flags 0) pn [Ping] 0)+                        }+            onPacketSent ldcc $ at HandshakeLevel 0 100+            onPacketSent ldcc $ at InitialLevel 1 200+            fmap snd <$> getPtoTimeAndSpace ldcc `shouldReturn` Just HandshakeLevel+        it "sends an anti-deadlock probe at the Handshake level once it has the keys" $ do+            cs <- newConnState Client+            ldcc <- newLDCC cs (\_ -> return ()) (\_ -> return ())+            fmap snd <$> getPtoTimeAndSpace ldcc `shouldReturn` Just InitialLevel+            atomically $ writeTVar (encryptionLevel cs) RTT1Level+            fmap snd <$> getPtoTimeAndSpace ldcc `shouldReturn` Just HandshakeLevel+        it "needs no anti-deadlock probe once a Handshake ACK arrives" $ do+            cs <- newConnState Client+            ldcc <- newLDCC cs (\_ -> return ()) (\_ -> return ())+            atomically $ writeTVar (encryptionLevel cs) RTT1Level+            onAckReceived ldcc HandshakeLevel (AckInfo 0 0 []) 0+            getPtoTimeAndSpace ldcc `shouldReturn` Nothing     describe "persistent congestion" $ do         it "does not find a pair" $ do             findDuration (Seq.fromList [sp0]) 0 `shouldBe` Nothing