diff options
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r-- | modules/caddyhttp/ip_matchers.go | 6 | ||||
-rw-r--r-- | modules/caddyhttp/matchers.go | 64 | ||||
-rw-r--r-- | modules/caddyhttp/replacer.go | 8 | ||||
-rw-r--r-- | modules/caddyhttp/reverseproxy/reverseproxy.go | 12 |
4 files changed, 90 insertions, 0 deletions
diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go index baa7c51ce..9101a0357 100644 --- a/modules/caddyhttp/ip_matchers.go +++ b/modules/caddyhttp/ip_matchers.go @@ -143,6 +143,9 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchRemoteIP) Match(r *http.Request) bool { + if r.TLS != nil && !r.TLS.HandshakeComplete { + return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed + } address := r.RemoteAddr clientIP, zoneID, err := parseIPZoneFromString(address) if err != nil { @@ -228,6 +231,9 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchClientIP) Match(r *http.Request) bool { + if r.TLS != nil && !r.TLS.HandshakeComplete { + return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed + } address := GetVar(r.Context(), ClientIPVarKey).(string) clientIP, zoneID, err := parseIPZoneFromString(address) if err != nil { diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 392312b6c..b7952ab69 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -178,6 +178,22 @@ type ( // "http/2", "http/3", or minimum versions: "http/2+", etc. MatchProtocol string + // MatchTLS matches HTTP requests based on the underlying + // TLS connection state. If this matcher is specified but + // the request did not come over TLS, it will never match. + // If this matcher is specified but is empty and the request + // did come in over TLS, it will always match. + MatchTLS struct { + // Matches if the TLS handshake has completed. QUIC 0-RTT early + // data may arrive before the handshake completes. Generally, it + // is unsafe to replay these requests if they are not idempotent; + // additionally, the remote IP of early data packets can more + // easily be spoofed. It is conventional to respond with HTTP 425 + // Too Early if the request cannot risk being processed in this + // state. + HandshakeComplete *bool `json:"handshake_complete,omitempty"` + } + // MatchNot matches requests by negating the results of its matcher // sets. A single "not" matcher takes one or more matcher sets. Each // matcher set is OR'ed; in other words, if any matcher set returns @@ -213,6 +229,7 @@ func init() { caddy.RegisterModule(MatchHeader{}) caddy.RegisterModule(MatchHeaderRE{}) caddy.RegisterModule(new(MatchProtocol)) + caddy.RegisterModule(MatchTLS{}) caddy.RegisterModule(MatchNot{}) } @@ -1237,6 +1254,53 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) { } // CaddyModule returns the Caddy module information. +func (MatchTLS) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.matchers.tls", + New: func() caddy.Module { return new(MatchTLS) }, + } +} + +// Match returns true if r matches m. +func (m MatchTLS) Match(r *http.Request) bool { + if r.TLS == nil { + return false + } + if m.HandshakeComplete != nil { + if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) || + (*m.HandshakeComplete && !r.TLS.HandshakeComplete) { + return false + } + } + return true +} + +// UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax: +// +// ... tls [early_data] +// +// EXPERIMENTAL SYNTAX: Subject to change. +func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one + for d.Next() { + if d.NextArg() { + switch d.Val() { + case "early_data": + var false bool + m.HandshakeComplete = &false + } + } + if d.NextArg() { + return d.ArgErr() + } + if d.NextBlock(0) { + return d.Err("malformed tls matcher: blocks are not supported yet") + } + } + return nil +} + +// CaddyModule returns the Caddy module information. func (MatchNot) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.matchers.not", diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 1cf3ec474..2c0f32357 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -142,8 +142,16 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } return port, true case "http.request.remote": + if req.TLS != nil && !req.TLS.HandshakeComplete { + // without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed + return nil, true + } return req.RemoteAddr, true case "http.request.remote.host": + if req.TLS != nil && !req.TLS.HandshakeComplete { + // without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed + return nil, true + } host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { // req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 1a559e5dd..4f97edead 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -605,6 +605,18 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. req.Header.Set("User-Agent", "") } + // Indicate if request has been conveyed in early data. + // RFC 8470: "An intermediary that forwards a request prior to the + // completion of the TLS handshake with its client MUST send it with + // the Early-Data header field set to “1” (i.e., it adds it if not + // present in the request). An intermediary MUST use the Early-Data + // header field if the request might have been subject to a replay and + // might already have been forwarded by it or another instance + // (see Section 6.2)." + if req.TLS != nil && !req.TLS.HandshakeComplete { + req.Header.Set("Early-Data", "1") + } + reqUpType := upgradeType(req.Header) removeConnectionHeaders(req.Header) |