packages feed

quic-0.3.20: Network/QUIC/Connection/RateControl.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Limits against a peer which makes us answer frames.
module Network.QUIC.Connection.RateControl (
    -- * Limits
    PerSecond,
    pathAndCIDFrameLimit,
    pathResponseLimit,
    queuedControlLimit,
    statelessResetLimit,
    statelessResetSize,
    statelessResetSizeFor,

    -- * Enforcing
    enforcePathAndCIDFrameLimit,
    withinPathResponseLimit,
    enforceQueuedControlLimit,
    withinStatelessResetLimit,
) where

import Network.Control (Rate, addRate)

import Network.QUIC.Connection.Misc
import Network.QUIC.Connection.Queue
import Network.QUIC.Connection.Types
import Network.QUIC.Imports
import Network.QUIC.Types

----------------------------------------------------------------

-- | How many events a counting window may hold.  A window starts at its
--   first event and lasts about a second, so a burst at the end of one
--   and the start of the next can reach twice the limit.
--
-- A peer can make us answer frames and keep the answers from leaving by
-- not opening its congestion window (CVE-2023-49295, CVE-2024-22189).
-- The limits are against that.  They are fixed, not configurable.  An
-- @enforce@ limit closes the connection with 'InternalError' when it is
-- exceeded; a @within@ limit drops what is past it, which is done only
-- for what the peer retransmits by itself.
--
-- Frames which must be answered and which another limit already bounds
-- are not rate-controlled.  STOP_SENDING and RESET_STREAM are bounded by
-- MAX_STREAMS, and a peer cancelling many streams at once does nothing
-- wrong.  A STOP_SENDING is answered once per stream.  NEW_CONNECTION_ID
-- is always processed, as dropping it would lose its Retire Prior To.
newtype PerSecond = PerSecond Int deriving (Eq, Ord, Show)

-- | Per connection, PATH_CHALLENGE, PATH_RESPONSE, NEW_CONNECTION_ID and
--   RETIRE_CONNECTION_ID received count together.  Enforced.
pathAndCIDFrameLimit :: PerSecond
pathAndCIDFrameLimit = PerSecond 32

-- | Per connection, PATH_CHALLENGE frames answered.  The rest are
--   dropped: the peer sends a new one if it gets no PATH_RESPONSE.
pathResponseLimit :: PerSecond
pathResponseLimit = PerSecond 10

-- | Per connection, control frames waiting for the sender, those of lost
--   packets to resend included.  Data, which flow control bounds, is not
--   counted.  Enforced.  With what the congestion window lets be in
--   flight, this bounds the RETIRE_CONNECTION_ID frames a NEW_CONNECTION_ID
--   flood asks for.  The value is quic-go's.
queuedControlLimit :: Int
queuedControlLimit = 16384

-- | Per server, stateless resets sent.  The rest are not sent.
statelessResetLimit :: PerSecond
statelessResetLimit = PerSecond 20

-- | The largest stateless reset we send, in octets.
statelessResetSize :: Int
statelessResetSize = 1280

-- | The size of a stateless reset in answer to a packet of this many
--   octets, if one can be sent.
--
-- Always smaller than the packet (RFC 9000, section 10.3.3): a reset looks
-- like a packet for an unknown connection, so two endpoints could answer
-- each other's resets for ever.  Half of it, so that such a loop ends in a
-- few rounds, and one octet less for a packet of 43 octets or fewer
-- (section 10.3).  None below the 21 octets a reset needs.  Being smaller,
-- it is also under three times the packet (section 10.3).
statelessResetSizeFor :: Int -> Maybe Int
statelessResetSizeFor bytes
    | siz < 21 = Nothing
    | otherwise = Just siz
  where
    siz
        | bytes <= 43 = bytes - 1
        | otherwise = min statelessResetSize (bytes `div` 2)

----------------------------------------------------------------

-- Adds n to the window and tells whether it is still within the limit.
-- The count includes what is being added, so the limit itself is let
-- through with '<='.  '<' let one fewer through than the name says.
addWithin :: Rate -> Int -> PerSecond -> IO Bool
addWithin rate n (PerSecond lim) = (<= lim) <$> addRate rate n

-- | Counting the path and CID frames of a packet against
--   'pathAndCIDFrameLimit'.
enforcePathAndCIDFrameLimit :: Connection -> [Frame] -> IO ()
enforcePathAndCIDFrameLimit conn frames = do
    let n = length $ filter isPathOrCID frames
    when (n /= 0) $ do
        ok <- addWithin (pathAndCIDFrameRate conn) n pathAndCIDFrameLimit
        unless ok $
            closeConnection conn InternalError "Too many path or CID frames"
  where
    isPathOrCID PathChallenge{} = True
    isPathOrCID PathResponse{} = True
    isPathOrCID NewConnectionID{} = True
    isPathOrCID RetireConnectionID{} = True
    isPathOrCID _ = False

-- | Whether a PATH_CHALLENGE may be answered under 'pathResponseLimit'.
withinPathResponseLimit :: Connection -> IO Bool
withinPathResponseLimit conn =
    addWithin (pathResponseRate conn) 1 pathResponseLimit

-- | Closing the connection if more control frames than
--   'queuedControlLimit' wait for the sender.
enforceQueuedControlLimit :: Connection -> IO ()
enforceQueuedControlLimit conn = do
    queued <- getQueuedControls conn
    when (queued > queuedControlLimit) $
        closeConnection conn InternalError "Too many control frames queued"

-- | Whether a stateless reset may be sent under 'statelessResetLimit'.
withinStatelessResetLimit :: Rate -> IO Bool
withinStatelessResetLimit rate = addWithin rate 1 statelessResetLimit