packages feed

quic 0.3.16 → 0.3.17

raw patch · 7 files changed

+180/−40 lines, 7 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Network.QUIC.Internal: isPathValidating :: Connection -> IO Bool
- Network.QUIC.Internal: newtype RecvQ
- Network.QUIC.Internal: setMigrationStarted :: Connection -> IO ()
+ Network.QUIC.Internal: claimMigration :: Connection -> IO Bool
+ Network.QUIC.Internal: data RecvQ
+ Network.QUIC.Internal: recvQLimit :: Int
- Network.QUIC.Internal: RecvQ :: TQueue ReceivedPacket -> RecvQ
+ Network.QUIC.Internal: RecvQ :: TQueue ReceivedPacket -> TVar Int -> RecvQ

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # ChangeLog +## 0.3.17++* Security fix: bound what an unauthenticated packet can take.+  [#168](https://github.com/kazu-yamamoto/quic/pull/168)+* Allow crypton 2.2 and the x509 family at 1.10.+ ## 0.3.16  * Stop waiting for a socket to be readable, and run on Windows.
Network/QUIC/Connection/Migration.hs view
@@ -22,8 +22,7 @@     choosePeerCIDForPrivacy,     setPeerStatelessResetToken,     isStatelessRestTokenValid,-    setMigrationStarted,-    isPathValidating,+    claimMigration,     checkResponse,     validatePath,     getMyRetirePriorTo,@@ -378,17 +377,26 @@ setChallenges Connection{..} pathInfo pdat =     atomically $ writeTVar migrationState $ SendChallenge pathInfo pdat -setMigrationStarted :: Connection -> IO ()-setMigrationStarted Connection{..} =-    atomically $ writeTVar migrationState MigrationStarted--isPathValidating :: Connection -> IO Bool-isPathValidating Connection{..} = do-    s <- readTVarIO migrationState+-- | Take on the migration, if no one has it.+--+-- True to the caller that took it, and that caller is then the one to carry+-- it out; False to everyone else, including every later caller until the+-- validation this one starts is finished.+--+-- Test and set together.  Apart, every packet that arrived before the+-- winner had set the state read it as free, and a peer that sends from an+-- address this connection has not seen -- which takes no more than another+-- port -- got a migration for each of those packets rather than one for all+-- of them.+claimMigration :: Connection -> IO Bool+claimMigration Connection{..} = atomically $ do+    s <- readTVar migrationState     case s of-        SendChallenge{} -> return True-        MigrationStarted -> return True-        _ -> return False+        SendChallenge{} -> return False+        MigrationStarted -> return False+        _ -> do+            writeTVar migrationState MigrationStarted+            return True  waitResponse :: Connection -> IO () waitResponse Connection{..} = atomically $ do
Network/QUIC/Server/Reader.hs view
@@ -518,8 +518,20 @@                     mPathInfo <- findPathInfo conn peersa                     case mPathInfo of                         Nothing -> do-                            forkManaged conn $-                                onClientMigration conn dCID peersa cidChanged+                            -- Taken here, before the thread rather than+                            -- inside it, so that a peer sending from an+                            -- address this connection has not seen gets one+                            -- thread for the migration and not one for every+                            -- packet.  'findPathInfo' knows the address in+                            -- use and the one before it and nothing else, so+                            -- a sender that moves its port -- which asks no+                            -- spoofing of it -- is unknown every time, and+                            -- this runs on packets nothing has authenticated+                            -- yet.+                            claimed <- claimMigration conn+                            when claimed $+                                forkManaged conn $+                                    onClientMigration conn dCID peersa cidChanged                         _ -> return ()                     writeRecvQ (connRecvQ conn) $ mkReceivedPacket cpkt tim siz lvl ----------------------------------------------------------------@@ -550,22 +562,20 @@ recvServer :: RecvQ -> IO ReceivedPacket recvServer = readRecvQ +-- | Carry out the migration the caller has claimed with 'claimMigration'. 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+    -- 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)
Network/QUIC/Types/Queue.hs view
@@ -1,19 +1,62 @@ module Network.QUIC.Types.Queue where  import Control.Concurrent.STM+import Control.Monad (when)  import Network.QUIC.Types.Packet -newtype RecvQ = RecvQ (TQueue ReceivedPacket)+-- | How many packets a connection may have waiting to be decrypted.+--+-- A server picks the connection a packet belongs to by the connection ID,+-- which is in the clear, and hands the packet over; whether the packet is+-- worth anything is settled later, in the receiver, by decrypting it.  So+-- anyone who knows a connection ID can make a server queue packets, and+-- they cost the sender less to send than the AEAD that rejects them costs+-- the receiver to refuse.  The queue grows, and before this it grew without+-- a limit: one connection's worth of junk could take all of a server's+-- memory.+--+-- This much is several milliseconds of a receiver's work, far more than the+-- scheduling jitter the queue is there to absorb, and each packet holds the+-- datagram it was cut from, so the queue is worth a few megabytes to a+-- sender rather than everything.+recvQLimit :: Int+recvQLimit = 4096 +data RecvQ = RecvQ (TQueue ReceivedPacket) (TVar Int)+ newRecvQ :: IO RecvQ-newRecvQ = RecvQ <$> newTQueueIO+newRecvQ = RecvQ <$> newTQueueIO <*> newTVarIO 0  readRecvQ :: RecvQ -> IO ReceivedPacket-readRecvQ (RecvQ q) = atomically $ readTQueue q+readRecvQ (RecvQ q qlen) = atomically $ do+    x <- readTQueue q+    modifyTVar' qlen (subtract 1)+    return x +-- | Queue a packet, or drop it if 'recvQLimit' are already waiting.+--+-- What is dropped is the packet that has just arrived rather than one+-- already queued, so that what the connection has taken on is kept and only+-- what is past the limit is lost.  A lost packet is something QUIC is built+-- to recover from.+--+-- Nothing is logged.  The only sender that can reach the limit at any rate+-- is one sending faster than the receiver can refuse what it sends, and a+-- line of log for each of those packets is the same attack by another road. writeRecvQ :: RecvQ -> ReceivedPacket -> IO ()-writeRecvQ (RecvQ q) x = atomically $ writeTQueue q x+writeRecvQ (RecvQ q qlen) x = atomically $ do+    n <- readTVar qlen+    when (n < recvQLimit) $ do+        writeTQueue q x+        writeTVar qlen $! n + 1 +-- | Put back a packet this connection had already taken.+--+-- Not subject to the limit: these come from the packets put off for want of+-- the keys to read them, which the handshake bounds, and dropping one would+-- lose a packet the connection has already accounted for. prependRecvQ :: RecvQ -> ReceivedPacket -> STM ()-prependRecvQ (RecvQ q) = unGetTQueue q+prependRecvQ (RecvQ q qlen) x = do+    unGetTQueue q x+    modifyTVar' qlen (+ 1)
quic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               quic-version:            0.3.16+version:            0.3.17 license:            BSD3 license-file:       LICENSE maintainer:         kazu@iij.ad.jp@@ -132,11 +132,11 @@         bytestring >=0.10,         containers,         crypto-token >=0.2.0 && <0.3,-        crypton >=2.1.0 && <2.2,-        crypton-x509 >=1.9.0 && <1.10,-        crypton-x509-store >=1.9.0 && <1.10,-        crypton-x509-system >=1.9.0 && <1.10,-        crypton-x509-validation >=1.9.0 && <1.10,+        crypton >=2.1.0 && <2.3,+        crypton-x509 >=1.9.0 && <1.11,+        crypton-x509-store >=1.9.0 && <1.11,+        crypton-x509-system >=1.9.0 && <1.11,+        crypton-x509-validation >=1.9.0 && <1.11,         fast-logger >=3.2.8 && <3.3,         filepath,         iproute >=1.7.12 && <1.8,@@ -233,6 +233,7 @@         HandshakeSpec         IOSpec         LoggerSpec+        MigrationSpec         PacketSpec         ReassSpec         ParametersSpec
+ test/MigrationSpec.hs view
@@ -0,0 +1,29 @@+module MigrationSpec where++import Control.Concurrent.Async (mapConcurrently)+import Test.Hspec++import Network.QUIC.Internal++import Config+import PacketSpec (makeConnections)++spec :: Spec+spec = do+    serverConf <- runIO makeTestServerConfig+    -- The dispatcher calls this for every packet that arrives from an+    -- address the connection has not seen, before anything has+    -- authenticated the packet, and 'findPathInfo' knows the address in use+    -- and the one before it and nothing else.  A peer that moves its port+    -- -- which asks no spoofing of it -- is therefore unknown on every+    -- packet it sends.  One of those packets is a migration; the rest are+    -- nothing.  See #167.+    describe "claimMigration" $ do+        it "gives the migration to one caller and refuses the rest" $ do+            (_, serverConn) <- makeConnections serverConf Version1+            rs <- mapConcurrently (const $ claimMigration serverConn) [1 .. 200 :: Int]+            length (filter id rs) `shouldBe` 1+        it "refuses while the validation it started is unfinished" $ do+            (_, serverConn) <- makeConnections serverConf Version1+            claimMigration serverConn `shouldReturn` True+            claimMigration serverConn `shouldReturn` False
test/TypesSpec.hs view
@@ -1,13 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}+ module TypesSpec where +import Control.Concurrent.STM (atomically)+import Control.Monad (replicateM)+import qualified Data.ByteString.Char8 as C8 import Data.List+import qualified System.Timeout as Timeout import Test.Hspec import Test.QuickCheck  import Network.QUIC.Internal +-- | A packet that carries nothing but a number, to tell it from the others.+numbered :: Int -> ReceivedPacket+numbered n =+    mkReceivedPacket+        (CryptPacket (Short (toCID "")) (Crypt 0 (C8.pack $ show n) 0))+        timeMicrosecond0+        1200+        RTT1Level++number :: ReceivedPacket -> Int+number rpkt =+    let CryptPacket _ crypt = rpCryptPacket rpkt+     in read $ C8.unpack $ cryptPacket crypt+ spec :: Spec spec = do+    -- A server queues a packet having read the connection ID, which is in+    -- the clear, and finds out whether the packet is worth anything later,+    -- when the receiver decrypts it.  So the queue has to hold a bound, or+    -- anyone who can see a connection ID can fill it.  See #167.+    describe "RecvQ" $ do+        it "keeps what it has taken and drops what is past the limit" $ do+            q <- newRecvQ+            mapM_ (writeRecvQ q . numbered) [1 .. recvQLimit + 100]+            ns <- replicateM recvQLimit (number <$> readRecvQ q)+            -- The packets kept are the ones that arrived first: what is+            -- dropped is what came after the queue was full, not what the+            -- connection had already taken on.+            ns `shouldBe` [1 .. recvQLimit]+            Timeout.timeout 100000 (readRecvQ q) `shouldReturn` Nothing+        it "takes back a packet it had already taken, full or not" $ do+            q <- newRecvQ+            mapM_ (writeRecvQ q . numbered) [1 .. recvQLimit]+            -- Putting back what was put off for want of keys is not the+            -- same as taking something new on, and the limit does not apply+            -- to it.  Dropping one would lose a packet already accounted+            -- for.+            atomically $ prependRecvQ q $ numbered 0+            number <$> readRecvQ q `shouldReturn` 0     describe "toAckInfo and fromAckInfo" $ do         it "should be dual" $ property $ \xs -> do             let rs =