@marcel91200 / caddy Mirror · read-only / commits / c3fb5f4d
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 10:45 UTC

caddyhttp: Reject 0-RTT early data in IP matchers and set Early-Data header when proxying (#6427)

* caddyhttp: Reject 0-RTT early data in IP matchers and set Early-Data header when proxying See RFC 8470: https://httpwg.org/specs/rfc8470.html Thanks to Michael Wedl (@MWedl) at the University of Applied Sciences St. Poelten for reporting this. * Don't return value for {remote} placeholder in early data * Add Caddyfile support

Matt Holt committed Jul 5, 2024 at 10:46 UTC c3fb5f4d3fb3eed9136f766cb88f2d8ac54de685
5 files changed +90 -6
listeners.go
-6
@@ -60,8 +60,6 @@ type NetworkAddress struct {
60 // ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
61 // (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
62 // It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
63 -//
64 -// TODO: Experimental API: subject to change or removal.
63 func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
64 var listeners []any
65 var err error
@@ -130,8 +128,6 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig)
128 // Unix sockets will be unlinked before being created, to ensure we can bind to
129 // it even if the previous program using it exited uncleanly; it will also be
130 // unlinked upon a graceful exit (or when a new config does not use that socket).
133 -//
134 -// TODO: Experimental API: subject to change or removal.
131 func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
132 if na.IsUnixNetwork() {
133 unixSocketsMu.Lock()
@@ -221,8 +217,6 @@ func (na NetworkAddress) JoinHostPort(offset uint) string {
217 }
218
219 // Expand returns one NetworkAddress for each port in the port range.
224 -//
225 -// This is EXPERIMENTAL and subject to change or removal.
220 func (na NetworkAddress) Expand() []NetworkAddress {
221 size := na.PortRangeSize()
222 addrs := make([]NetworkAddress, size)
modules/caddyhttp/ip_matchers.go
+6
@@ -143,6 +143,9 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
143
144 // Match returns true if r matches m.
145 func (m MatchRemoteIP) Match(r *http.Request) bool {
146 + if r.TLS != nil && !r.TLS.HandshakeComplete {
147 + return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
148 + }
149 address := r.RemoteAddr
150 clientIP, zoneID, err := parseIPZoneFromString(address)
151 if err != nil {
@@ -228,6 +231,9 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error {
231
232 // Match returns true if r matches m.
233 func (m MatchClientIP) Match(r *http.Request) bool {
234 + if r.TLS != nil && !r.TLS.HandshakeComplete {
235 + return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
236 + }
237 address := GetVar(r.Context(), ClientIPVarKey).(string)
238 clientIP, zoneID, err := parseIPZoneFromString(address)
239 if err != nil {
modules/caddyhttp/matchers.go
+64
@@ -178,6 +178,22 @@ type (
178 // "http/2", "http/3", or minimum versions: "http/2+", etc.
179 MatchProtocol string
180
181 + // MatchTLS matches HTTP requests based on the underlying
182 + // TLS connection state. If this matcher is specified but
183 + // the request did not come over TLS, it will never match.
184 + // If this matcher is specified but is empty and the request
185 + // did come in over TLS, it will always match.
186 + MatchTLS struct {
187 + // Matches if the TLS handshake has completed. QUIC 0-RTT early
188 + // data may arrive before the handshake completes. Generally, it
189 + // is unsafe to replay these requests if they are not idempotent;
190 + // additionally, the remote IP of early data packets can more
191 + // easily be spoofed. It is conventional to respond with HTTP 425
192 + // Too Early if the request cannot risk being processed in this
193 + // state.
194 + HandshakeComplete *bool `json:"handshake_complete,omitempty"`
195 + }
196 +
197 // MatchNot matches requests by negating the results of its matcher
198 // sets. A single "not" matcher takes one or more matcher sets. Each
199 // matcher set is OR'ed; in other words, if any matcher set returns
@@ -213,6 +229,7 @@ func init() {
229 caddy.RegisterModule(MatchHeader{})
230 caddy.RegisterModule(MatchHeaderRE{})
231 caddy.RegisterModule(new(MatchProtocol))
232 + caddy.RegisterModule(MatchTLS{})
233 caddy.RegisterModule(MatchNot{})
234 }
235
@@ -1236,6 +1253,53 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) {
1253 )
1254 }
1255
1256 +// CaddyModule returns the Caddy module information.
1257 +func (MatchTLS) CaddyModule() caddy.ModuleInfo {
1258 + return caddy.ModuleInfo{
1259 + ID: "http.matchers.tls",
1260 + New: func() caddy.Module { return new(MatchTLS) },
1261 + }
1262 +}
1263 +
1264 +// Match returns true if r matches m.
1265 +func (m MatchTLS) Match(r *http.Request) bool {
1266 + if r.TLS == nil {
1267 + return false
1268 + }
1269 + if m.HandshakeComplete != nil {
1270 + if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) ||
1271 + (*m.HandshakeComplete && !r.TLS.HandshakeComplete) {
1272 + return false
1273 + }
1274 + }
1275 + return true
1276 +}
1277 +
1278 +// UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax:
1279 +//
1280 +// ... tls [early_data]
1281 +//
1282 +// EXPERIMENTAL SYNTAX: Subject to change.
1283 +func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1284 + // iterate to merge multiple matchers into one
1285 + for d.Next() {
1286 + if d.NextArg() {
1287 + switch d.Val() {
1288 + case "early_data":
1289 + var false bool
1290 + m.HandshakeComplete = &false
1291 + }
1292 + }
1293 + if d.NextArg() {
1294 + return d.ArgErr()
1295 + }
1296 + if d.NextBlock(0) {
1297 + return d.Err("malformed tls matcher: blocks are not supported yet")
1298 + }
1299 + }
1300 + return nil
1301 +}
1302 +
1303 // CaddyModule returns the Caddy module information.
1304 func (MatchNot) CaddyModule() caddy.ModuleInfo {
1305 return caddy.ModuleInfo{
modules/caddyhttp/replacer.go
+8
@@ -142,8 +142,16 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
142 }
143 return port, true
144 case "http.request.remote":
145 + if req.TLS != nil && !req.TLS.HandshakeComplete {
146 + // without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed
147 + return nil, true
148 + }
149 return req.RemoteAddr, true
150 case "http.request.remote.host":
151 + if req.TLS != nil && !req.TLS.HandshakeComplete {
152 + // without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed
153 + return nil, true
154 + }
155 host, _, err := net.SplitHostPort(req.RemoteAddr)
156 if err != nil {
157 // req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path
modules/caddyhttp/reverseproxy/reverseproxy.go
+12
@@ -605,6 +605,18 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
605 req.Header.Set("User-Agent", "")
606 }
607
608 + // Indicate if request has been conveyed in early data.
609 + // RFC 8470: "An intermediary that forwards a request prior to the
610 + // completion of the TLS handshake with its client MUST send it with
611 + // the Early-Data header field set to “1” (i.e., it adds it if not
612 + // present in the request). An intermediary MUST use the Early-Data
613 + // header field if the request might have been subject to a replay and
614 + // might already have been forwarded by it or another instance
615 + // (see Section 6.2)."
616 + if req.TLS != nil && !req.TLS.HandshakeComplete {
617 + req.Header.Set("Early-Data", "1")
618 + }
619 +
620 reqUpType := upgradeType(req.Header)
621 removeConnectionHeaders(req.Header)
622