packages feed

quic 0.3.1 → 0.3.2

raw patch · 27 files changed

+489/−13 lines, 27 filesdep ~stmdep ~unix-time

Dependency ranges changed: stm, unix-time

Files

ChangeLog.md view
@@ -1,5 +1,10 @@ # ChangeLog +## 0.3.2++* Support Unreliable Datagrams extension (RFC9221)+  [#92](https://github.com/kazu-yamamoto/quic/pull/92)+ ## 0.3.1  * Using tls v2.4.0.
Network/QUIC.hs view
@@ -34,6 +34,10 @@     recvStream,     sendStream,     sendStreamMany,+    sendDatagram,+    recvDatagram,+    recvDatagramMany,+    recvDatagramSTM,      -- * Information     ConnectionInfo,
Network/QUIC/Client.hs view
@@ -12,6 +12,7 @@     ccPortName,     ccALPN,     ccUse0RTT,+    ccMaxDatagramFrameSize,     ccResumption,     ccCiphers,     ccGroups,
Network/QUIC/Client/Run.hs view
@@ -10,6 +10,7 @@ ) where  import Control.Concurrent+import Control.Concurrent.STM import Control.Concurrent.Async import qualified Control.Exception as E import Foreign.C.Types@@ -139,6 +140,7 @@     let myAuthCIDs = defaultAuthCIDs{initSrcCID = Just myCID}         peerAuthCIDs = defaultAuthCIDs{initSrcCID = Just peerCID, origDstCID = Just peerCID}     genSRT <- makeGenStatelessReset+    connRecvDatagramQ <- newTQueueIO     conn <-         clientConnection             conf@@ -151,6 +153,7 @@             sref             piref             q+            connRecvDatagramQ             send             recv             genSRT
Network/QUIC/Config.hs view
@@ -90,6 +90,7 @@     , ccTlsHooks :: ClientHooks     , ccUse0RTT :: Bool     -- ^ Use 0-RTT on the 2nd connection if possible.+    , ccMaxDatagramFrameSize :: Int     -- client original     --     -- Default: 'False'@@ -149,6 +150,7 @@         , ccHooks = defaultHooks         , ccTlsHooks = defaultClientHooks         , ccUse0RTT = False+        , ccMaxDatagramFrameSize = 0         , -- client original           ccServerName = "127.0.0.1"         , ccPortName = "4433"@@ -185,6 +187,7 @@     , scHooks :: Hooks     , scTlsHooks :: ServerHooks     , scUse0RTT :: Bool+    , scMaxDatagramFrameSize :: Int     -- ^ Use 0-RTT on the 2nd connection if possible.     -- server original     , scAddresses :: [(IP, PortNumber)]@@ -219,6 +222,7 @@         , scHooks = defaultHooks         , scTlsHooks = defaultServerHooks         , scUse0RTT = False+        , scMaxDatagramFrameSize = 0         , -- server original           scAddresses = [("0.0.0.0", 4433), ("::", 4433)]         , scALPN = Nothing
Network/QUIC/Connection/Misc.hs view
@@ -147,6 +147,7 @@         , initialMaxStreamDataUni = resumptionInitialMaxStreamDataUni         , initialMaxStreamsBidi = resumptionInitialMaxStreamsBidi         , initialMaxStreamsUni = resumptionInitialMaxStreamsUni+        , maxDatagramFrameSize = resumptionMaxDatagramFrameSize         }  ----------------------------------------------------------------
Network/QUIC/Connection/Role.hs view
@@ -127,6 +127,7 @@         , resumptionInitialMaxStreamDataUni = initialMaxStreamDataUni         , resumptionInitialMaxStreamsBidi = initialMaxStreamsBidi         , resumptionInitialMaxStreamsUni = initialMaxStreamsUni+        , resumptionMaxDatagramFrameSize = maxDatagramFrameSize         }  ----------------------------------------------------------------
Network/QUIC/Connection/State.hs view
@@ -2,6 +2,7 @@  module Network.QUIC.Connection.State (     setConnection0RTTReady,+    isConnection0RTTReady,     isConnection1RTTReady,     setConnection1RTTReady,     isConnectionEstablished,@@ -57,6 +58,11 @@ setConnectionClosed conn = setConnectionState conn Closed  ----------------------------------------------------------------++isConnection0RTTReady :: Connection -> IO Bool+isConnection0RTTReady Connection{..} = atomically $ do+    st <- readTVar $ connectionState connState+    return (st >= ReadyFor0RTT && st /= Closed)  isConnection1RTTReady :: Connection -> IO Bool isConnection1RTTReady Connection{..} = atomically $ do
Network/QUIC/Connection/Types.hs view
@@ -242,6 +242,7 @@     , connRecv          :: ~Recv -- ~ for testing     -- Manage     , connRecvQ         :: RecvQ+    , connRecvDatagramQ :: DatagramQ     , connSocket        :: IORef Socket     , genStatelessResetToken :: CID -> StatelessResetToken     , readers           :: IORef (Map Word64 (Weak ThreadId))@@ -337,11 +338,12 @@     -> IORef Socket     -> IORef PeerInfo     -> RecvQ+    -> DatagramQ     -> Send     -> Recv     -> (CID -> StatelessResetToken)     -> IO Connection-newConnection rl myParameters origVersionInfo myAuthCIDs peerAuthCIDs connDebugLog connQLog connHooks connSocket peerInfo connRecvQ ~connSend ~connRecv genStatelessResetToken = do+newConnection rl myParameters origVersionInfo myAuthCIDs peerAuthCIDs connDebugLog connQLog connHooks connSocket peerInfo connRecvQ connRecvDatagramQ ~connSend ~connRecv genStatelessResetToken = do     connState         <- newConnState rl     -- Manage     readers           <- newIORef Map.empty@@ -355,7 +357,7 @@     -- Peer     peerParameters    <- newIORef baseParameters     peerCIDDB         <- newTVarIO (newCIDDB peerCID)-    -- Queus+    -- Queues     inputQ            <- newTQueueIO     cryptoQ           <- newTQueueIO     outputQ           <- newTQueueIO@@ -429,12 +431,17 @@     -> IORef Socket     -> IORef PeerInfo     -> RecvQ+    -> DatagramQ     -> Send     -> Recv     -> (CID -> StatelessResetToken)     -> IO Connection clientConnection ClientConfig{..} verInfo myAuthCIDs peerAuthCIDs =-    newConnection Client ccParameters verInfo myAuthCIDs peerAuthCIDs+    newConnection Client ccParameters' verInfo myAuthCIDs peerAuthCIDs+  where+    ccParameters' = ccParameters+      { maxDatagramFrameSize = ccMaxDatagramFrameSize+      }  serverConnection     :: ServerConfig@@ -447,12 +454,17 @@     -> IORef Socket     -> IORef PeerInfo     -> RecvQ+    -> DatagramQ     -> Send     -> Recv     -> (CID -> StatelessResetToken)     -> IO Connection serverConnection ServerConfig{..} verInfo myAuthCIDs peerAuthCIDs =-    newConnection Server scParameters verInfo myAuthCIDs peerAuthCIDs+    newConnection Server scParameters' verInfo myAuthCIDs peerAuthCIDs+  where+    scParameters' = scParameters+      { maxDatagramFrameSize = scMaxDatagramFrameSize+      }  ---------------------------------------------------------------- @@ -471,6 +483,8 @@ ----------------------------------------------------------------  type SendStreamQ = TQueue TxStreamData++type DatagramQ = TQueue ByteString  data Shared = Shared     { sharedCloseSent :: IORef Bool
Network/QUIC/Handshake.hs view
@@ -104,7 +104,13 @@ handshakeClient :: ClientConfig -> Connection -> AuthCIDs -> IO (IO ()) handshakeClient conf conn myAuthCIDs = do     qlogParamsSet conn (ccParameters conf, "local") -- fixme-    handshakeClient' conf conn myAuthCIDs <$> getVersion conn <*> newHndStateRef+    handshakeClient' conf' conn myAuthCIDs <$> getVersion conn <*> newHndStateRef+  where+    conf' = conf+      { ccParameters = (ccParameters conf)+        { maxDatagramFrameSize = ccMaxDatagramFrameSize conf+        }+      }  handshakeClient'     :: ClientConfig -> Connection -> AuthCIDs -> Version -> IORef HndState -> IO ()@@ -167,6 +173,7 @@     params =         (setCIDsToParameters myAuthCIDs $ scParameters conf)             { statelessResetToken = Just srt+            , maxDatagramFrameSize = scMaxDatagramFrameSize conf             }  handshakeServer'@@ -292,6 +299,15 @@                 _ -> 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+         setPeerParameters conn params         mapM_ (setPeerStatelessResetToken conn) $ statelessResetToken params         setTxMaxData conn $ initialMaxData params@@ -336,6 +352,7 @@ sendCCTLSError :: Connection -> TLS.TLSException -> IO () sendCCTLSError conn (TLS.HandshakeFailed (TLS.Error_Misc "WrongTransportParameter")) = closeConnection conn TransportParameterError "Transport parameter error" sendCCTLSError conn (TLS.HandshakeFailed (TLS.Error_Misc "WrongVersionInformation")) = closeConnection conn VersionNegotiationError "Version negotiation error"+sendCCTLSError conn (TLS.HandshakeFailed (TLS.Error_Misc "WrongDatagramSizeParameter")) = closeConnection conn ProtocolViolation "0-RTT datagram size violation" sendCCTLSError conn e = closeConnection conn err msg   where     tlserr = getErrorCause e
Network/QUIC/IO.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ module Network.QUIC.IO where  import Control.Concurrent.STM@@ -8,6 +10,7 @@ import Network.QUIC.Connection import Network.QUIC.Connector import Network.QUIC.Imports+import Network.QUIC.Parameters import Network.QUIC.Stream import Network.QUIC.Types @@ -235,3 +238,41 @@         lvl <- getEncryptionLevel conn         let frame = StopSending sid aerr         putOutput conn $ OutControl lvl [frame]++-- | Sending a DATAGRAM frame to the peer.+--   If the datagram is larger than the peer's `max_datagram_frame_size`+--   an exception is thrown.+sendDatagram :: Connection -> ByteString -> IO ()+sendDatagram conn dat = do+    -- Determine the send level from connection readiness state rather than+    -- the encryptionLevel TVar, which is not set to RTT0Level during 0-RTT.+    ready1rtt <- isConnection1RTTReady conn+    lvl <- if ready1rtt+        then return RTT1Level+        else do+            ready0rtt <- isConnection0RTTReady conn+            if ready0rtt+                then return RTT0Level+                else E.throwIO $ ConnectionIsClosed "Cannot send DATAGRAM"+    limitBytes <- maxDatagramFrameSize <$> getPeerParameters conn+    when (limitBytes == 0) $+      E.throwIO $ ConnectionIsClosed "DATAGRAM not supported by peer"+    let frameOverhead = 1 + BS.length (encodeInt (fromIntegral $ BS.length dat))+    when (BS.length dat + frameOverhead > limitBytes) $+        E.throwIO $ ConnectionIsClosed "DATAGRAM size violation"+    let frame = Datagram False dat+    putOutput conn $ OutControl lvl [frame]++-- | Receiving a DATAGRAM frame.+--   This blocks until a DATAGRAM frame is received.+recvDatagram :: Connection -> IO ByteString+recvDatagram = atomically . recvDatagramSTM++-- | Receive all available DATAGRAM frames.+--   This function is non-blocking.+recvDatagramMany :: Connection -> IO [ByteString]+recvDatagramMany = atomically . flushTQueue . connRecvDatagramQ++-- | Receiving a DATAGRAM frame in STM.+recvDatagramSTM :: Connection -> STM ByteString+recvDatagramSTM = readTQueue . connRecvDatagramQ
Network/QUIC/Info.hs view
@@ -4,6 +4,7 @@ import Network.QUIC.Connection import Network.QUIC.Types import Network.QUIC.Types.Info+import qualified Network.QUIC.Parameters as P import qualified Network.Socket as NS  ----------------------------------------------------------------@@ -17,6 +18,7 @@     peersa <- peerSockAddr <$> getPathInfo conn     mycid <- getMyCID conn     peercid <- getPeerCID conn+    peerdgsize <- P.maxDatagramFrameSize <$> getPeerParameters conn     c <- getCipher conn RTT1Level     mproto <- getApplicationProtocol conn     mode <- getTLSMode conn@@ -33,6 +35,7 @@             , remoteSockAddr = peersa             , localCID = mycid             , remoteCID = peercid+            , remoteDatagramFrameSize = peerdgsize             }  ----------------------------------------------------------------
Network/QUIC/Packet/Frame.hs view
@@ -19,7 +19,11 @@  encodeFrames :: [Frame] -> IO ByteString encodeFrames frames = withWriteBuffer 2048 $ \wbuf ->-    mapM_ (encodeFrame wbuf) frames+    let loop [] = return ()+        loop [Datagram _ dat] = encodeDatagramFinal wbuf dat+        loop [f] = encodeFrame wbuf f+        loop (f:fs) = encodeFrame wbuf f >> loop fs+    in loop frames  encodeFramesWithPadding     :: Buffer@@ -134,9 +138,19 @@     copyShortByteString wbuf reason encodeFrame wbuf HandshakeDone =     write8 wbuf 0x1e+encodeFrame wbuf (Datagram _ dat) = do+    write8 wbuf 0x31+    encodeInt' wbuf $ fromIntegral $ BS.length dat+    copyByteString wbuf dat encodeFrame wbuf (UnknownFrame typ) =     write8 wbuf $ fromIntegral typ +-- only valid when the datagram is the last frame and there's no padding+encodeDatagramFinal :: WriteBuffer -> ByteString -> IO ()+encodeDatagramFinal wbuf dat = do+    write8 wbuf 0x30+    copyByteString wbuf dat+ ----------------------------------------------------------------  decodeFramesBS :: ByteString -> IO (Maybe [Frame])@@ -190,6 +204,8 @@         0x1c -> decodeConnectionClose rbuf         0x1d -> decodeConnectionCloseApp rbuf         0x1e -> return HandshakeDone+        0x30 -> decodeDatagram rbuf False+        0x31 -> decodeDatagram rbuf True         x -> return $ UnknownFrame x  decodePadding :: ReadBuffer -> IO Frame@@ -360,3 +376,10 @@ decodePathResponse :: ReadBuffer -> IO Frame decodePathResponse rbuf =     PathResponse . PathData <$> extractShortByteString rbuf 8++decodeDatagram :: ReadBuffer -> Bool -> IO Frame+decodeDatagram rbuf hasLen = do+    len <- if hasLen+             then fromIntegral <$> decodeInt' rbuf+             else remainingSize rbuf+    Datagram hasLen <$> extractByteString rbuf len
Network/QUIC/Parameters.hs view
@@ -70,6 +70,8 @@ pattern RetrySourceConnectionId          = Key 0x10 pattern VersionInformation              :: Key pattern VersionInformation               = Key 0x11+pattern MaxDatagramFrameSize            :: Key+pattern MaxDatagramFrameSize             = Key 0x20 pattern Grease                          :: Key pattern Grease                           = Key 0xff pattern GreaseQuicBit                   :: Key@@ -98,6 +100,7 @@     , grease :: Maybe ByteString     , greaseQuicBit :: Bool     , versionInformation :: Maybe VersionInfo+    , maxDatagramFrameSize :: Int     }     deriving (Eq, Show) @@ -125,6 +128,7 @@         , grease = Nothing         , greaseQuicBit = False         , versionInformation = Nothing+        , maxDatagramFrameSize = 0         }  decInt :: ByteString -> Int@@ -205,6 +209,8 @@         x{greaseQuicBit = True}     update x (VersionInformation, v) =         x{versionInformation = toVersionInfo v}+    update x (MaxDatagramFrameSize, v) =+        x{maxDatagramFrameSize = decInt v}     update x _ = x  diff@@ -256,6 +262,7 @@         , diff p greaseQuicBit GreaseQuicBit (const "")         , diff p grease Grease fromJust         , diff p versionInformation VersionInformation fromVersionInfo+        , diff p maxDatagramFrameSize MaxDatagramFrameSize encInt         ]  encSRT :: Maybe StatelessResetToken -> ByteString@@ -289,7 +296,7 @@ -- | An example parameters obsoleted in the near future. -- -- >>> defaultParameters--- Parameters {originalDestinationConnectionId = Nothing, maxIdleTimeout = 30000, statelessResetToken = Nothing, maxUdpPayloadSize = 2048, initialMaxData = 16777216, initialMaxStreamDataBidiLocal = 262144, initialMaxStreamDataBidiRemote = 262144, initialMaxStreamDataUni = 262144, initialMaxStreamsBidi = 64, initialMaxStreamsUni = 3, ackDelayExponent = 3, maxAckDelay = 25, disableActiveMigration = False, preferredAddress = Nothing, activeConnectionIdLimit = 5, initialSourceConnectionId = Nothing, retrySourceConnectionId = Nothing, grease = Nothing, greaseQuicBit = True, versionInformation = Nothing}+-- Parameters {originalDestinationConnectionId = Nothing, maxIdleTimeout = 30000, statelessResetToken = Nothing, maxUdpPayloadSize = 2048, initialMaxData = 16777216, initialMaxStreamDataBidiLocal = 262144, initialMaxStreamDataBidiRemote = 262144, initialMaxStreamDataUni = 262144, initialMaxStreamsBidi = 64, initialMaxStreamsUni = 3, ackDelayExponent = 3, maxAckDelay = 25, disableActiveMigration = False, preferredAddress = Nothing, activeConnectionIdLimit = 5, initialSourceConnectionId = Nothing, retrySourceConnectionId = Nothing, grease = Nothing, greaseQuicBit = True, versionInformation = Nothing, maxDatagramFrameSize = 0} defaultParameters :: Parameters defaultParameters =     baseParameters@@ -303,6 +310,7 @@         , initialMaxStreamsUni = 3         , activeConnectionIdLimit = 5         , greaseQuicBit = True+        , maxDatagramFrameSize = 0         }  data AuthCIDs = AuthCIDs
Network/QUIC/Qlog.hs view
@@ -115,6 +115,7 @@ frameType ConnectionClose{} = "connection_close" frameType ConnectionCloseApp{} = "connection_close" frameType HandshakeDone{} = "handshake_done"+frameType Datagram{} = "datagram" frameType UnknownFrame{} = "unknown"  {-# INLINE frameExtra #-}@@ -169,6 +170,7 @@         <> toLogStr (Short.fromShort reason)         <> "\"" -- fixme frameExtra HandshakeDone{} = ""+frameExtra (Datagram _ dat) = ",\"length\":" <> sw (BS.length dat) frameExtra (UnknownFrame _Int) = ""  transportError :: TransportError -> LogStr
Network/QUIC/Receiver.hs view
@@ -23,6 +23,7 @@ import Network.QUIC.Parameters import Network.QUIC.Qlog import Network.QUIC.Recovery+import Control.Concurrent.STM import Network.QUIC.Stream import Network.QUIC.Types as QUIC @@ -474,6 +475,16 @@     getConnectionInfo conn >>= onConnectionEstablished (connHooks conn)     -- to receive NewSessionTicket     fire conn (Microseconds 1000000) $ killHandshaker conn lvl+processFrame conn lvl (Datagram hasLen dat) = do+    when (lvl /= RTT0Level && lvl /= RTT1Level) $+        closeConnection conn ProtocolViolation "DATAGRAM in Initial or Handshake"+    let limitBytes = maxDatagramFrameSize (getMyParameters conn)+    let lenSize = if hasLen then BS.length (encodeInt . fromIntegral $ BS.length dat) else 0+    when (limitBytes == 0) $+        closeConnection conn ProtocolViolation "DATAGRAM not supported"+    when (1 + lenSize + BS.length dat > limitBytes) $+        closeConnection conn ProtocolViolation "DATAGRAM size violation"+    atomically $ writeTQueue (connRecvDatagramQ conn) dat processFrame conn _ _ = closeConnection conn ProtocolViolation "Frame is not allowed"  -- Return value indicates duplication.
Network/QUIC/Sender.hs view
@@ -340,6 +340,7 @@     let r = MaxData newMax     rs <- adjustForRetransmit conn xs     return (r : rs)+adjustForRetransmit conn (Datagram{} : xs) = adjustForRetransmit conn xs adjustForRetransmit conn (x : xs) = do     rs <- adjustForRetransmit conn xs     return (x : rs)
Network/QUIC/Server.hs view
@@ -12,6 +12,7 @@     scALPN,     scRequireRetry,     scUse0RTT,+    scMaxDatagramFrameSize,     scCiphers,     scGroups,     scVersions,
Network/QUIC/Server/Run.hs view
@@ -160,6 +160,7 @@     (qLog, qclean) <- dirQLogger scQLog accTime ocid "server"     (debugLog, dclean) <- dirDebugLogger scDebugLog ocid     debugLog $ "Original CID: " <> bhow ocid+    connRecvDatagramQ <- newTQueueIO     conn <-         serverConnection             conf@@ -172,6 +173,7 @@             sref             piref             accRecvQ+            connRecvDatagramQ             send             recv             (genStatelessReset dispatch)
Network/QUIC/Types/Exception.hs view
@@ -31,6 +31,7 @@     = MustNotReached     | ExitConnection     | WrongTransportParameter+    | WrongDatagramSizeParameter     | WrongVersionInformation     | BreakForever     deriving (Eq, Show)
Network/QUIC/Types/Frame.hs view
@@ -40,6 +40,7 @@     | ConnectionClose TransportError FrameType ReasonPhrase     | ConnectionCloseApp ApplicationProtocolError ReasonPhrase     | HandshakeDone+    | Datagram Bool ByteString     | UnknownFrame Int     deriving (Eq, Show) 
Network/QUIC/Types/Info.hs view
@@ -24,6 +24,7 @@     , remoteSockAddr :: NS.SockAddr     , localCID :: CID     , remoteCID :: CID+    , remoteDatagramFrameSize :: Int     }  instance Show ConnectionInfo where@@ -51,4 +52,7 @@             ++ "\n"             ++ "Remote SockAddr: "             ++ show remoteSockAddr+            ++ "\n"+            ++ "Remote DatagramFrameSize: "+            ++ show remoteDatagramFrameSize             ++ if retry then "\nQUIC retry" else ""
Network/QUIC/Types/Resumption.hs view
@@ -26,6 +26,7 @@     , resumptionInitialMaxStreamDataUni :: Int     , resumptionInitialMaxStreamsBidi :: Int     , resumptionInitialMaxStreamsUni :: Int+    , resumptionMaxDatagramFrameSize :: Int     }     deriving (Eq, Show, Generic) @@ -45,6 +46,7 @@         , resumptionInitialMaxStreamDataUni = 0         , resumptionInitialMaxStreamsBidi = 0         , resumptionInitialMaxStreamsUni = 0+        , resumptionMaxDatagramFrameSize = 0         }  -- | Is 0RTT possible?
quic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      >=1.10 name:               quic-version:            0.3.1+version:            0.3.2 license:            BSD3 license-file:       LICENSE maintainer:         kazu@iij.ad.jp@@ -237,6 +237,7 @@     hs-source-dirs:     test     other-modules:         Config+        DatagramSpec         ErrorSpec         FrameSpec         HandshakeSpec@@ -262,6 +263,7 @@         network >=3.2.2,         quic,         ram,+        stm,         tls,         unix-time 
test/Config.hs view
@@ -150,7 +150,7 @@   where     hints =         defaultHints-            { addrSocketType = Datagram+            { addrSocketType = Network.Socket.Datagram             , addrFlags = [AI_NUMERICHOST]             , addrFamily = AF_INET             }
+ test/DatagramSpec.hs view
@@ -0,0 +1,314 @@+{-# LANGUAGE OverloadedStrings #-}++module DatagramSpec where++import Control.Concurrent+import qualified Control.Exception as E+import Control.Monad+import Data.ByteString ()+import qualified Data.ByteString as BS+import Test.Hspec++import Network.QUIC+import Network.QUIC.Client as C+import Network.QUIC.Internal hiding (RTT0)+import Network.QUIC.Server as S+import Network.TLS (HandshakeMode13 (..))++import Config++spec :: Spec+spec = do+    sc0' <- runIO makeTestServerConfig+    let sc0 = sc0'+    describe "Datagram MUST requirements" $ do+        -- "An endpoint that receives a DATAGRAM frame when it has not indicated support via the transport parameter MUST terminate the connection with an error of type PROTOCOL_VIOLATION."+        it "Server MUST send PROTOCOL_VIOLATION if DATAGRAM received without negotiation" $ do+            let cc = testClientConfig { ccHooks = (ccHooks testClientConfig) { onPlainCreated = injectDatagram1RTT } }+            testServerExpectError cc sc0 isProtocolViolationReceived++        -- "An endpoint that receives a DATAGRAM frame when it has not indicated support via the transport parameter MUST terminate the connection with an error of type PROTOCOL_VIOLATION."+        it "Client MUST send PROTOCOL_VIOLATION if DATAGRAM received without negotiation" $ do+            let sc = sc0 { scHooks = (scHooks sc0) { onPlainCreated = injectDatagram1RTT } }+            testClientExpectError testClientConfig sc isProtocolViolationSent++        -- "Similarly, an endpoint that receives a DATAGRAM frame that is larger than the value it sent in its max_datagram_frame_size transport parameter MUST terminate the connection with an error of type PROTOCOL_VIOLATION."+        it "Rejects DATAGRAM larger than max_datagram_frame_size" $ do+            let scDtgm = sc0 { scMaxDatagramFrameSize = 10 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 10+                                          , ccHooks = (ccHooks testClientConfig) { onPlainCreated = injectLargeDatagram } }+            testServerExpectError ccDtgm scDtgm isProtocolViolationReceived++        -- "Like STREAM frames, DATAGRAM frames contain application data and MUST be protected with either 0-RTT or 1-RTT keys."+        it "MUST send PROTOCOL_VIOLATION if DATAGRAM in Initial or Handshake" $ do+            let scDtgm = sc0 { scMaxDatagramFrameSize = 1024 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 1024+                                          , ccHooks = (ccHooks testClientConfig) { onPlainCreated = injectDatagramInitial } }+            testServerExpectError ccDtgm scDtgm isProtocolViolationReceived++        -- "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 }+                cc0RTT = testClientConfig { ccMaxDatagramFrameSize = 1024 }+            testDatagram0RTTDowngrade cc0RTT scInit scDowngrade++        -- "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 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 0 }+            mvar <- newEmptyMVar+            smgr <- newSessionManager+            let sc' = scDtgm { scSessionManager = smgr+                             , scHooks = (scHooks scDtgm) { onServerReady = putMVar mvar () } }+            E.bracket (forkIO $ E.handle (\(E.SomeException _) -> return ()) $ S.run sc' (\conn -> waitEstablished conn >> threadDelay 2000000)) killThread $ \_ -> do+                takeMVar mvar+                let isNoSupport (ConnectionIsClosed err) = err == "DATAGRAM not supported by peer"+                    isNoSupport _ = False+                C.run ccDtgm (\conn -> do+                    waitEstablished conn+                    sendDatagram conn "data") `shouldThrow` isNoSupport++        -- "An endpoint MUST NOT send DATAGRAM frames that are larger than the max_datagram_frame_size value it has received from its peer."+        it "Endpoint MUST NOT send DATAGRAM larger than peer's max_datagram_frame_size" $ do+            let scDtgm = sc0 { scMaxDatagramFrameSize = 10 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 10 }+            mvar <- newEmptyMVar+            smgr <- newSessionManager+            let sc' = scDtgm { scSessionManager = smgr+                             , scHooks = (scHooks scDtgm) { onServerReady = putMVar mvar () } }+            E.bracket (forkIO $ E.handle (\(E.SomeException _) -> return ()) $ S.run sc' (\conn -> waitEstablished conn >> threadDelay 2000000)) killThread $ \_ -> do+                takeMVar mvar+                let isSizeViolation (ConnectionIsClosed err) = err == "DATAGRAM size violation"+                    isSizeViolation _ = False+                C.run ccDtgm (\conn -> do+                    waitEstablished conn+                    sendDatagram conn (BS.replicate 20 0x41)) `shouldThrow` isSizeViolation++        -- "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 }+                cc0RTT = testClientConfig { ccMaxDatagramFrameSize = 1024 }++            mvar <- newEmptyMVar+            smgr <- newSessionManager++            -- Phase 1+            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 cc0RTT $ \conn -> do+                    waitEstablished conn+                    threadDelay 50000+                    getResumptionInfo conn++            -- Phase 2+            let cc2 = cc0RTT { ccResumption = res, ccUse0RTT = True }+            mvar2 <- newEmptyMVar+            let scDowngrade' = scDowngrade { scSessionManager = smgr, scUse0RTT = True, scHooks = (scHooks scDowngrade) { onServerReady = putMVar mvar2 () } }++            E.bracket (forkIO $ E.handle (\(E.SomeException _) -> return ()) $ S.run scDowngrade' (\conn -> do+                        waitEstablished conn+                        threadDelay 2000000)) killThread $ \_ -> do+                takeMVar mvar2+                C.run cc2 (\_ -> threadDelay 2000000) `shouldThrow` isWrongDatagramSize++        -- "The sender MUST either delay sending the frame until the controller allows it or drop the frame without sending it"+        it "Sender MUST apply congestion control to DATAGRAM frames" $ do+            let scDtgm = sc0 { scMaxDatagramFrameSize = 1024 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 1024 }+            mvar <- newEmptyMVar+            smgr <- newSessionManager+            let sc' = scDtgm { scSessionManager = smgr+                             , scHooks = (scHooks scDtgm) { onServerReady = putMVar mvar () } }+            E.bracket (forkIO $ E.handle (\(E.SomeException _) -> return ()) $ S.run sc' (\conn -> waitEstablished conn >> threadDelay 2000000)) killThread $ \_ -> do+                takeMVar mvar+                C.run ccDtgm $ \conn -> do+                    waitEstablished conn+                    replicateM_ 100 $ do+                        sendDatagram conn (BS.replicate 50 0x41)+                        threadDelay 500++    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+            let scDtgm = sc0 { scMaxDatagramFrameSize = 1024 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 1024 }+            testDatagram1RTT ccDtgm scDtgm True++        -- Zero-length datagrams are explicitly permitted by the spec (Section 4)+        it "Can handle zero-length DATAGRAM" $ do+            let scDtgm = sc0 { scMaxDatagramFrameSize = 1024 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 1024+                                          , ccHooks = (ccHooks testClientConfig) { onPlainCreated = injectZeroLengthDatagram } }+            testDatagram1RTT ccDtgm scDtgm False++        -- "When clients use 0-RTT, they MAY store the value of the server's max_datagram_frame_size transport parameter. Doing so allows the client to send DATAGRAM frames in 0-RTT packets."+        it "Can utilize 0-RTT DATAGRAM successfully" $ do+            let scDtgm = sc0 { scMaxDatagramFrameSize = 1024 }+                ccDtgm = testClientConfig { ccMaxDatagramFrameSize = 1024 }+            testDatagram0RTT ccDtgm scDtgm++injectDatagram1RTT :: EncryptionLevel -> Plain -> Plain+injectDatagram1RTT lvl plain+    | lvl == RTT1Level = plain { plainFrames = Datagram False "hello" : plainFrames plain }+    | otherwise = plain++injectLargeDatagram :: EncryptionLevel -> Plain -> Plain+injectLargeDatagram lvl plain+    | lvl == RTT1Level = plain { plainFrames = Datagram False (BS.replicate 20 0x41) : plainFrames plain }+    | otherwise = plain++injectDatagramInitial :: EncryptionLevel -> Plain -> Plain+injectDatagramInitial lvl plain+    | lvl == InitialLevel = plain { plainFrames = Datagram False "hello" : plainFrames plain }+    | otherwise = plain++injectZeroLengthDatagram :: EncryptionLevel -> Plain -> Plain+injectZeroLengthDatagram lvl plain+    | lvl == RTT1Level = plain { plainFrames = Datagram False "" : plainFrames plain }+    | otherwise = plain++isProtocolViolationSent :: QUICException -> Bool+isProtocolViolationSent (TransportErrorIsSent ProtocolViolation _) = True+isProtocolViolationSent _ = False++isProtocolViolationReceived :: QUICException -> Bool+isProtocolViolationReceived (TransportErrorIsReceived ProtocolViolation _) = True+isProtocolViolationReceived _ = False++isWrongDatagramSize :: QUICException -> Bool+isWrongDatagramSize (TransportErrorIsSent ProtocolViolation _) = True+isWrongDatagramSize (TransportErrorIsReceived ProtocolViolation _) = True+isWrongDatagramSize _ = False++-- | Server detects the error and sends ConnectionClose; client receives it.+testServerExpectError :: ClientConfig -> ServerConfig -> (QUICException -> Bool) -> IO ()+testServerExpectError cc sc selector = do+    mvar <- newEmptyMVar+    smgr <- newSessionManager+    let sc' = sc { scSessionManager = smgr+                 , scHooks = (scHooks sc) { onServerReady = putMVar mvar () } }+    E.bracket (forkIO $ server sc') killThread $ \_ -> client mvar+  where+    client mvar = do+        () <- takeMVar mvar+        C.run cc (\conn -> do+            wait1RTTReady conn+            threadDelay 2000000) `shouldThrow` selector+    server sc' =+        E.handle (\(E.SomeException _) -> return ()) $ S.run sc' $ \conn -> do+            wait1RTTReady conn+            threadDelay 2000000++-- | Client detects the error and sends ConnectionClose itself.+testClientExpectError :: ClientConfig -> ServerConfig -> (QUICException -> Bool) -> IO ()+testClientExpectError cc sc selector = do+    mvar <- newEmptyMVar+    smgr <- newSessionManager+    let sc' = sc { scSessionManager = smgr+                 , scHooks = (scHooks sc) { onServerReady = putMVar mvar () } }+    E.bracket (forkIO $ server sc') killThread $ \_ -> client mvar+  where+    client mvar = do+        () <- takeMVar mvar+        C.run cc (\conn -> do+            wait1RTTReady conn+            threadDelay 2000000) `shouldThrow` selector+    server sc' =+        E.handle (\(E.SomeException _) -> return ()) $ S.run sc' $ \conn -> do+            wait1RTTReady conn+            threadDelay 2000000++testDatagram1RTT :: ClientConfig -> ServerConfig -> Bool -> IO ()+testDatagram1RTT cc sc expectServerData = do+    mvar <- newEmptyMVar+    smgr <- newSessionManager+    let sc' = sc { scSessionManager = smgr+                 , scHooks = (scHooks sc) { onServerReady = putMVar mvar () } }+    E.bracket (forkIO $ server sc') killThread $ \_ -> client mvar+  where+    client mvar = do+        () <- takeMVar mvar+        C.run cc $ \conn -> do+            waitEstablished conn+            sendDatagram conn "client_data"+            when expectServerData $ do+                recv <- recvDatagram conn+                recv `shouldBe` "server_data"+    server sc' = do+        S.run sc' $ \conn -> do+            waitEstablished conn+            recv <- recvDatagram conn+            when (BS.length recv == 0 || recv == "hello") $ void (recvDatagram conn)+            when expectServerData $ sendDatagram conn "server_data"+++testDatagram0RTT :: ClientConfig -> ServerConfig -> IO ()+testDatagram0RTT cc1 sc1 = do+    mvar <- newEmptyMVar+    smgr <- newSessionManager++    -- Phase 1: initial connection to get resumption info+    let scInit = sc1 { scSessionManager = smgr, scUse0RTT = True, scHooks = (scHooks sc1) { 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++    -- Phase 2: 0-RTT connection with datagram+    let cc2 = cc1 { ccResumption = res, ccUse0RTT = True }++    mvar2 <- newEmptyMVar+    let sc0RTT = scInit { scHooks = (scHooks sc1) { onServerReady = putMVar mvar2 () } }++    E.bracket (forkIO $ server0RTT sc0RTT) killThread $ \_ -> client0RTT cc2 mvar2+  where+    client0RTT cc2 mvar2 = do+        () <- takeMVar mvar2+        C.run cc2 $ \conn -> do+            sendDatagram conn "0rtt_client_data"+            waitEstablished conn+            threadDelay 50000+    server0RTT sc = S.run sc $ \conn -> do+        waitEstablished conn+        info <- getConnectionInfo conn+        when (handshakeMode info == RTT0) $ do+            recv <- recvDatagram conn+            recv `shouldBe` "0rtt_client_data"++testDatagram0RTTDowngrade :: ClientConfig -> ServerConfig -> ServerConfig -> IO ()+testDatagram0RTTDowngrade cc1 scInit scDowngrade = do+    mvar <- newEmptyMVar+    smgr <- newSessionManager++    -- Phase 1: initial connection to get resumption info+    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++    -- Phase 2: reconnect with downgraded server params; client should detect and reject+    let cc2 = cc1 { ccResumption = res, ccUse0RTT = True }++    mvar2 <- newEmptyMVar+    let scDowngrade' = scDowngrade { scSessionManager = smgr, scUse0RTT = True, scHooks = (scHooks scDowngrade) { onServerReady = putMVar mvar2 () } }++    E.bracket (forkIO $ server scDowngrade') killThread $ \_ ->+        client mvar2 cc2+  where+    server sc =+        E.handle (\(E.SomeException _) -> return ()) $ S.run sc $ \conn -> do+            waitEstablished conn+            threadDelay 2000000+    client mvar2 cc2 = do+        () <- takeMVar mvar2+        C.run cc2 (\_ -> threadDelay 2000000) `shouldThrow` isWrongDatagramSize
test/PacketSpec.hs view
@@ -10,6 +10,7 @@ import Test.Hspec  import Network.QUIC.Internal+import Control.Concurrent.STM  import Config @@ -54,6 +55,7 @@     let ver = v         verInfo = VersionInfo ver [ver]     genSRT <- makeGenStatelessReset+    dq <- newTQueueIO     ----     clientConn <-         clientConnection@@ -67,8 +69,9 @@             sref             piref             q-            undefined-            undefined+            dq+            (\_ _ -> return ())+            (return undefined)             genSRT     initializeCoder clientConn InitialLevel $ initialSecrets ver serverCID     serverConn <-@@ -83,8 +86,9 @@             sref             piref -- dummy             q-            undefined-            undefined+            dq+            (\_ _ -> return ())+            (return undefined)             genSRT     initializeCoder serverConn InitialLevel $ initialSecrets ver serverCID     ----