mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
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
This commit is contained in:
parent
15d986e1c9
commit
c3fb5f4d3f
5 changed files with 90 additions and 6 deletions
|
@ -60,8 +60,6 @@ type NetworkAddress struct {
|
||||||
// ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
|
// ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
|
||||||
// (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
|
// (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
|
||||||
// It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
|
// It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
|
||||||
//
|
|
||||||
// TODO: Experimental API: subject to change or removal.
|
|
||||||
func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
|
func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
|
||||||
var listeners []any
|
var listeners []any
|
||||||
var err error
|
var err error
|
||||||
|
@ -130,8 +128,6 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig)
|
||||||
// Unix sockets will be unlinked before being created, to ensure we can bind to
|
// Unix sockets will be unlinked before being created, to ensure we can bind to
|
||||||
// it even if the previous program using it exited uncleanly; it will also be
|
// it even if the previous program using it exited uncleanly; it will also be
|
||||||
// unlinked upon a graceful exit (or when a new config does not use that socket).
|
// unlinked upon a graceful exit (or when a new config does not use that socket).
|
||||||
//
|
|
||||||
// TODO: Experimental API: subject to change or removal.
|
|
||||||
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
||||||
if na.IsUnixNetwork() {
|
if na.IsUnixNetwork() {
|
||||||
unixSocketsMu.Lock()
|
unixSocketsMu.Lock()
|
||||||
|
@ -221,8 +217,6 @@ func (na NetworkAddress) JoinHostPort(offset uint) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand returns one NetworkAddress for each port in the port range.
|
// Expand returns one NetworkAddress for each port in the port range.
|
||||||
//
|
|
||||||
// This is EXPERIMENTAL and subject to change or removal.
|
|
||||||
func (na NetworkAddress) Expand() []NetworkAddress {
|
func (na NetworkAddress) Expand() []NetworkAddress {
|
||||||
size := na.PortRangeSize()
|
size := na.PortRangeSize()
|
||||||
addrs := make([]NetworkAddress, size)
|
addrs := make([]NetworkAddress, size)
|
||||||
|
|
|
@ -143,6 +143,9 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchRemoteIP) Match(r *http.Request) bool {
|
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
|
address := r.RemoteAddr
|
||||||
clientIP, zoneID, err := parseIPZoneFromString(address)
|
clientIP, zoneID, err := parseIPZoneFromString(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -228,6 +231,9 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error {
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchClientIP) Match(r *http.Request) bool {
|
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)
|
address := GetVar(r.Context(), ClientIPVarKey).(string)
|
||||||
clientIP, zoneID, err := parseIPZoneFromString(address)
|
clientIP, zoneID, err := parseIPZoneFromString(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -178,6 +178,22 @@ type (
|
||||||
// "http/2", "http/3", or minimum versions: "http/2+", etc.
|
// "http/2", "http/3", or minimum versions: "http/2+", etc.
|
||||||
MatchProtocol string
|
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
|
// MatchNot matches requests by negating the results of its matcher
|
||||||
// sets. A single "not" matcher takes one or more matcher sets. Each
|
// 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
|
// matcher set is OR'ed; in other words, if any matcher set returns
|
||||||
|
@ -213,6 +229,7 @@ func init() {
|
||||||
caddy.RegisterModule(MatchHeader{})
|
caddy.RegisterModule(MatchHeader{})
|
||||||
caddy.RegisterModule(MatchHeaderRE{})
|
caddy.RegisterModule(MatchHeaderRE{})
|
||||||
caddy.RegisterModule(new(MatchProtocol))
|
caddy.RegisterModule(new(MatchProtocol))
|
||||||
|
caddy.RegisterModule(MatchTLS{})
|
||||||
caddy.RegisterModule(MatchNot{})
|
caddy.RegisterModule(MatchNot{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1236,6 +1253,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.
|
// CaddyModule returns the Caddy module information.
|
||||||
func (MatchNot) CaddyModule() caddy.ModuleInfo {
|
func (MatchNot) CaddyModule() caddy.ModuleInfo {
|
||||||
return caddy.ModuleInfo{
|
return caddy.ModuleInfo{
|
||||||
|
|
|
@ -142,8 +142,16 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
}
|
}
|
||||||
return port, true
|
return port, true
|
||||||
case "http.request.remote":
|
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
|
return req.RemoteAddr, true
|
||||||
case "http.request.remote.host":
|
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)
|
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path
|
// req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path
|
||||||
|
|
|
@ -605,6 +605,18 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
|
||||||
req.Header.Set("User-Agent", "")
|
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)
|
reqUpType := upgradeType(req.Header)
|
||||||
removeConnectionHeaders(req.Header)
|
removeConnectionHeaders(req.Header)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue