@marcel91200 / caddy Mirror · read-only / commits / 223cbe3d
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 13, 06:45 UTC

caddyhttp: Add server-level `trusted_proxies` config (#5103)

Francis Lavoie committed Jan 10, 2023 at 00:08 UTC 223cbe3d0b50487117c785f0755bb80a9ee65010
8 files changed +117 -23
caddyconfig/httpcaddyfile/serveroptions.go
+11
@@ -42,6 +42,7 @@ type serverOptions struct {
42 MaxHeaderBytes int
43 Protocols []string
44 StrictSNIHost *bool
45 + TrustedProxies []string
46 ShouldLogCredentials bool
47 Metrics *caddyhttp.Metrics
48 }
@@ -176,6 +177,15 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
177 }
178 serverOpts.StrictSNIHost = &boolVal
179
180 + case "trusted_proxies":
181 + for d.NextArg() {
182 + if d.Val() == "private_ranges" {
183 + serverOpts.TrustedProxies = append(serverOpts.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
184 + continue
185 + }
186 + serverOpts.TrustedProxies = append(serverOpts.TrustedProxies, d.Val())
187 + }
188 +
189 case "metrics":
190 if d.NextArg() {
191 return nil, d.ArgErr()
@@ -269,6 +279,7 @@ func applyServerOptions(
279 server.MaxHeaderBytes = opts.MaxHeaderBytes
280 server.Protocols = opts.Protocols
281 server.StrictSNIHost = opts.StrictSNIHost
282 + server.TrustedProxies = opts.TrustedProxies
283 server.Metrics = opts.Metrics
284 if opts.ShouldLogCredentials {
285 if server.Logs == nil {
caddytest/integration/caddyfile_adapt/global_server_options_single.txt
+9
@@ -14,6 +14,7 @@
14 log_credentials
15 protocols h1 h2 h2c h3
16 strict_sni_host
17 + trusted_proxies private_ranges
18 }
19 }
20
@@ -55,6 +56,14 @@ foo.com {
56 }
57 ],
58 "strict_sni_host": true,
59 + "trusted_proxies": [
60 + "192.168.0.0/16",
61 + "172.16.0.0/12",
62 + "10.0.0.0/8",
63 + "127.0.0.1/8",
64 + "fd00::/8",
65 + "::1"
66 + ],
67 "logs": {
68 "should_log_credentials": true
69 },
modules/caddyhttp/app.go
+20
@@ -20,7 +20,9 @@ import (
20 "fmt"
21 "net"
22 "net/http"
23 + "net/netip"
24 "strconv"
25 + "strings"
26 "sync"
27 "time"
28
@@ -222,6 +224,24 @@ func (app *App) Provision(ctx caddy.Context) error {
224 srv.StrictSNIHost = &trueBool
225 }
226
227 + // parse trusted proxy CIDRs ahead of time
228 + for _, str := range srv.TrustedProxies {
229 + if strings.Contains(str, "/") {
230 + ipNet, err := netip.ParsePrefix(str)
231 + if err != nil {
232 + return fmt.Errorf("parsing CIDR expression: '%s': %v", str, err)
233 + }
234 + srv.trustedProxies = append(srv.trustedProxies, ipNet)
235 + } else {
236 + ipAddr, err := netip.ParseAddr(str)
237 + if err != nil {
238 + return fmt.Errorf("invalid IP address: '%s': %v", str, err)
239 + }
240 + ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
241 + srv.trustedProxies = append(srv.trustedProxies, ipNew)
242 + }
243 + }
244 +
245 // process each listener address
246 for i := range srv.Listen {
247 lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
modules/caddyhttp/matchers.go
+1 -8
@@ -1281,14 +1281,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1281 continue
1282 }
1283 if d.Val() == "private_ranges" {
1284 - m.Ranges = append(m.Ranges, []string{
1285 - "192.168.0.0/16",
1286 - "172.16.0.0/12",
1287 - "10.0.0.0/8",
1288 - "127.0.0.1/8",
1289 - "fd00::/8",
1290 - "::1",
1291 - }...)
1284 + m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
1285 continue
1286 }
1287 m.Ranges = append(m.Ranges, d.Val())
modules/caddyhttp/replacer.go
+1 -2
@@ -290,8 +290,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
290 // middleware variables
291 if strings.HasPrefix(key, varsReplPrefix) {
292 varName := key[len(varsReplPrefix):]
293 - tbl := req.Context().Value(VarsCtxKey).(map[string]any)
294 - raw := tbl[varName]
293 + raw := GetVar(req.Context(), varName)
294 // variables can be dynamic, so always return true
295 // even when it may not be set; treat as empty then
296 return raw, true
modules/caddyhttp/reverseproxy/caddyfile.go
+1 -8
@@ -549,14 +549,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
549 case "trusted_proxies":
550 for d.NextArg() {
551 if d.Val() == "private_ranges" {
552 - h.TrustedProxies = append(h.TrustedProxies, []string{
553 - "192.168.0.0/16",
554 - "172.16.0.0/12",
555 - "10.0.0.0/8",
556 - "127.0.0.1/8",
557 - "fd00::/8",
558 - "::1",
559 - }...)
552 + h.TrustedProxies = append(h.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
553 continue
554 }
555 h.TrustedProxies = append(h.TrustedProxies, d.Val())
modules/caddyhttp/reverseproxy/reverseproxy.go
+2 -4
@@ -701,16 +701,14 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
701
702 // Client IP may contain a zone if IPv6, so we need
703 // to pull that out before parsing the IP
704 - if before, _, found := strings.Cut(clientIP, "%"); found {
705 - clientIP = before
706 - }
704 + clientIP, _, _ = strings.Cut(clientIP, "%")
705 ipAddr, err := netip.ParseAddr(clientIP)
706 if err != nil {
707 return fmt.Errorf("invalid IP address: '%s': %v", clientIP, err)
708 }
709
710 // Check if the client is a trusted proxy
713 - trusted := false
711 + trusted := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool)
712 for _, ipRange := range h.trustedProxies {
713 if ipRange.Contains(ipAddr) {
714 trusted = true
modules/caddyhttp/server.go
+72 -1
@@ -21,6 +21,7 @@ import (
21 "fmt"
22 "net"
23 "net/http"
24 + "net/netip"
25 "net/url"
26 "runtime"
27 "strings"
@@ -117,6 +118,18 @@ type Server struct {
118 // client authentication.
119 StrictSNIHost *bool `json:"strict_sni_host,omitempty"`
120
121 + // A list of IP ranges (supports CIDR notation) from which
122 + // requests should be trusted. By default, no proxies are
123 + // trusted.
124 + //
125 + // On its own, this configuration will not do anything,
126 + // but it can be used as a default set of ranges for
127 + // handlers or matchers in routes to pick up, instead
128 + // of needing to configure each of them. See the
129 + // `reverse_proxy` handler for example, which uses this
130 + // to trust sensitive incoming `X-Forwarded-*` headers.
131 + TrustedProxies []string `json:"trusted_proxies,omitempty"`
132 +
133 // Enables access logging and configures how access logs are handled
134 // in this server. To minimally enable access logs, simply set this
135 // to a non-null, empty struct.
@@ -175,6 +188,9 @@ type Server struct {
188 h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create
189 addresses []caddy.NetworkAddress
190
191 + // Holds the parsed CIDR ranges from TrustedProxies
192 + trustedProxies []netip.Prefix
193 +
194 shutdownAt time.Time
195 shutdownAtMu *sync.RWMutex
196
@@ -675,7 +691,9 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
691 // set up the context for the request
692 ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
693 ctx = context.WithValue(ctx, ServerCtxKey, s)
678 - ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
694 + ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
695 + TrustedProxyVarKey: determineTrustedProxy(r, s),
696 + })
697 ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{}))
698 var url2 url.URL // avoid letting this escape to the heap
699 ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
@@ -705,6 +723,43 @@ func originalRequest(req *http.Request, urlCopy *url.URL) http.Request {
723 }
724 }
725
726 +// determineTrustedProxy parses the remote IP address of
727 +// the request, and determines (if the server configured it)
728 +// if the client is a trusted proxy.
729 +func determineTrustedProxy(r *http.Request, s *Server) bool {
730 + // If there's no server, then we can't check anything
731 + if s == nil {
732 + return false
733 + }
734 +
735 + // Parse the remote IP, ignore the error as non-fatal,
736 + // but the remote IP is required to continue, so we
737 + // just return early. This should probably never happen
738 + // though, unless some other module manipulated the request's
739 + // remote address and used an invalid value.
740 + clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
741 + if err != nil {
742 + return false
743 + }
744 +
745 + // Client IP may contain a zone if IPv6, so we need
746 + // to pull that out before parsing the IP
747 + clientIP, _, _ = strings.Cut(clientIP, "%")
748 + ipAddr, err := netip.ParseAddr(clientIP)
749 + if err != nil {
750 + return false
751 + }
752 +
753 + // Check if the client is a trusted proxy
754 + for _, ipRange := range s.trustedProxies {
755 + if ipRange.Contains(ipAddr) {
756 + return true
757 + }
758 + }
759 +
760 + return false
761 +}
762 +
763 // cloneURL makes a copy of r.URL and returns a
764 // new value that doesn't reference the original.
765 func cloneURL(from, to *url.URL) {
@@ -716,6 +771,19 @@ func cloneURL(from, to *url.URL) {
771 }
772 }
773
774 +// PrivateRangesCIDR returns a list of private CIDR range
775 +// strings, which can be used as a configuration shortcut.
776 +func PrivateRangesCIDR() []string {
777 + return []string{
778 + "192.168.0.0/16",
779 + "172.16.0.0/12",
780 + "10.0.0.0/8",
781 + "127.0.0.1/8",
782 + "fd00::/8",
783 + "::1",
784 + }
785 +}
786 +
787 // Context keys for HTTP request context values.
788 const (
789 // For referencing the server instance
@@ -727,4 +795,7 @@ const (
795 // For a partial copy of the unmodified request that
796 // originally came into the server's entry handler
797 OriginalRequestCtxKey caddy.CtxKey = "original_request"
798 +
799 + // For tracking whether the client is a trusted proxy
800 + TrustedProxyVarKey string = "trusted_proxy"
801 )