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, 05:30 UTC
caddyhttp: Pluggable trusted proxy IP range sources (#5328)
* caddyhttp: Pluggable trusted proxy IP range sources * Add request to the IPRangeSource interface
Francis Lavoie committed
Feb 6, 2023 at 14:44 UTC
12bcbe2c4924ecbf6730fc340a7a4250bddcc9be
6 files changed
+188
-55
caddyconfig/httpcaddyfile/serveroptions.go
+20
-8
@@ -43,7 +43,7 @@ type serverOptions struct {
43
MaxHeaderBytes int
44
Protocols []string
45
StrictSNIHost *bool
46
- TrustedProxies []string
46
+ TrustedProxiesRaw json.RawMessage
47
ShouldLogCredentials bool
48
Metrics *caddyhttp.Metrics
49
}
@@ -188,13 +188,25 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
188
serverOpts.StrictSNIHost = &boolVal
189
190
case "trusted_proxies":
191
- for d.NextArg() {
192
- if d.Val() == "private_ranges" {
193
- serverOpts.TrustedProxies = append(serverOpts.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
194
- continue
195
- }
196
- serverOpts.TrustedProxies = append(serverOpts.TrustedProxies, d.Val())
191
+ if !d.NextArg() {
192
+ return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument")
193
+ }
194
+ modID := "http.ip_sources." + d.Val()
195
+ unm, err := caddyfile.UnmarshalModule(d, modID)
196
+ if err != nil {
197
+ return nil, err
198
+ }
199
+ source, ok := unm.(caddyhttp.IPRangeSource)
200
+ if !ok {
201
+ return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm)
202
}
203
+ jsonSource := caddyconfig.JSONModuleObject(
204
+ source,
205
+ "source",
206
+ source.(caddy.Module).CaddyModule().ID.Name(),
207
+ nil,
208
+ )
209
+ serverOpts.TrustedProxiesRaw = jsonSource
210
211
case "metrics":
212
if d.NextArg() {
@@ -304,7 +316,7 @@ func applyServerOptions(
316
server.MaxHeaderBytes = opts.MaxHeaderBytes
317
server.Protocols = opts.Protocols
318
server.StrictSNIHost = opts.StrictSNIHost
307
- server.TrustedProxies = opts.TrustedProxies
319
+ server.TrustedProxiesRaw = opts.TrustedProxiesRaw
320
server.Metrics = opts.Metrics
321
if opts.ShouldLogCredentials {
322
if server.Logs == nil {
caddytest/integration/caddyfile_adapt/global_server_options_single.txt
+12
-9
@@ -14,7 +14,7 @@
14
log_credentials
15
protocols h1 h2 h2c h3
16
strict_sni_host
17
- trusted_proxies private_ranges
17
+ trusted_proxies static private_ranges
18
}
19
}
20
@@ -56,14 +56,17 @@ 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
- ],
59
+ "trusted_proxies": {
60
+ "ranges": [
61
+ "192.168.0.0/16",
62
+ "172.16.0.0/12",
63
+ "10.0.0.0/8",
64
+ "127.0.0.1/8",
65
+ "fd00::/8",
66
+ "::1"
67
+ ],
68
+ "source": "static"
69
+ },
70
"logs": {
71
"should_log_credentials": true
72
},
caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt
+1
-3
@@ -1,4 +1,3 @@
1
-
1
https://example.com {
2
reverse_proxy /path https://localhost:54321 {
3
header_up Host {upstream_hostport}
@@ -24,13 +23,12 @@ https://example.com {
23
max_conns_per_host 5
24
keepalive_idle_conns_per_host 2
25
keepalive_interval 30s
27
-
26
+
27
tls_renegotiation freely
28
tls_except_ports 8181 8182
29
}
30
}
31
}
33
-
32
----------
33
{
34
"apps": {
modules/caddyhttp/app.go
+6
-17
@@ -20,9 +20,7 @@ import (
20
"fmt"
21
"net"
22
"net/http"
23
- "net/netip"
23
"strconv"
25
- "strings"
24
"sync"
25
"time"
26
@@ -224,22 +222,13 @@ func (app *App) Provision(ctx caddy.Context) error {
222
srv.StrictSNIHost = &trueBool
223
}
224
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)
225
+ // set up the trusted proxies source
226
+ for srv.TrustedProxiesRaw != nil {
227
+ val, err := ctx.LoadModule(srv, "TrustedProxiesRaw")
228
+ if err != nil {
229
+ return fmt.Errorf("loading trusted proxies modules: %v", err)
230
}
231
+ srv.trustedProxies = val.(IPRangeSource)
232
}
233
234
// process each listener address
modules/caddyhttp/ip_range.go
new
+142
@@ -0,0 +1,142 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyhttp
16
+
17
+import (
18
+ "fmt"
19
+ "net/http"
20
+ "net/netip"
21
+ "strings"
22
+
23
+ "github.com/caddyserver/caddy/v2"
24
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
25
+)
26
+
27
+func init() {
28
+ caddy.RegisterModule(StaticIPRange{})
29
+}
30
+
31
+// IPRangeSource gets a list of IP ranges.
32
+//
33
+// The request is passed as an argument to allow plugin implementations
34
+// to have more flexibility. But, a plugin MUST NOT modify the request.
35
+// The caller will have read the `r.RemoteAddr` before getting IP ranges.
36
+//
37
+// This should be a very fast function -- instant if possible.
38
+// The list of IP ranges should be sourced as soon as possible if loaded
39
+// from an external source (i.e. initially loaded during Provisioning),
40
+// so that it's ready to be used when requests start getting handled.
41
+// A read lock should probably be used to get the cached value if the
42
+// ranges can change at runtime (e.g. periodically refreshed).
43
+// Using a `caddy.UsagePool` may be a good idea to avoid having refetch
44
+// the values when a config reload occurs, which would waste time.
45
+//
46
+// If the list of IP ranges cannot be sourced, then provisioning SHOULD
47
+// fail. Getting the IP ranges at runtime MUST NOT fail, because it would
48
+// cancel incoming requests. If refreshing the list fails, then the
49
+// previous list of IP ranges should continue to be returned so that the
50
+// server can continue to operate normally.
51
+type IPRangeSource interface {
52
+ GetIPRanges(*http.Request) []netip.Prefix
53
+}
54
+
55
+// StaticIPRange provides a static range of IP address prefixes (CIDRs).
56
+type StaticIPRange struct {
57
+ // A static list of IP ranges (supports CIDR notation).
58
+ Ranges []string `json:"ranges,omitempty"`
59
+
60
+ // Holds the parsed CIDR ranges from Ranges.
61
+ ranges []netip.Prefix
62
+}
63
+
64
+// CaddyModule returns the Caddy module information.
65
+func (StaticIPRange) CaddyModule() caddy.ModuleInfo {
66
+ return caddy.ModuleInfo{
67
+ ID: "http.ip_sources.static",
68
+ New: func() caddy.Module { return new(StaticIPRange) },
69
+ }
70
+}
71
+
72
+func (s *StaticIPRange) Provision(ctx caddy.Context) error {
73
+ for _, str := range s.Ranges {
74
+ prefix, err := CIDRExpressionToPrefix(str)
75
+ if err != nil {
76
+ return err
77
+ }
78
+ s.ranges = append(s.ranges, prefix)
79
+ }
80
+
81
+ return nil
82
+}
83
+
84
+func (s *StaticIPRange) GetIPRanges(_ *http.Request) []netip.Prefix {
85
+ return s.ranges
86
+}
87
+
88
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
89
+func (m *StaticIPRange) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
90
+ if !d.Next() {
91
+ return nil
92
+ }
93
+ for d.NextArg() {
94
+ if d.Val() == "private_ranges" {
95
+ m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
96
+ continue
97
+ }
98
+ m.Ranges = append(m.Ranges, d.Val())
99
+ }
100
+ return nil
101
+}
102
+
103
+// CIDRExpressionToPrefix takes a string which could be either a
104
+// CIDR expression or a single IP address, and returns a netip.Prefix.
105
+func CIDRExpressionToPrefix(expr string) (netip.Prefix, error) {
106
+ // Having a slash means it should be a CIDR expression
107
+ if strings.Contains(expr, "/") {
108
+ prefix, err := netip.ParsePrefix(expr)
109
+ if err != nil {
110
+ return netip.Prefix{}, fmt.Errorf("parsing CIDR expression: '%s': %v", expr, err)
111
+ }
112
+ return prefix, nil
113
+ }
114
+
115
+ // Otherwise it's likely a single IP address
116
+ parsed, err := netip.ParseAddr(expr)
117
+ if err != nil {
118
+ return netip.Prefix{}, fmt.Errorf("invalid IP address: '%s': %v", expr, err)
119
+ }
120
+ prefix := netip.PrefixFrom(parsed, parsed.BitLen())
121
+ return prefix, nil
122
+}
123
+
124
+// PrivateRangesCIDR returns a list of private CIDR range
125
+// strings, which can be used as a configuration shortcut.
126
+func PrivateRangesCIDR() []string {
127
+ return []string{
128
+ "192.168.0.0/16",
129
+ "172.16.0.0/12",
130
+ "10.0.0.0/8",
131
+ "127.0.0.1/8",
132
+ "fd00::/8",
133
+ "::1",
134
+ }
135
+}
136
+
137
+// Interface guards
138
+var (
139
+ _ caddy.Provisioner = (*StaticIPRange)(nil)
140
+ _ caddyfile.Unmarshaler = (*StaticIPRange)(nil)
141
+ _ IPRangeSource = (*StaticIPRange)(nil)
142
+)
modules/caddyhttp/server.go
+7
-18
@@ -118,7 +118,7 @@ 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
121
+ // A module which provides a source of IP ranges, from which
122
// requests should be trusted. By default, no proxies are
123
// trusted.
124
//
@@ -128,7 +128,7 @@ type Server struct {
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"`
131
+ TrustedProxiesRaw json.RawMessage `json:"trusted_proxies,omitempty" caddy:"namespace=http.ip_sources inline_key=source"`
132
133
// Enables access logging and configures how access logs are handled
134
// in this server. To minimally enable access logs, simply set this
@@ -188,8 +188,7 @@ 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
191
+ trustedProxies IPRangeSource
192
193
shutdownAt time.Time
194
shutdownAtMu *sync.RWMutex
@@ -751,7 +750,10 @@ func determineTrustedProxy(r *http.Request, s *Server) bool {
750
}
751
752
// Check if the client is a trusted proxy
754
- for _, ipRange := range s.trustedProxies {
753
+ if s.trustedProxies == nil {
754
+ return false
755
+ }
756
+ for _, ipRange := range s.trustedProxies.GetIPRanges(r) {
757
if ipRange.Contains(ipAddr) {
758
return true
759
}
@@ -771,19 +773,6 @@ func cloneURL(from, to *url.URL) {
773
}
774
}
775
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
-
776
// Context keys for HTTP request context values.
777
const (
778
// For referencing the server instance