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
)