packages feed

quic-0.3.2: test/DatagramSpec.hs

{-# 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