mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
caddyhttp: Add server-level trusted_proxies
config (#5103)
This commit is contained in:
parent
66ce0c5c63
commit
223cbe3d0b
8 changed files with 117 additions and 23 deletions
|
@ -42,6 +42,7 @@ type serverOptions struct {
|
|||
MaxHeaderBytes int
|
||||
Protocols []string
|
||||
StrictSNIHost *bool
|
||||
TrustedProxies []string
|
||||
ShouldLogCredentials bool
|
||||
Metrics *caddyhttp.Metrics
|
||||
}
|
||||
|
@ -176,6 +177,15 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||
}
|
||||
serverOpts.StrictSNIHost = &boolVal
|
||||
|
||||
case "trusted_proxies":
|
||||
for d.NextArg() {
|
||||
if d.Val() == "private_ranges" {
|
||||
serverOpts.TrustedProxies = append(serverOpts.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
|
||||
continue
|
||||
}
|
||||
serverOpts.TrustedProxies = append(serverOpts.TrustedProxies, d.Val())
|
||||
}
|
||||
|
||||
case "metrics":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
|
@ -269,6 +279,7 @@ func applyServerOptions(
|
|||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||
server.Protocols = opts.Protocols
|
||||
server.StrictSNIHost = opts.StrictSNIHost
|
||||
server.TrustedProxies = opts.TrustedProxies
|
||||
server.Metrics = opts.Metrics
|
||||
if opts.ShouldLogCredentials {
|
||||
if server.Logs == nil {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
log_credentials
|
||||
protocols h1 h2 h2c h3
|
||||
strict_sni_host
|
||||
trusted_proxies private_ranges
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,6 +56,14 @@ foo.com {
|
|||
}
|
||||
],
|
||||
"strict_sni_host": true,
|
||||
"trusted_proxies": [
|
||||
"192.168.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"10.0.0.0/8",
|
||||
"127.0.0.1/8",
|
||||
"fd00::/8",
|
||||
"::1"
|
||||
],
|
||||
"logs": {
|
||||
"should_log_credentials": true
|
||||
},
|
||||
|
|
|
@ -20,7 +20,9 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -222,6 +224,24 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||
srv.StrictSNIHost = &trueBool
|
||||
}
|
||||
|
||||
// parse trusted proxy CIDRs ahead of time
|
||||
for _, str := range srv.TrustedProxies {
|
||||
if strings.Contains(str, "/") {
|
||||
ipNet, err := netip.ParsePrefix(str)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing CIDR expression: '%s': %v", str, err)
|
||||
}
|
||||
srv.trustedProxies = append(srv.trustedProxies, ipNet)
|
||||
} else {
|
||||
ipAddr, err := netip.ParseAddr(str)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid IP address: '%s': %v", str, err)
|
||||
}
|
||||
ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
|
||||
srv.trustedProxies = append(srv.trustedProxies, ipNew)
|
||||
}
|
||||
}
|
||||
|
||||
// process each listener address
|
||||
for i := range srv.Listen {
|
||||
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
|
||||
|
|
|
@ -1281,14 +1281,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||
continue
|
||||
}
|
||||
if d.Val() == "private_ranges" {
|
||||
m.Ranges = append(m.Ranges, []string{
|
||||
"192.168.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"10.0.0.0/8",
|
||||
"127.0.0.1/8",
|
||||
"fd00::/8",
|
||||
"::1",
|
||||
}...)
|
||||
m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
|
||||
continue
|
||||
}
|
||||
m.Ranges = append(m.Ranges, d.Val())
|
||||
|
|
|
@ -290,8 +290,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
|||
// middleware variables
|
||||
if strings.HasPrefix(key, varsReplPrefix) {
|
||||
varName := key[len(varsReplPrefix):]
|
||||
tbl := req.Context().Value(VarsCtxKey).(map[string]any)
|
||||
raw := tbl[varName]
|
||||
raw := GetVar(req.Context(), varName)
|
||||
// variables can be dynamic, so always return true
|
||||
// even when it may not be set; treat as empty then
|
||||
return raw, true
|
||||
|
|
|
@ -549,14 +549,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||
case "trusted_proxies":
|
||||
for d.NextArg() {
|
||||
if d.Val() == "private_ranges" {
|
||||
h.TrustedProxies = append(h.TrustedProxies, []string{
|
||||
"192.168.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"10.0.0.0/8",
|
||||
"127.0.0.1/8",
|
||||
"fd00::/8",
|
||||
"::1",
|
||||
}...)
|
||||
h.TrustedProxies = append(h.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
|
||||
continue
|
||||
}
|
||||
h.TrustedProxies = append(h.TrustedProxies, d.Val())
|
||||
|
|
|
@ -701,16 +701,14 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
|
|||
|
||||
// Client IP may contain a zone if IPv6, so we need
|
||||
// to pull that out before parsing the IP
|
||||
if before, _, found := strings.Cut(clientIP, "%"); found {
|
||||
clientIP = before
|
||||
}
|
||||
clientIP, _, _ = strings.Cut(clientIP, "%")
|
||||
ipAddr, err := netip.ParseAddr(clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid IP address: '%s': %v", clientIP, err)
|
||||
}
|
||||
|
||||
// Check if the client is a trusted proxy
|
||||
trusted := false
|
||||
trusted := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool)
|
||||
for _, ipRange := range h.trustedProxies {
|
||||
if ipRange.Contains(ipAddr) {
|
||||
trusted = true
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
@ -117,6 +118,18 @@ type Server struct {
|
|||
// client authentication.
|
||||
StrictSNIHost *bool `json:"strict_sni_host,omitempty"`
|
||||
|
||||
// A list of IP ranges (supports CIDR notation) from which
|
||||
// requests should be trusted. By default, no proxies are
|
||||
// trusted.
|
||||
//
|
||||
// On its own, this configuration will not do anything,
|
||||
// but it can be used as a default set of ranges for
|
||||
// handlers or matchers in routes to pick up, instead
|
||||
// of needing to configure each of them. See the
|
||||
// `reverse_proxy` handler for example, which uses this
|
||||
// to trust sensitive incoming `X-Forwarded-*` headers.
|
||||
TrustedProxies []string `json:"trusted_proxies,omitempty"`
|
||||
|
||||
// Enables access logging and configures how access logs are handled
|
||||
// in this server. To minimally enable access logs, simply set this
|
||||
// to a non-null, empty struct.
|
||||
|
@ -175,6 +188,9 @@ type Server struct {
|
|||
h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create
|
||||
addresses []caddy.NetworkAddress
|
||||
|
||||
// Holds the parsed CIDR ranges from TrustedProxies
|
||||
trustedProxies []netip.Prefix
|
||||
|
||||
shutdownAt time.Time
|
||||
shutdownAtMu *sync.RWMutex
|
||||
|
||||
|
@ -675,7 +691,9 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
|
|||
// set up the context for the request
|
||||
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
|
||||
ctx = context.WithValue(ctx, ServerCtxKey, s)
|
||||
ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
|
||||
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
|
||||
TrustedProxyVarKey: determineTrustedProxy(r, s),
|
||||
})
|
||||
ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{}))
|
||||
var url2 url.URL // avoid letting this escape to the heap
|
||||
ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
|
||||
|
@ -705,6 +723,43 @@ func originalRequest(req *http.Request, urlCopy *url.URL) http.Request {
|
|||
}
|
||||
}
|
||||
|
||||
// determineTrustedProxy parses the remote IP address of
|
||||
// the request, and determines (if the server configured it)
|
||||
// if the client is a trusted proxy.
|
||||
func determineTrustedProxy(r *http.Request, s *Server) bool {
|
||||
// If there's no server, then we can't check anything
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the remote IP, ignore the error as non-fatal,
|
||||
// but the remote IP is required to continue, so we
|
||||
// just return early. This should probably never happen
|
||||
// though, unless some other module manipulated the request's
|
||||
// remote address and used an invalid value.
|
||||
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Client IP may contain a zone if IPv6, so we need
|
||||
// to pull that out before parsing the IP
|
||||
clientIP, _, _ = strings.Cut(clientIP, "%")
|
||||
ipAddr, err := netip.ParseAddr(clientIP)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the client is a trusted proxy
|
||||
for _, ipRange := range s.trustedProxies {
|
||||
if ipRange.Contains(ipAddr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// cloneURL makes a copy of r.URL and returns a
|
||||
// new value that doesn't reference the original.
|
||||
func cloneURL(from, to *url.URL) {
|
||||
|
@ -716,6 +771,19 @@ func cloneURL(from, to *url.URL) {
|
|||
}
|
||||
}
|
||||
|
||||
// PrivateRangesCIDR returns a list of private CIDR range
|
||||
// strings, which can be used as a configuration shortcut.
|
||||
func PrivateRangesCIDR() []string {
|
||||
return []string{
|
||||
"192.168.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"10.0.0.0/8",
|
||||
"127.0.0.1/8",
|
||||
"fd00::/8",
|
||||
"::1",
|
||||
}
|
||||
}
|
||||
|
||||
// Context keys for HTTP request context values.
|
||||
const (
|
||||
// For referencing the server instance
|
||||
|
@ -727,4 +795,7 @@ const (
|
|||
// For a partial copy of the unmodified request that
|
||||
// originally came into the server's entry handler
|
||||
OriginalRequestCtxKey caddy.CtxKey = "original_request"
|
||||
|
||||
// For tracking whether the client is a trusted proxy
|
||||
TrustedProxyVarKey string = "trusted_proxy"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue