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
|
MaxHeaderBytes int
|
||||||
Protocols []string
|
Protocols []string
|
||||||
StrictSNIHost *bool
|
StrictSNIHost *bool
|
||||||
|
TrustedProxies []string
|
||||||
ShouldLogCredentials bool
|
ShouldLogCredentials bool
|
||||||
Metrics *caddyhttp.Metrics
|
Metrics *caddyhttp.Metrics
|
||||||
}
|
}
|
||||||
|
@ -176,6 +177,15 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||||
}
|
}
|
||||||
serverOpts.StrictSNIHost = &boolVal
|
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":
|
case "metrics":
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
|
@ -269,6 +279,7 @@ func applyServerOptions(
|
||||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||||
server.Protocols = opts.Protocols
|
server.Protocols = opts.Protocols
|
||||||
server.StrictSNIHost = opts.StrictSNIHost
|
server.StrictSNIHost = opts.StrictSNIHost
|
||||||
|
server.TrustedProxies = opts.TrustedProxies
|
||||||
server.Metrics = opts.Metrics
|
server.Metrics = opts.Metrics
|
||||||
if opts.ShouldLogCredentials {
|
if opts.ShouldLogCredentials {
|
||||||
if server.Logs == nil {
|
if server.Logs == nil {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
log_credentials
|
log_credentials
|
||||||
protocols h1 h2 h2c h3
|
protocols h1 h2 h2c h3
|
||||||
strict_sni_host
|
strict_sni_host
|
||||||
|
trusted_proxies private_ranges
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +56,14 @@ foo.com {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"strict_sni_host": true,
|
"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": {
|
"logs": {
|
||||||
"should_log_credentials": true
|
"should_log_credentials": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,7 +20,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -222,6 +224,24 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||||
srv.StrictSNIHost = &trueBool
|
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
|
// process each listener address
|
||||||
for i := range srv.Listen {
|
for i := range srv.Listen {
|
||||||
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
|
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
|
||||||
|
|
|
@ -1281,14 +1281,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if d.Val() == "private_ranges" {
|
if d.Val() == "private_ranges" {
|
||||||
m.Ranges = append(m.Ranges, []string{
|
m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
|
||||||
"192.168.0.0/16",
|
|
||||||
"172.16.0.0/12",
|
|
||||||
"10.0.0.0/8",
|
|
||||||
"127.0.0.1/8",
|
|
||||||
"fd00::/8",
|
|
||||||
"::1",
|
|
||||||
}...)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.Ranges = append(m.Ranges, d.Val())
|
m.Ranges = append(m.Ranges, d.Val())
|
||||||
|
|
|
@ -290,8 +290,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
// middleware variables
|
// middleware variables
|
||||||
if strings.HasPrefix(key, varsReplPrefix) {
|
if strings.HasPrefix(key, varsReplPrefix) {
|
||||||
varName := key[len(varsReplPrefix):]
|
varName := key[len(varsReplPrefix):]
|
||||||
tbl := req.Context().Value(VarsCtxKey).(map[string]any)
|
raw := GetVar(req.Context(), varName)
|
||||||
raw := tbl[varName]
|
|
||||||
// variables can be dynamic, so always return true
|
// variables can be dynamic, so always return true
|
||||||
// even when it may not be set; treat as empty then
|
// even when it may not be set; treat as empty then
|
||||||
return raw, true
|
return raw, true
|
||||||
|
|
|
@ -549,14 +549,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
case "trusted_proxies":
|
case "trusted_proxies":
|
||||||
for d.NextArg() {
|
for d.NextArg() {
|
||||||
if d.Val() == "private_ranges" {
|
if d.Val() == "private_ranges" {
|
||||||
h.TrustedProxies = append(h.TrustedProxies, []string{
|
h.TrustedProxies = append(h.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
|
||||||
"192.168.0.0/16",
|
|
||||||
"172.16.0.0/12",
|
|
||||||
"10.0.0.0/8",
|
|
||||||
"127.0.0.1/8",
|
|
||||||
"fd00::/8",
|
|
||||||
"::1",
|
|
||||||
}...)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h.TrustedProxies = append(h.TrustedProxies, d.Val())
|
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
|
// Client IP may contain a zone if IPv6, so we need
|
||||||
// to pull that out before parsing the IP
|
// to pull that out before parsing the IP
|
||||||
if before, _, found := strings.Cut(clientIP, "%"); found {
|
clientIP, _, _ = strings.Cut(clientIP, "%")
|
||||||
clientIP = before
|
|
||||||
}
|
|
||||||
ipAddr, err := netip.ParseAddr(clientIP)
|
ipAddr, err := netip.ParseAddr(clientIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid IP address: '%s': %v", clientIP, err)
|
return fmt.Errorf("invalid IP address: '%s': %v", clientIP, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the client is a trusted proxy
|
// Check if the client is a trusted proxy
|
||||||
trusted := false
|
trusted := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool)
|
||||||
for _, ipRange := range h.trustedProxies {
|
for _, ipRange := range h.trustedProxies {
|
||||||
if ipRange.Contains(ipAddr) {
|
if ipRange.Contains(ipAddr) {
|
||||||
trusted = true
|
trusted = true
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -117,6 +118,18 @@ type Server struct {
|
||||||
// client authentication.
|
// client authentication.
|
||||||
StrictSNIHost *bool `json:"strict_sni_host,omitempty"`
|
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
|
// Enables access logging and configures how access logs are handled
|
||||||
// in this server. To minimally enable access logs, simply set this
|
// in this server. To minimally enable access logs, simply set this
|
||||||
// to a non-null, empty struct.
|
// 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
|
h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create
|
||||||
addresses []caddy.NetworkAddress
|
addresses []caddy.NetworkAddress
|
||||||
|
|
||||||
|
// Holds the parsed CIDR ranges from TrustedProxies
|
||||||
|
trustedProxies []netip.Prefix
|
||||||
|
|
||||||
shutdownAt time.Time
|
shutdownAt time.Time
|
||||||
shutdownAtMu *sync.RWMutex
|
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
|
// set up the context for the request
|
||||||
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
|
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
|
||||||
ctx = context.WithValue(ctx, ServerCtxKey, s)
|
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{}))
|
ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{}))
|
||||||
var url2 url.URL // avoid letting this escape to the heap
|
var url2 url.URL // avoid letting this escape to the heap
|
||||||
ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
|
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
|
// cloneURL makes a copy of r.URL and returns a
|
||||||
// new value that doesn't reference the original.
|
// new value that doesn't reference the original.
|
||||||
func cloneURL(from, to *url.URL) {
|
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.
|
// Context keys for HTTP request context values.
|
||||||
const (
|
const (
|
||||||
// For referencing the server instance
|
// For referencing the server instance
|
||||||
|
@ -727,4 +795,7 @@ const (
|
||||||
// For a partial copy of the unmodified request that
|
// For a partial copy of the unmodified request that
|
||||||
// originally came into the server's entry handler
|
// originally came into the server's entry handler
|
||||||
OriginalRequestCtxKey caddy.CtxKey = "original_request"
|
OriginalRequestCtxKey caddy.CtxKey = "original_request"
|
||||||
|
|
||||||
|
// For tracking whether the client is a trusted proxy
|
||||||
|
TrustedProxyVarKey string = "trusted_proxy"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue