quic-0.2.22: Network/QUIC/Server/Reader.hs
{-# LANGUAGE CPP #-}
{-# 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
#if MIN_VERSION_random(1,3,0)
import System.Random (getStdRandom, randomRIO, uniformByteString)
#else
import System.Random (getStdRandom, randomRIO, genByteString)
#endif
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)
#if MIN_VERSION_random(1,3,0)
body <- getStdRandom $ uniformByteString 1263
#else
body <- getStdRandom $ genByteString 1263
#endif
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)