feat: add tarpit using http headers#126
Conversation
476abe6 to
ad44c1c
Compare
|
Damn, just tested with https and I get the following: I think I may need to approach this from a different way. I think a TLS hijack is not possible since Caddy will handle the TLS connection setup from its side, but when we get to the handler, we don't have the TLS context to write TLS record frames with the data that we want to write. So there is a mismatch and Go's opinion here is that we can't do that. I can't seem to find an approach to do this within a HTTP handler either. ResponseWriter.Write always calls ResponseWriter.WriteHeader, which means the headers are written immediately, so that Write only deals with writing body. If you try to do RW.Headers().{Write,Flush} they both will force a WriteHeader as well if one hasn't been sent. So this makes things kind of annoying. |
Seems fine to use Hijacker in responders. See https://github.com/caddyserver/caddy/blob/4babe4b201eef3e9794851b74f734a03338f7a20/caddyhttp/proxy/reverseproxy.go#L241 I'll try to take a look at this within the next few days. |
Thanks for this, I now have: And it works as intended over https sites. Unfortunately it has to be a global option it seems rather than a per site directive. But I'm happy with that. |
Amazing. I don't think http header tarpitting is possible on h2 given how it compresses frames and multiplexes streams over a single TCP connection - you can't hijack without breaking all other active requests on that connection. For now, forcing if len(protocols) == 0 || contains(protocols, "h2") || contains(protocols, "h2c") {
return fmt.Errorf("header_tarpit requires HTTP/1.1 only - add 'protocols h1' to your server block. See: https://caddy.community/t/help-force-http-1-1-on-a-wild-card/19978")
}That way it's explicit and documented upfront. Besides that + docs this looks good to me. |
This is an attempt to implement http header tarpit per discussion in #123.
This is implemented using the http.Hijacker, it writes the http/response code status header, then any subsequent custom headers specified in the defender config, both are immediately have flushed writes, followed successive tarpit headers interspersed by a custom delay specified in the config (defaults to 4 seconds).
I'm not sure if lifecycle of the connection is handled correctly; for example what if you set the timeout to 900s, and the connection disconnects after say 100s, does the handler have to wait for the timeout, or does the Go know to cancel the goroutine accordingly. However, the cost of holding the connection isn't such a big deal until timeout happens, but it is something that I was a bit iffy about.
Love to hear any feedback. I'm also not sure how to approach testing. Should a test of say a timeout of 5 seconds, with a 2 second per header, and throw in a few custom headers, and verify the transcript:
Example CaddyFile:
And an example run: