packages feed

quic 0.3.12 → 0.3.13

raw patch · 15 files changed

+589/−210 lines, 15 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Network.QUIC.Internal: [getMask] :: Protector -> IO Buffer
+ Network.QUIC.Internal: [withMask] :: Protector -> WithMask
+ Network.QUIC.Internal: dropIfUnwritable :: IO () -> IO ()
+ Network.QUIC.Internal: type WithMask = Buffer -> IO () -> IO Bool
- Network.QUIC.Internal: Protector :: (Buffer -> IO ()) -> IO Buffer -> (Sample -> Mask) -> Protector
+ Network.QUIC.Internal: Protector :: (Buffer -> IO ()) -> WithMask -> (Sample -> Mask) -> Protector
- Network.QUIC.Internal: makeGcmEncrypt :: Cipher -> Key -> IV -> Key -> IO (Maybe (NiteEncrypt, Buffer -> IO (), IO Buffer))
+ Network.QUIC.Internal: makeGcmEncrypt :: Cipher -> Key -> IV -> Key -> IO (Maybe (NiteEncrypt, Buffer -> IO (), WithMask))
- Network.QUIC.Internal: makeNiteProtector :: Cipher -> Key -> IO (Buffer -> IO (), IO Buffer)
+ Network.QUIC.Internal: makeNiteProtector :: Cipher -> Key -> IO (Buffer -> IO (), WithMask)

Files

ChangeLog.md view
@@ -1,5 +1,62 @@ # ChangeLog +## 0.3.13++One line of debug output that could end the connection it described, and+three ways a failure or a coder left something behind.++* A debug write can no longer end the connection it describes.  With a+  debug directory set every line goes to the connection's file and also+  to stdout, and a daemon has no stdout to write to: it is closed, or a+  pipe whose reader has gone.  The write then throws in whichever+  protocol thread happened to log, and six of those run under nested+  `concurrently_` and take the rest down with them -- so the connection+  ended over a line of debug output.  The first write of all is the+  original CID, before the connection has been built, so the peer heard+  nothing at all and saw a handshake that never finished; a later one+  arrived as an INTERNAL_ERROR, once 0.3.12 began saying when a+  connection ends of something in here.  Against mighty that was every+  shape we had been chasing at once: the freeze, the CONNECTION_CLOSE+  that never came, and the bursts of connections that died together.  It+  came and went because a stdout that is not a terminal is+  block-buffered and it is the flush that fails.  The writes now drop an+  `IOException` -- only that, an asynchronous exception is not one, so+  cancelling a thread that is logging still cancels it.+  [#148](https://github.com/kazu-yamamoto/quic/pull/148)++* The qlog writer goes the same way and for the same reason.  It is+  called from the sender, the receiver and the closer, the same threads,+  and the disk a qlog directory sits on can fill.+  [#150](https://github.com/kazu-yamamoto/quic/pull/150)++* Free what a failed setup took.  A connection's setup, a client's, and+  the server's own are each the acquire of their own `bracket`, and an+  acquire that throws gets no release.  A server connection was leaving+  two log files, three 2048-byte buffers and a registration in the+  dispatcher; a client the same, less a log file and plus its socket,+  which nothing else closes because on the ordinary path the closer does+  and a connection that never began has no closer; the server itself+  every address it had already bound, the dispatchers on them and the+  token manager thread.  `closure''`, which runs on the way out of every+  connection, left its buffers behind if the CONNECTION_CLOSE could not+  be encoded.  Setup does fail -- a client and a server in one process+  pointed at one qlog directory ask for the same file, and the second is+  told the file is busy -- and the bursts above were leaking two handles+  apiece.+  [#149](https://github.com/kazu-yamamoto/quic/pull/149)++* The header protection mask no longer leaks a buffer for every coder.+  It was taken with `mallocBytes` and freed nowhere: 16 bytes under the+  AES-GCM ciphers and 32 under ChaCha20-Poly1305, four coders to a+  connection at each end, held for the life of the process.  A server+  taking a thousand connections a second lost about 5GB a day.  The+  buffer was raw because `getMask` handed it out and the caller read it+  after the call had returned, which nothing a garbage collector owns+  would survive; the reader is handed in instead now, so the buffer can+  be a `ForeignPtr` held across exactly the use.  This changes+  `Protector` in `Network.QUIC.Internal`.+  [#151](https://github.com/kazu-yamamoto/quic/pull/151)+ ## 0.3.12  A server that looked frozen, a Stateless Reset half the peers threw away,
Network/QUIC/Client/Run.hs view
@@ -10,8 +10,8 @@ ) where  import Control.Concurrent-import Control.Concurrent.STM import Control.Concurrent.Async+import Control.Concurrent.STM import qualified Control.Exception as E import Foreign.C.Types import qualified Network.Socket as NS@@ -112,70 +112,79 @@ createClientConnection :: ClientConfig -> VersionInfo -> IO ConnRes createClientConnection conf@ClientConfig{..} verInfo = do     (sock, peersa) <- clientSocket ccServerName ccPortName-    when ccSockConnected $ NS.connect sock peersa-    q <- newRecvQ-    sref <- newIORef sock-    pathInfo <- newPathInfo peersa-    piref <- newIORef $ PeerInfo pathInfo Nothing-    let send buf siz-            | ccSockConnected = do-                s <- readIORef sref-                void $ NS.sendBuf s buf siz-            | otherwise = do-                s <- readIORef sref-                PeerInfo pinfo _ <- readIORef piref-                void $ NS.sendBufTo s buf siz $ peerSockAddr pinfo-        recv = recvClient q-    myCID <- newCID-    -- Creating peer's CIDDB with the temporary CID.  This is-    -- overridden by resetPeerCID later since no sequence number is-    -- assigned to the temporary CID by spec.-    peerCID <- newCID-    now <- getTimeMicrosecond-    (qLog, qclean) <- dirQLogger ccQLog now peerCID "client"-    let debugLog msg-            | ccDebugLog = stdoutLogger msg-            | otherwise = return ()-    debugLog $ "Original CID: " <> bhow peerCID-    let myAuthCIDs = defaultAuthCIDs{initSrcCID = Just myCID}-        peerAuthCIDs = defaultAuthCIDs{initSrcCID = Just peerCID, origDstCID = Just peerCID}-    genSRT <- makeGenStatelessReset-    connRecvDatagramQ <- newTQueueIO-    conn <--        clientConnection-            conf-            verInfo-            myAuthCIDs-            peerAuthCIDs-            debugLog-            qLog-            ccHooks-            sref-            piref-            q-            connRecvDatagramQ-            send-            recv-            genSRT-    setSockConnected conn ccSockConnected-    addResource conn qclean-    modifytPeerParameters conn ccResumption-    let ver = chosenVersion verInfo-    initializeCoder conn InitialLevel $ initialSecrets ver peerCID-    setupCryptoStreams conn -- fixme: cleanup-    -- RFC9000 \S14.2-    -- "In the absence of these mechanisms, QUIC endpoints SHOULD-    -- NOT send datagrams larger than the smallest allowed maximum-    -- datagram size."-    ---    -- Thus use 1200 bytes for minimum packet size.-    let pktSiz0 = fromMaybe (defaultPacketSize peersa) ccPacketSize-        pktSiz = (defaultQUICPacketSize `max` pktSiz0) `min` maximumPacketSize peersa-    setMaxPacketSize conn pktSiz-    setInitialCongestionWindow (connLDCC conn) pktSiz-    setAddressValidated pathInfo-    let reader = readerClient sock conn -- dies when s0 is closed.-    return $ ConnRes conn myAuthCIDs reader+    -- As in 'createServerConnection': this is 'run's bracket acquire, so+    -- nothing releases what it has taken when it throws.  Here that is a+    -- socket as well -- 'clse' does not close it, the closer does, and a+    -- connection that never began has no closer.+    flip E.onException (NS.close sock) $ do+        when ccSockConnected $ NS.connect sock peersa+        q <- newRecvQ+        sref <- newIORef sock+        pathInfo <- newPathInfo peersa+        piref <- newIORef $ PeerInfo pathInfo Nothing+        let send buf siz+                | ccSockConnected = do+                    s <- readIORef sref+                    void $ NS.sendBuf s buf siz+                | otherwise = do+                    s <- readIORef sref+                    PeerInfo pinfo _ <- readIORef piref+                    void $ NS.sendBufTo s buf siz $ peerSockAddr pinfo+            recv = recvClient q+        myCID <- newCID+        -- Creating peer's CIDDB with the temporary CID.  This is+        -- overridden by resetPeerCID later since no sequence number is+        -- assigned to the temporary CID by spec.+        peerCID <- newCID+        now <- getTimeMicrosecond+        (qLog, qclean) <- dirQLogger ccQLog now peerCID "client"+        flip E.onException qclean $ do+            let debugLog msg+                    | ccDebugLog = stdoutLogger msg+                    | otherwise = return ()+            debugLog $ "Original CID: " <> bhow peerCID+            let myAuthCIDs = defaultAuthCIDs{initSrcCID = Just myCID}+                peerAuthCIDs = defaultAuthCIDs{initSrcCID = Just peerCID, origDstCID = Just peerCID}+            genSRT <- makeGenStatelessReset+            connRecvDatagramQ <- newTQueueIO+            conn <-+                clientConnection+                    conf+                    verInfo+                    myAuthCIDs+                    peerAuthCIDs+                    debugLog+                    qLog+                    ccHooks+                    sref+                    piref+                    q+                    connRecvDatagramQ+                    send+                    recv+                    genSRT+            flip E.onException (setDead conn >> freeResources conn) $ do+                setSockConnected conn ccSockConnected+                modifytPeerParameters conn ccResumption+                let ver = chosenVersion verInfo+                initializeCoder conn InitialLevel $ initialSecrets ver peerCID+                setupCryptoStreams conn -- fixme: cleanup+                -- RFC9000 \S14.2+                -- "In the absence of these mechanisms, QUIC endpoints SHOULD+                -- NOT send datagrams larger than the smallest allowed maximum+                -- datagram size."+                --+                -- Thus use 1200 bytes for minimum packet size.+                let pktSiz0 = fromMaybe (defaultPacketSize peersa) ccPacketSize+                    pktSiz = (defaultQUICPacketSize `max` pktSiz0) `min` maximumPacketSize peersa+                setMaxPacketSize conn pktSiz+                setInitialCongestionWindow (connLDCC conn) pktSiz+                setAddressValidated pathInfo+                let reader = readerClient sock conn -- dies when s0 is closed.+                -- Handing the qlog over.  Nothing below can throw, so from here+                -- 'freeResources' is the only releaser.+                addResource conn qclean+                return $ ConnRes conn myAuthCIDs reader  -- | Creating a new socket and execute a path validation --   with a new connection ID. Typically, this is used
Network/QUIC/Closer.hs view
@@ -79,39 +79,46 @@     connected <- getSockConnected conn     -- send     let bufsiz = maximumUdpPayloadSize+    -- Both buffers are taken before anything that can throw, and one action+    -- frees them.  They used to be taken either side of 'encodeCC' and+    -- 'killReaders' with nothing to free them if those threw, and this runs+    -- on the way out of every connection.     sendbuf <- mallocBytes bufsiz-    -- This must be called before freeResourcesin runClient.-    siz <- encodeCC conn sendbuf bufsiz frame-    let send-            | connected = void $ NS.sendBuf sock sendbuf siz-            | otherwise = void $ NS.sendBufTo sock sendbuf siz peersa-    -- recv and clos-    killReaders conn -- client only-    (recv, freeRecvBuf, clos) <-+    mrecvbuf <-         if isServer conn-            then return (void $ connRecv conn, free sendbuf, return ())-            else do-                recvbuf <- mallocBytes bufsiz-                let recv'-                        | connected = void $ NS.recvBuf sock recvbuf bufsiz-                        | otherwise = do-                            (_, sa) <- NS.recvBufFrom sock recvbuf bufsiz-                            when (sa /= peersa) recv'-                    free' = free recvbuf >> free sendbuf-                    clos' = do-                        NS.close sock-                        -- This is just in case.-                        getSocket conn >>= NS.close-                return (recv', free', clos')-    -- hook-    let hook = onCloseCompleted $ connHooks conn-    pto <- getPTO ldcc-    void $ forkFinally (closer conn pto send recv hook) $ \e -> do-        case e of-            Left e' -> connDebugLog conn $ "closure' " <> bhow e'-            Right _ -> return ()-        freeRecvBuf-        clos+            then return Nothing+            else Just <$> mallocBytes bufsiz `E.onException` free sendbuf+    let freeBufs = free sendbuf >> mapM_ free mrecvbuf+    flip E.onException freeBufs $ do+        -- This must be called before freeResourcesin runClient.+        siz <- encodeCC conn sendbuf bufsiz frame+        let send+                | connected = void $ NS.sendBuf sock sendbuf siz+                | otherwise = void $ NS.sendBufTo sock sendbuf siz peersa+        -- recv and clos+        killReaders conn -- client only+        let (recv, clos) = case mrecvbuf of+                Nothing -> (void $ connRecv conn, return ())+                Just recvbuf ->+                    let recv'+                            | connected = void $ NS.recvBuf sock recvbuf bufsiz+                            | otherwise = do+                                (_, sa) <- NS.recvBufFrom sock recvbuf bufsiz+                                when (sa /= peersa) recv'+                        clos' = do+                            NS.close sock+                            -- This is just in case.+                            getSocket conn >>= NS.close+                     in (recv', clos')+        -- hook+        let hook = onCloseCompleted $ connHooks conn+        pto <- getPTO ldcc+        void $ forkFinally (closer conn pto send recv hook) $ \e -> do+            case e of+                Left e' -> connDebugLog conn $ "closure' " <> bhow e'+                Right _ -> return ()+            freeBufs+            clos  encodeCC :: Connection -> Buffer -> BufferSize -> Frame -> IO Int encodeCC conn sendbuf0 bufsiz0 frame = do
Network/QUIC/Connection/Crypto.hs view
@@ -171,11 +171,11 @@     -- comes back from the same call as the ciphertext.  ChaCha20-Poly1305     -- has no equivalent there and takes the path below.     mgcm <- makeGcmEncrypt cipher txPayloadKey txPayloadIV txHeaderKey-    (enc, set, get) <- case mgcm of+    (enc, set, wmask) <- case mgcm of         Just gcm -> return gcm         Nothing -> do-            (s', g') <- makeNiteProtector cipher txHeaderKey-            return (makeNiteEncrypt cipher txPayloadKey txPayloadIV, s', g')+            (s', w') <- makeNiteProtector cipher txHeaderKey+            return (makeNiteEncrypt cipher txPayloadKey txPayloadIV, s', w')     let dec = case makeGcmDecrypt cipher rxPayloadKey rxPayloadIV of             Just d -> d             Nothing -> makeNiteDecrypt cipher rxPayloadKey rxPayloadIV@@ -187,7 +187,7 @@     let protector =             Protector                 { setSample = set-                , getMask = get+                , withMask = wmask                 , unprotect = unp                 }     return (coder, protector)
Network/QUIC/Connection/Types.hs view
@@ -154,7 +154,7 @@  data Protector = Protector     { setSample :: Buffer -> IO ()-    , getMask :: IO Buffer+    , withMask :: WithMask     , unprotect :: Sample -> Mask     } @@ -162,7 +162,7 @@ initialProtector =     Protector         { setSample = \_ -> return ()-        , getMask = return nullPtr+        , withMask = \_ -> return False         , unprotect = \_ -> Mask ""         } 
Network/QUIC/Crypto/Nite.hs view
@@ -27,12 +27,15 @@ import qualified Data.ByteArray as Byte (ByteArrayAccess (..), convert) import qualified Data.ByteString as BS import qualified Data.ByteString.Internal as BS-import Foreign.ForeignPtr (ForeignPtr, mallocForeignPtrBytes, newForeignPtr_, withForeignPtr)-import Foreign.Marshal.Alloc (mallocBytes)+import Foreign.ForeignPtr (+    ForeignPtr,+    mallocForeignPtrBytes,+    newForeignPtr_,+    withForeignPtr,+ ) import Foreign.Marshal.Utils (copyBytes)-import Foreign.Ptr (Ptr, minusPtr, nullPtr, plusPtr)+import Foreign.Ptr (Ptr, castPtr, minusPtr, nullPtr, plusPtr) import Foreign.Storable (peek, poke, pokeByteOff)-import Foreign.Ptr (castPtr) import Network.TLS hiding (Version) import qualified Network.TLS as TLS import Network.TLS.Extra.Cipher@@ -278,11 +281,11 @@  ---------------------------------------------------------------- -makeNiteProtector :: Cipher -> Key -> IO (Buffer -> IO (), IO Buffer)+makeNiteProtector :: Cipher -> Key -> IO (Buffer -> IO (), WithMask) makeNiteProtector cipher key = do     ref <- newIORef nullPtr-    dstbuf <- mallocBytes 32 -- fixme: free-    return (niteSetSample ref, niteGetMask ref samplelen mkMask dstbuf)+    dstfp <- mallocForeignPtrBytes 32+    return (niteSetSample ref, niteWithMask ref samplelen mkMask dstfp)   where     samplelen = 16 -- sampleLength cipher -- fixme     mkMask = protectionMask cipher key@@ -290,15 +293,18 @@ niteSetSample :: IORef Buffer -> Buffer -> IO () niteSetSample = writeIORef -niteGetMask :: IORef Buffer -> Int -> (Sample -> Mask) -> Buffer -> IO Buffer-niteGetMask ref samplelen mkMask dstbuf = do+niteWithMask+    :: IORef Buffer -> Int -> (Sample -> Mask) -> ForeignPtr Word8 -> WithMask+niteWithMask ref samplelen mkMask dstfp act = do     srcbuf <- readIORef ref     sample <- do         fptr <- newForeignPtr_ srcbuf         return $ PS fptr 0 samplelen     let Mask mask = mkMask $ Sample sample-    _len <- copyBS dstbuf mask-    return dstbuf+    withForeignPtr dstfp $ \dstbuf -> do+        _len <- copyBS dstbuf mask+        act dstbuf+    return True  ---------------------------------------------------------------- @@ -355,19 +361,21 @@  -- | The encryption side, with the mask.  The two extra actions are the -- 'Protector' halves: they and the encryption share the buffer the mask is--- written to and the address the sample is taken from.+-- written to and the address the sample is taken from.  The buffer is a+-- 'ForeignPtr' and is held only across the reader handed to 'WithMask', so+-- the collector frees it with the coder. makeGcmEncrypt     :: Cipher     -> Key     -> IV     -> Key-    -> IO (Maybe (NiteEncrypt, Buffer -> IO (), IO Buffer))+    -> IO (Maybe (NiteEncrypt, Buffer -> IO (), WithMask)) makeGcmEncrypt cipher (Key key) iv (Key hpkey) = case gcmKeySize cipher of     Nothing -> return Nothing     Just _ -> case (mctx, mhk) of         (Just ctx, Just hk) -> do             ref <- newIORef nullPtr-            maskBuf <- mallocBytes 16 -- fixme: free+            maskfp <- mallocForeignPtrBytes 16             ivfp <- mallocForeignPtrBytes 12             noncefp <- mallocForeignPtrBytes 12             let IV ivbs = iv@@ -379,18 +387,20 @@                         withForeignPtr ivfp $ \ivp ->                             writeNonce np ivp (fromIntegral pn)                     ok <--                        GCM.encryptWithMask-                            ctx-                            hk-                            (NoncePtr noncefp)-                            ad-                            plaintext-                            16-                            off-                            dst-                            maskBuf+                        withForeignPtr maskfp $ \maskBuf ->+                            GCM.encryptWithMask+                                ctx+                                hk+                                (NoncePtr noncefp)+                                ad+                                plaintext+                                16+                                off+                                dst+                                maskBuf                     return $ if ok then BS.length plaintext + 16 else -1-            return $ Just (enc, writeIORef ref, return maskBuf)+                gcmWithMask act = withForeignPtr maskfp act >> return True+            return $ Just (enc, writeIORef ref, gcmWithMask)         _ -> return Nothing   where     mctx = maybeCryptoError $ GCM.newContext key
Network/QUIC/Crypto/Types.hs view
@@ -9,6 +9,7 @@     AssDat (..),     Sample (..),     Mask (..),+    WithMask,     Nonce (..),     Salt,     Label (..),@@ -38,6 +39,22 @@ newtype Secret = Secret ScrubbedBytes deriving (Eq) newtype AssDat = AssDat ByteString deriving (Eq) newtype Sample = Sample ByteString deriving (Eq)++-- | Running an action on the header protection mask, for as long as the+--   action takes and no longer.+--+-- The mask lives in a buffer the encryption side owns, and the only way to+-- keep the garbage collector from taking that buffer out from under the+-- reader was to allocate it with 'mallocBytes' and never free it -- a leak+-- of 16 or 32 bytes for every coder, which is three to five per connection.+-- Handing the reader in instead of handing the buffer out lets the buffer+-- be a 'ForeignPtr' held across exactly the use, and the collector frees it+-- with the coder.+--+-- 'False' when there is no mask to be had, which is a protector without+-- keys.+type WithMask = (Buffer -> IO ()) -> IO Bool+ newtype Mask = Mask ByteString deriving (Eq) newtype Label = Label ByteString deriving (Eq) newtype Nonce = Nonce ByteString deriving (Eq)
Network/QUIC/Logger.hs view
@@ -1,13 +1,16 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Network.QUIC.Logger (     Builder,     DebugLogger,     bhow,+    dropIfUnwritable,     stdoutLogger,     dirDebugLogger, ) where +import qualified Control.Exception as E import Data.ByteString.Builder (byteString, toLazyByteString) import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy.Char8 as BL@@ -23,8 +26,32 @@ bhow :: Show a => a -> Builder bhow = byteString . C8.pack . show +-- | Running a write that describes a connection, dropping the message if+--   it cannot be written.  Shared with the qlog writer, which is called+--   from the same threads and must be no more able to end them.+--+-- A debug logger must not be able to end the connection it is describing.+-- It is called from the protocol threads, six of which run under nested+-- 'concurrently_' and take the rest down with them, so an exception from+-- the write ends the connection over a line of debug output.  A daemon has+-- no stdout to write to -- it is closed, or a pipe whose reader has gone --+-- and that is what mighty was doing with a debug directory set:+--+--   debug: handshaker: \<stdout\>: hPutBuf: invalid argument (Bad file descriptor)+--   debug: handshaker: \<stdout\>: hPutBuf: resource vanished (Broken pipe)+--+-- The peer saw a handshake that never finished, or, once a connection+-- ending of something in here said so, an INTERNAL_ERROR.  It came and went+-- because a stdout that is not a terminal is block-buffered: most writes+-- only fill the buffer and it is the flush that fails.+--+-- Only 'E.IOException' is dropped.  An asynchronous exception is not one,+-- so cancelling a thread that is logging still cancels it.+dropIfUnwritable :: IO () -> IO ()+dropIfUnwritable action = action `E.catch` \(_ :: E.IOException) -> return ()+ stdoutLogger :: DebugLogger-stdoutLogger b = BL.putStrLn $ toLazyByteString b+stdoutLogger b = dropIfUnwritable $ BL.putStrLn $ toLazyByteString b  dirDebugLogger :: Maybe FilePath -> CID -> IO (DebugLogger, IO ()) dirDebugLogger Nothing _ = do@@ -35,6 +62,6 @@     let file = dir </> (show cid <> ".txt")     (fastlogger, clean) <- newFastLogger1 (LogFileNoRotate file 4096)     let dLog msg = do-            fastlogger (toLogStr msg <> "\n")+            dropIfUnwritable $ fastlogger (toLogStr msg <> "\n")             stdoutLogger msg     return (dLog, clean)
Network/QUIC/Packet/Encode.hs view
@@ -249,14 +249,15 @@     protector <- getProtector conn lvl     setSample protector sampleBeg     len <- encrypt coder cryptoBeg plaintext (AssDat header) pn-    maskBeg <- getMask protector     ---    if len < 0 || maskBeg == nullPtr+    if len < 0         then return (-1, -1)         else do-            -- protecting header-            protectHeader headerBeg pnBeg epnLen maskBeg-            return (packetLen, padLen)+            -- protecting header.  The mask is read where it lives rather+            -- than handed out, so the buffer holding it can be one the+            -- garbage collector owns; 'False' is a protector without keys.+            ok <- withMask protector $ protectHeader headerBeg pnBeg epnLen+            return $ if ok then (packetLen, padLen) else (-1, -1)   where     calcLen cipher lengthOrPNBeg payloadWithoutPaddingSiz = do         let headerLen =
Network/QUIC/QLogger.hs view
@@ -5,6 +5,7 @@     dirQLogger, ) where +import qualified Control.Exception as E import qualified Data.ByteString.Char8 as C8 import System.FilePath import System.Log.FastLogger@@ -30,5 +31,5 @@ dirQLogger (Just dir) tim cid rl = do     let file = dir </> (show cid <> "-" <> C8.unpack rl <> ".qlog")     (fastlogger, clean) <- newFastLogger1 $ LogFileNoRotate file 4096-    qlogger <- newQlogger tim rl cid fastlogger+    qlogger <- newQlogger tim rl cid fastlogger `E.onException` clean     return (qlogger, clean)
Network/QUIC/Qlog.hs view
@@ -29,6 +29,7 @@ import Text.Printf  import Network.QUIC.Imports+import Network.QUIC.Logger (dropIfUnwritable) import Network.QUIC.Parameters import Network.QUIC.Types @@ -331,22 +332,32 @@  type QLogger = QlogMsg -> IO () +-- | A qlog writer.+--+-- The writes are dropped if they cannot be made, for the reason the debug+-- logger's are: this is called from the sender, the receiver and the+-- closer, which are protocol threads under nested 'concurrently_', so an+-- IOException from the write ends the connection the write was describing.+-- A qlog directory is asked for by name, as a debug directory is, and the+-- disk it is on can fill.  A connection with a hole in its qlog is the+-- lesser of the two. newQlogger :: TimeMicrosecond -> ByteString -> CID -> FastLogger -> IO QLogger newQlogger base rl ocid fastLogger = do     let ocid' = toLogStr $ enc16 $ fromCID ocid-    fastLogger $-        "{\"qlog_format\":\"NDJSON\",\"qlog_version\":\"draft-02\",\"title\":\"Haskell quic qlog\",\"trace\":{\"vantage_point\":{\"type\":\""-            <> toLogStr rl-            <> "\"},\"common_fields\":{\"ODCID\":\""-            <> ocid'-            <> "\",\"group_id\":\""-            <> ocid'-            <> "\",\"reference_time\":"-            <> swtim base timeMicrosecond0-            <> "}}}\n"+    dropIfUnwritable $+        fastLogger $+            "{\"qlog_format\":\"NDJSON\",\"qlog_version\":\"draft-02\",\"title\":\"Haskell quic qlog\",\"trace\":{\"vantage_point\":{\"type\":\""+                <> toLogStr rl+                <> "\"},\"common_fields\":{\"ODCID\":\""+                <> ocid'+                <> "\",\"group_id\":\""+                <> ocid'+                <> "\",\"reference_time\":"+                <> swtim base timeMicrosecond0+                <> "}}}\n"     let qlogger qmsg = do             let msg = toLogStrTime qmsg base-            fastLogger msg+            dropIfUnwritable $ fastLogger msg     return qlogger  ----------------------------------------------------------------
Network/QUIC/Server/Run.hs view
@@ -56,16 +56,27 @@             check $ st == Stopped   where     debugLog _msg = return ()+    -- 'setup' is this bracket's acquire, so 'teardown' does not run when it+    -- throws -- and it does throw: a server given two addresses whose second+    -- port is taken leaves the first socket bound and the token manager+    -- thread running, for good.  Each step frees what the steps before it+    -- took, which for a list means each element frees itself.     setup stvar = do         dispatch <- newDispatch conf         let forkConn acc = void $ forkIO (runServer conf server dispatch stvar acc)-        ssas <- mapM serverSocket $ scAddresses conf-        tids <- mapM (runDispatcher dispatch conf stvar forkConn) ssas-        return (dispatch, tids, ssas)+        flip E.onException (clearDispatch dispatch) $ do+            ssas <- openAll $ scAddresses conf+            tids <-+                runAll dispatch conf stvar forkConn ssas+                    `E.onException` mapM_ NS.close ssas+            return (dispatch, tids, ssas)     teardown (dispatch, tids, ssas) = do         clearDispatch dispatch         mapM_ killThread tids         mapM_ NS.close ssas+    openAll [] = return []+    openAll (a : as) =+        E.bracketOnError (serverSocket a) NS.close $ \s -> (s :) <$> openAll as  -- | Running a QUIC server. --   The action is executed with a new connection@@ -82,15 +93,32 @@             check $ st == Stopped   where     debugLog _msg = return ()+    -- As in 'run'.  The sockets are the caller's here, so only the token+    -- manager and the dispatchers are ours to free.     setup stvar = do         dispatch <- newDispatch conf         let forkConn acc = void $ forkIO (runServer conf server dispatch stvar acc)-        tids <- mapM (runDispatcher dispatch conf stvar forkConn) ssas-        return (dispatch, tids)+        flip E.onException (clearDispatch dispatch) $ do+            tids <- runAll dispatch conf stvar forkConn ssas+            return (dispatch, tids)     teardown (dispatch, tids) = do         clearDispatch dispatch         mapM_ killThread tids +-- | Running a dispatcher on each socket, killing the ones already running if+--   a later one cannot be started.+runAll+    :: Dispatch+    -> ServerConfig+    -> TVar ServerState+    -> (Accept -> IO ())+    -> [NS.Socket]+    -> IO [ThreadId]+runAll _ _ _ _ [] = return []+runAll dispatch conf stvar forkConn (s : ss) =+    E.bracketOnError (runDispatcher dispatch conf stvar forkConn s) killThread $+        \t -> (t :) <$> runAll dispatch conf stvar forkConn ss+ -- Typically, ConnectionIsClosed breaks acceptStream. -- And the exception should be ignored. runServer@@ -211,65 +239,78 @@         recv = recvServer accRecvQ     let myCID = fromJust $ initSrcCID accMyAuthCIDs         ocid = fromJust $ origDstCID accMyAuthCIDs-    (qLog, qclean) <- dirQLogger scQLog accTime ocid "server"-    (debugLog, dclean) <- dirDebugLogger scDebugLog ocid-    debugLog $ "Original CID: " <> bhow ocid-    connRecvDatagramQ <- newTQueueIO-    conn <--        serverConnection-            conf-            accVersionInfo-            accMyAuthCIDs-            accPeerAuthCIDs-            debugLog-            qLog-            scHooks-            sref-            piref-            accRecvQ-            connRecvDatagramQ-            send-            recv-            (genStatelessReset dispatch)-    addResource conn qclean-    addResource conn dclean-    let cid = fromMaybe ocid $ retrySrcCID accMyAuthCIDs-        ver = chosenVersion accVersionInfo-    initializeCoder conn InitialLevel $ initialSecrets ver cid-    setupCryptoStreams conn -- fixme: cleanup-    let peersa = accPeerSockAddr-        -- RFC9000 \S14.2-        -- "In the absence of these mechanisms, QUIC endpoints SHOULD-        -- NOT send datagrams larger than the smallest allowed maximum-        -- datagram size."-        ---        -- Thus use 1200 bytes for minimum packet size.-        pktSiz =-            (defaultQUICPacketSize `max` accPacketSize)-                `min` maximumPacketSize peersa-    setMaxPacketSize conn pktSiz-    setInitialCongestionWindow (connLDCC conn) pktSiz-    debugLog $ "Packet size: " <> bhow pktSiz <> " (" <> bhow accPacketSize <> ")"-    when accAddressValidated $ setAddressValidated pathInfo-    ---    let retried = isJust $ retrySrcCID accMyAuthCIDs-    when retried $ do-        qlogRecvInitial conn-        qlogSentRetry conn-    ---    let mgr = tokenMgr dispatch-    setTokenManager conn mgr-    ---    setStopServer conn $ atomically $ writeTVar stvar Stopped-    ---    setRegister conn accRegister accUnregister-    accRegister myCID conn-    addResource conn $ do-        myCIDs <- getMyCIDs conn-        mapM_ accUnregister myCIDs-+    -- Nothing here is under 'runServer's bracket: that bracket's acquire is+    -- this function, so whatever it has taken when it throws is released by+    -- nobody.  It was taking two log files, three 2048-byte buffers and a+    -- registration in the dispatcher, and a connection setup does fail --+    -- 'dirQLogger' says how, and that is what it was doing in the field.     ---    return $ ConnRes conn accMyAuthCIDs undefined+    -- One releaser is in force at a time.  The logs stay this function's own+    -- until the end and are handed to the connection only once nothing is+    -- left that can throw, so 'freeResources' and the handlers here never+    -- both close the same thing.+    (qLog, qclean) <- dirQLogger scQLog accTime ocid "server"+    (debugLog, dclean) <- dirDebugLogger scDebugLog ocid `E.onException` qclean+    flip E.onException (dclean >> qclean) $ do+        debugLog $ "Original CID: " <> bhow ocid+        connRecvDatagramQ <- newTQueueIO+        conn <-+            serverConnection+                conf+                accVersionInfo+                accMyAuthCIDs+                accPeerAuthCIDs+                debugLog+                qLog+                scHooks+                sref+                piref+                accRecvQ+                connRecvDatagramQ+                send+                recv+                (genStatelessReset dispatch)+        flip E.onException (setDead conn >> freeResources conn) $ do+            let cid = fromMaybe ocid $ retrySrcCID accMyAuthCIDs+                ver = chosenVersion accVersionInfo+            initializeCoder conn InitialLevel $ initialSecrets ver cid+            setupCryptoStreams conn -- fixme: cleanup+            let peersa = accPeerSockAddr+                -- RFC9000 \S14.2+                -- "In the absence of these mechanisms, QUIC endpoints SHOULD+                -- NOT send datagrams larger than the smallest allowed maximum+                -- datagram size."+                --+                -- Thus use 1200 bytes for minimum packet size.+                pktSiz =+                    (defaultQUICPacketSize `max` accPacketSize)+                        `min` maximumPacketSize peersa+            setMaxPacketSize conn pktSiz+            setInitialCongestionWindow (connLDCC conn) pktSiz+            debugLog $ "Packet size: " <> bhow pktSiz <> " (" <> bhow accPacketSize <> ")"+            when accAddressValidated $ setAddressValidated pathInfo+            --+            let retried = isJust $ retrySrcCID accMyAuthCIDs+            when retried $ do+                qlogRecvInitial conn+                qlogSentRetry conn+            --+            let mgr = tokenMgr dispatch+            setTokenManager conn mgr+            --+            setStopServer conn $ atomically $ writeTVar stvar Stopped+            --+            setRegister conn accRegister accUnregister+            accRegister myCID conn+            addResource conn $ do+                myCIDs <- getMyCIDs conn+                mapM_ accUnregister myCIDs+            -- Handing the logs over.  Nothing below can throw, so from here+            -- 'freeResources' is the only releaser and the handlers above+            -- have nothing left to free.+            addResource conn qclean+            addResource conn dclean+            return $ ConnRes conn accMyAuthCIDs undefined  afterHandshakeServer :: ServerConfig -> Connection -> IO () afterHandshakeServer ServerConfig{..} conn = handleLogT logAction $ do
quic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               quic-version:            0.3.12+version:            0.3.13 license:            BSD3 license-file:       LICENSE maintainer:         kazu@iij.ad.jp@@ -232,12 +232,14 @@         FrameSpec         HandshakeSpec         IOSpec+        LoggerSpec         PacketSpec         ReassSpec         ParametersSpec         QLoggerSpec         RecoverySpec         ResetSpec+        SetupSpec         TLSSpec         TokenSpec         TransportError@@ -260,6 +262,7 @@         crypto-token,         crypton,         directory,+        fast-logger >=3.2.2 && <3.3,         filepath,         hspec,         network >=3.2.2,
+ test/LoggerSpec.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE OverloadedStrings #-}++module LoggerSpec where++import qualified Control.Exception as E+import Control.Monad (when)+import Data.IORef+import GHC.IO.Handle (hDuplicate, hDuplicateTo)+import System.Directory+import System.FilePath+import System.IO+import System.Log.FastLogger (FastLogger)+import Test.Hspec++import Network.QUIC.Internal++spec :: Spec+spec = do+    -- A debug logger is called from the protocol threads, six of which run+    -- under nested concurrently_ and take the rest down with them, so a+    -- write that throws ends the connection it was describing.  A daemon+    -- has no stdout to write to: mighty's connections were ending on+    --+    --   debug: handshaker: <stdout>: hPutBuf: invalid argument+    --                                (Bad file descriptor)+    --+    -- and the peer saw a handshake that never finished.+    describe "stdoutLogger" $+        it "drops a message stdout cannot take" $+            withUnwritableStdout (stdoutLogger "a line no one can receive")+                `shouldReturn` ()++    describe "dirDebugLogger" $+        -- The file is what the caller asked for, and it is still written+        -- when stdout is gone.+        it "writes the file when stdout cannot be written" $+            withDebugDir $ \dir -> do+                (dLog, clean) <- dirDebugLogger (Just dir) cid+                withUnwritableStdout $ dLog "a line the file can take"+                clean+                readFile (dir </> show cid <> ".txt")+                    `shouldReturn` "a line the file can take\n"+    -- The qlog writer is called from the sender, the receiver and the+    -- closer, the same protocol threads as the debug logger, so it must be+    -- no more able to end them.  A qlog directory is asked for by name, as+    -- a debug directory is, and the disk it is on can fill.+    describe "newQlogger" $ do+        it "does not throw when the header cannot be written" $ do+            tim <- getTimeMicrosecond+            _ <- newQlogger tim "server" cid full+            return ()++        it "drops a message that cannot be written" $ do+            tim <- getTimeMicrosecond+            qlogger <- newQlogger tim "server" cid full+            qlogger (QDebug "a line the disk has no room for" tim)+                `shouldReturn` ()++        -- Only an IOException is dropped.  Nothing else is, and an+        -- asynchronous exception is not one, so cancelling a thread that is+        -- writing a qlog still cancels it.+        it "lets anything that is not an IOException through" $ do+            tim <- getTimeMicrosecond+            -- The header is the first write, and it has to get through for+            -- there to be a logger to try.+            afterTheHeader <- brokenAfter 1+            qlogger <- newQlogger tim "server" cid afterTheHeader+            qlogger (QDebug "not the disk's fault" tim)+                `shouldThrow` errorCall "boom"+  where+    full :: FastLogger+    full _ = E.throwIO $ userError "no space left on device"+    brokenAfter :: Int -> IO FastLogger+    brokenAfter k = do+        ref <- newIORef (0 :: Int)+        return $ \_ -> do+            n <- atomicModifyIORef' ref $ \x -> (x + 1, x)+            when (n >= k) $ E.throwIO $ E.ErrorCall "boom"+    cid = makeCID "\x01\x02\x03\x04\x05\x06\x07\x08"++-- | Running an action with a stdout every write throws on, and putting the+--   real one back afterwards.  stdout is redirected rather than closed:+--   hspec reports through it, and a handle that is only redirected can be+--   restored from the duplicate however the action ends.+withUnwritableStdout :: IO a -> IO a+withUnwritableStdout action = do+    saved <- hDuplicate stdout+    let redirected = withFile "/dev/null" ReadMode $ \h -> do+            hDuplicateTo h stdout+            action `E.finally` hDuplicateTo saved stdout+    redirected `E.finally` hClose saved++withDebugDir :: (FilePath -> IO a) -> IO a+withDebugDir = E.bracket newDir removePathForcibly+  where+    newDir = do+        tmp <- getTemporaryDirectory+        let dir = tmp </> "quic-logger-spec"+        removePathForcibly dir+        createDirectory dir+        return dir
+ test/SetupSpec.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | What a connection or a server setup leaves behind when it fails.+--+-- 'createServerConnection' and 'run's own @setup@ are the acquire of their+-- bracket, so the release does not run when they throw and whatever they+-- have taken is freed by nobody.+--+-- Only the first has a test here.  What @setup@ leaks is a bound UDP socket,+-- and whether a port is still taken is not a portable question: on Linux two+-- UDP sockets with SO_REUSEADDR may hold the same address and port at once,+-- so the bind that is supposed to fail succeeds and the bind that is+-- supposed to succeed says nothing about the leak.  A first attempt at it+-- hung every CI job on the former.+module SetupSpec where++import Control.Concurrent (threadDelay)+import Control.Concurrent.Async+import qualified Control.Exception as E+import System.Directory+import System.FilePath+import System.IO+import Test.Hspec++import Network.QUIC+import qualified Network.QUIC.Client as C+import Network.QUIC.Internal+import Network.QUIC.Server++import Config++spec :: Spec+spec = do+    -- The qlog is opened before the debug log, and a debug log that cannot+    -- be opened is a real failure: a directory that is not there, or the+    -- "openFile: resource busy" of a client and a server in one process+    -- pointed at one directory.+    describe "a server connection setup that fails" $+        it "does not leave the qlog it had already opened open" $+            withTempDir $ \dir -> do+                let qdir = dir </> "qlog"+                createDirectory qdir+                sc0 <- makeTestServerConfig+                let sc =+                        sc0+                            { scQLog = Just qdir+                            , scDebugLog = Just (dir </> "not-a-directory")+                            }+                    cc =+                        testClientConfig+                            { ccParameters =+                                (ccParameters testClientConfig)+                                    { maxIdleTimeout = Milliseconds 1000+                                    }+                            }+                withAsync (run sc $ \_ -> return ()) $ \_ ->+                    C.run cc (\_ -> return ())+                        `shouldThrow` (\(_ :: QUICException) -> True)+                -- If the handle is still open, GHC's own lock on the file+                -- says so.+                files <- listDirectory qdir+                files `shouldNotSatisfy` null+                mapM_ (waitUnlocked . (qdir </>)) files++-- | Waiting for a file to come unlocked, which is to say for the handle on+--   it to be closed.+--+-- The server forks a connection for each Initial it cannot place, and a+-- client that hears nothing retransmits, so when the client gives up there+-- may be an attempt still on its way out holding the file.  Taking that for+-- a leak would be a test that fails when the machine is busy; a leaked+-- handle, on the other hand, is held for the life of the process, so+-- waiting tells the two apart.+waitUnlocked :: FilePath -> IO ()+waitUnlocked file = go (100 :: Int)+  where+    open = withFile file WriteMode $ \_ -> return ()+    go 0 = open -- out of patience: let the lock speak for itself+    go n = do+        r <- E.try open+        case r of+            Right () -> return ()+            Left (_ :: E.IOException) -> threadDelay 20000 >> go (n - 1)++withTempDir :: (FilePath -> IO a) -> IO a+withTempDir = E.bracket newDir removePathForcibly+  where+    newDir = do+        tmp <- getTemporaryDirectory+        let dir = tmp </> "quic-setup-spec"+        removePathForcibly dir+        createDirectory dir+        return dir