@marcel91200 / caddy Mirror · read-only / commits / 12bcbe2c
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, 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