quic-0.2.21: Network/QUIC/Server/Reader.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Network.QUIC.Server.Reader (
Dispatch,
newDispatch,
clearDispatch,
runDispatcher,
tokenMgr,
genStatelessReset,
-- * Accepting
Accept (..),
-- * Receiving and reading
RecvQ,
recvServer,
ServerState (..),
) where
import Control.Concurrent
import Control.Concurrent.STM
import qualified Control.Exception as E
import qualified Crypto.Token as CT
import qualified Data.ByteString as BS
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import qualified GHC.IO.Exception as E
import Network.ByteOrder
import Network.Control (LRUCacheRef, Rate, getRate, newRate)
import qualified Network.Control as LRUCache
import Network.Socket (SockAddr, Socket, waitReadSocketSTM)
import qualified Network.Socket.ByteString as NSB
import qualified System.IO.Error as E
import System.Log.FastLogger
import System.Random (getStdRandom, randomRIO, uniformByteString)
import Network.QUIC.Common
import Network.QUIC.Config
import Network.QUIC.Connection
import Network.QUIC.Connector
import Network.QUIC.Exception
import Network.QUIC.Imports
import Network.QUIC.Logger
import Network.QUIC.Packet
import Network.QUIC.Parameters
import Network.QUIC.Qlog
import Network.QUIC.Types
import Network.QUIC.Windows
----------------------------------------------------------------
data Dispatch = Dispatch
{ tokenMgr :: CT.TokenManager
, dstTable :: IORef ConnectionDict
, srcTable :: RecvQDict
, genStatelessReset :: CID -> StatelessResetToken
, statelessResetRate :: Rate
}
statelessResetLimit :: Int
statelessResetLimit = 20
newDispatch :: ServerConfig -> IO Dispatch
newDispatch ServerConfig{..} =
Dispatch
<$> CT.spawnTokenManager conf
<*> newIORef emptyConnectionDict
<*> newRecvQDict
<*> makeGenStatelessReset
<*> newRate
where
conf =
CT.defaultConfig
{ CT.tokenLifetime = scTicketLifetime
, CT.threadName = "QUIC token manager"
}
clearDispatch :: Dispatch -> IO ()
clearDispatch d = CT.killTokenManager $ tokenMgr d
----------------------------------------------------------------
newtype ConnectionDict = ConnectionDict (Map CID Connection)
emptyConnectionDict :: ConnectionDict
emptyConnectionDict = ConnectionDict M.empty
lookupConnectionDict :: IORef ConnectionDict -> CID -> IO (Maybe Connection)
lookupConnectionDict ref cid = do
ConnectionDict tbl <- readIORef ref
return $ M.lookup cid tbl
registerConnectionDict :: IORef ConnectionDict -> CID -> Connection -> IO ()
registerConnectionDict ref cid conn = atomicModifyIORef'' ref $
\(ConnectionDict tbl) -> ConnectionDict $ M.insert cid conn tbl
unregisterConnectionDict :: IORef ConnectionDict -> CID -> IO ()
unregisterConnectionDict ref cid = atomicModifyIORef'' ref $
\(ConnectionDict tbl) -> ConnectionDict $ M.delete cid tbl
----------------------------------------------------------------
-- Source CID -> RecvQ
-- Initials and RTT0 are queued before Conneciton is created.
newtype RecvQDict = RecvQDict (LRUCacheRef CID RecvQ)
recvQDictSize :: Int
recvQDictSize = 100
newRecvQDict :: IO RecvQDict
newRecvQDict = RecvQDict <$> LRUCache.newLRUCacheRef recvQDictSize
-- Looking up and insert a new RecvQ if not exist.
lookupInsertRecvQDict :: RecvQDict -> CID -> IO (RecvQ, Bool)
lookupInsertRecvQDict (RecvQDict ref) dcid = LRUCache.cached ref dcid newRecvQ
lookupRecvQDict :: RecvQDict -> CID -> IO (Maybe RecvQ)
lookupRecvQDict (RecvQDict ref) dcid = LRUCache.cached' ref dcid
----------------------------------------------------------------
data Accept = Accept
{ accVersionInfo :: VersionInfo
, accMyAuthCIDs :: AuthCIDs
, accPeerAuthCIDs :: AuthCIDs
, accMySocket :: Socket
, accPeerSockAddr :: SockAddr
, accRecvQ :: RecvQ
, accPacketSize :: Int
, accRegister :: CID -> Connection -> IO ()
, accUnregister :: CID -> IO ()
, accAddressValidated :: Bool
, accTime :: TimeMicrosecond
}
----------------------------------------------------------------
runDispatcher
:: Dispatch
-> ServerConfig
-> TVar ServerState
-> (Accept -> IO ())
-> Socket
-> IO ThreadId
runDispatcher d conf stvar forkConn mysock = forkIO $ dispatcher d conf stvar forkConn mysock
data ServerState = Running | Stopped deriving (Eq, Show)
checkLoop :: TVar ServerState -> Socket -> IO Bool
checkLoop stvar mysock = do
st0 <- readTVarIO stvar
if st0 == Stopped
then return False
else do
wait <- waitReadSocketSTM mysock
atomically $ do
st <- readTVar stvar
if st == Stopped
then return False
else do
wait -- blocking is retry
return True
dispatcher
:: Dispatch
-> ServerConfig
-> TVar ServerState
-> (Accept -> IO ())
-> Socket
-> IO ()
dispatcher d conf stvar forkConnection mysock = do
labelMe "QUIC dispatcher"
handleLogUnit logAction loop
where
loop = do
cont <- checkLoop stvar mysock
when cont $ do
(bs, peersa) <- safeRecv $ NSB.recvFrom mysock 2048
now <- getTimeMicrosecond
let send' b = void $ NSB.sendTo mysock b peersa
-- cf: greaseQuicBit $ getMyParameters conn
quicBit = greaseQuicBit $ scParameters conf
cpckts <- decodeCryptPackets bs (not quicBit)
let bytes = BS.length bs
switch = dispatch d conf forkConnection logAction mysock peersa send' bytes now
mapM_ switch cpckts
loop
logAction _msg = return ()
safeRecv rcv = do
ex <- E.try $ windowsThreadBlockHack rcv
case ex of
Right x -> return x
Left se | isAsyncException se -> E.throwIO (se :: E.SomeException)
Left se -> case E.fromException se of
Just e | E.ioeGetErrorType e == E.InvalidArgument -> E.throwIO se
_ -> do
logAction $ "recv again: " <> bhow se
rcv
----------------------------------------------------------------
-- If client initial is fragmented into multiple packets,
-- there is no way to put the all packets into a single queue.
-- Rather, each fragment packet is put into its own queue.
-- For the first fragment, handshake would successif others are
-- retransmitted.
-- For the other fragments, handshake will fail since its socket
-- cannot be connected.
dispatch
:: Dispatch
-> ServerConfig
-> (Accept -> IO ())
-> DebugLogger
-> Socket
-> SockAddr
-> (ByteString -> IO ())
-> Int
-> TimeMicrosecond
-> (CryptPacket, EncryptionLevel, Int)
-> IO ()
dispatch
Dispatch{..}
ServerConfig{..}
forkConnection
logAction
mysock
peersa
send'
bytes
tim
(cpkt@(CryptPacket (Initial peerVer dCID sCID token) _), lvl, siz)
| bytes < defaultQUICPacketSize = do
logAction $ "too small " <> bhow bytes <> ", " <> bhow peersa
| peerVer `notElem` myVersions = do
let offerVersions
| peerVer == GreasingVersion = GreasingVersion2 : myVersions
| otherwise = GreasingVersion : myVersions
bss <-
encodeVersionNegotiationPacket $
VersionNegotiationPacket sCID dCID offerVersions
send' bss
| token == "" = do
mconn <- lookupConnectionDict dstTable dCID
case mconn of
Nothing
| scRequireRetry -> sendRetry
| otherwise -> pushToAcceptFirst False
Just conn -> writeRecvQ (connRecvQ conn) $ mkReceivedPacket cpkt tim siz lvl
| otherwise = do
mconn <- lookupConnectionDict dstTable dCID
case mconn of
Nothing -> do
mct <- decryptToken tokenMgr token
case mct of
Just ct
| isRetryToken ct -> do
ok <- isRetryTokenValid ct
if ok then pushToAcceptRetried ct else sendRetry
_ -> pushToAcceptFirst True
Just conn -> writeRecvQ (connRecvQ conn) $ mkReceivedPacket cpkt tim siz lvl
where
myVersions = scVersions
pushToAcceptQ myAuthCIDs peerAuthCIDs addrValid = do
let key = nonZeroLengthCID sCID peersa
(q, exist) <- lookupInsertRecvQDict srcTable key
writeRecvQ q $ mkReceivedPacket cpkt tim siz lvl
unless exist $ do
let reg = registerConnectionDict dstTable
unreg cid =
fire' (Microseconds 1000000) $ unregisterConnectionDict dstTable cid
acc =
Accept
{ accVersionInfo = VersionInfo peerVer myVersions
, accMyAuthCIDs = myAuthCIDs
, accPeerAuthCIDs = peerAuthCIDs
, accMySocket = mysock
, accPeerSockAddr = peersa
, accRecvQ = q
, accPacketSize = bytes
, accRegister = reg
, accUnregister = unreg
, accAddressValidated = addrValid
, accTime = tim
}
forkConnection acc
-- Initial: DCID=S1, SCID=C1 ->
-- <- Initial: DCID=C1, SCID=S2
-- ...
-- 1-RTT: DCID=S2 ->
-- <- 1-RTT: DCID=C1
--
-- initial_source_connection_id = S2 (newdCID)
-- original_destination_connection_id = S1 (dCID)
-- retry_source_connection_id = Nothing
pushToAcceptFirst addrValid = do
newdCID <- newCID
let myAuthCIDs =
defaultAuthCIDs
{ initSrcCID = Just newdCID
, origDstCID = Just dCID
}
peerAuthCIDs =
defaultAuthCIDs
{ initSrcCID = Just sCID
}
pushToAcceptQ myAuthCIDs peerAuthCIDs addrValid
-- Initial: DCID=S1, SCID=C1 ->
-- <- Retry: DCID=C1, SCID=S2
-- Initial: DCID=S2, SCID=C1 ->
-- <- Initial: DCID=C1, SCID=S3
-- ...
-- 1-RTT: DCID=S3 ->
-- <- 1-RTT: DCID=C1
--
-- initial_source_connection_id = S3 (dCID) S2 in our server
-- original_destination_connection_id = S1 (o)
-- retry_source_connection_id = S2 (dCID)
pushToAcceptRetried (CryptoToken _ _ _ (Just (_, _, o))) = do
let myAuthCIDs =
defaultAuthCIDs
{ initSrcCID = Just dCID
, origDstCID = Just o
, retrySrcCID = Just dCID
}
peerAuthCIDs =
defaultAuthCIDs
{ initSrcCID = Just sCID
}
pushToAcceptQ myAuthCIDs peerAuthCIDs True
pushToAcceptRetried _ = return ()
isRetryTokenValid (CryptoToken _tver life etim (Just (l, r, _))) = do
diff <- getElapsedTimeMicrosecond etim
return $
diff <= Microseconds (fromIntegral life * 1000000)
&& dCID == l
&& sCID == r
-- Initial for ACK contains the retry token but
-- the version would be already version 2, sigh.
&& _tver == peerVer
isRetryTokenValid _ = return False
sendRetry = do
newdCID <- newCID
retryToken <- generateRetryToken peerVer scTicketLifetime newdCID sCID dCID
mnewtoken <-
timeout (Microseconds 100000) "sendRetry" $ encryptToken tokenMgr retryToken
case mnewtoken of
Nothing -> logAction "retry token stacked"
Just newtoken -> do
bss <- encodeRetryPacket $ RetryPacket peerVer sCID newdCID newtoken (Left dCID)
send' bss
----------------------------------------------------------------
dispatch
Dispatch{..}
_
_
_
_mysock
peersa
_
_
tim
(cpkt@(CryptPacket (RTT0 _ _dCID sCID) _), lvl, siz) = do
let key = nonZeroLengthCID sCID peersa
mq <- lookupRecvQDict srcTable key
case mq of
Just q -> writeRecvQ q $ mkReceivedPacket cpkt tim siz lvl
Nothing -> return ()
----------------------------------------------------------------
dispatch
Dispatch{..}
_
_
logAction
mysock
peersa
send'
bytes
tim
(cpkt@(CryptPacket (Short dCID) _), lvl, siz) = do
mconn <- lookupConnectionDict dstTable dCID
case mconn of
Nothing -> do
-- Three times rule for stateless reset
-- Our packet size is 1280
when (bytes > 427) $ do
srRate <- getRate statelessResetRate
-- fixme: hard coding
when (srRate < statelessResetLimit) $ do
flag <- randomRIO (0, 127)
body <- getStdRandom $ uniformByteString 1263
let srt = genStatelessReset dCID
statelessReset = BS.concat [BS.singleton flag, body, fromStatelessResetToken srt]
send' statelessReset
logAction $ "Stateless reset is sent to " <> bhow peersa
Just conn -> do
alive <- getAlive conn
when alive $ do
void $ setSocket conn mysock -- fixme
curCID <- getMyCID conn
-- setMyCID is not called here since setMyCID is
-- done in Receiver.
let cidChanged = curCID /= dCID
mPathInfo <- findPathInfo conn peersa
case mPathInfo of
Nothing -> do
forkManaged conn $
onClientMigration conn dCID peersa cidChanged
_ -> return ()
writeRecvQ (connRecvQ conn) $ mkReceivedPacket cpkt tim siz lvl
----------------------------------------------------------------
dispatch
Dispatch{..}
_
_
logAction
mysock
peersa
_
_
tim
(cpkt@(CryptPacket hdr _crypt), lvl, siz) = do
let dCID = headerMyCID hdr
mconn <- lookupConnectionDict dstTable dCID
case mconn of
Nothing -> logAction $ "CID no match: " <> bhow dCID <> ", " <> bhow peersa
Just conn -> do
-- fixme: is this block necessary?
void $ setSocket conn mysock
mPathInfo <- findPathInfo conn peersa
when (isNothing mPathInfo) $ do
pathInfo <- newPathInfo peersa
addPathInfo conn pathInfo
writeRecvQ (connRecvQ conn) $ mkReceivedPacket cpkt tim siz lvl
recvServer :: RecvQ -> IO ReceivedPacket
recvServer = readRecvQ
onClientMigration :: Connection -> CID -> SockAddr -> Bool -> IO ()
onClientMigration conn newdCID peersa cidChanged = handleLogUnit logAction $ do
migrating <- isPathValidating conn -- fixme: test and set
unless migrating $ do
setMigrationStarted conn
-- fixme: should not block
mcidinfo <-
if cidChanged
then timeout (Microseconds 100000) "onClientMigration" $ waitPeerCID conn
else return Nothing -- PathChallenge only, no RetireConnectionID
let msg = "Migration: " <> bhow peersa <> " (" <> bhow newdCID <> ")"
qlogDebug conn $ Debug $ toLogStr msg
connDebugLog conn $ "debug: onClientMigration: " <> msg
pathInfo <- newPathInfo peersa
-- assumed that this PathInfo is not stored in PeerInfo
addPathInfo conn pathInfo
validatePath conn pathInfo mcidinfo
where
logAction msg = connDebugLog conn ("debug: onClientMigration: " <> msg)