@marcel91200 / caddy Mirror · read-only / commits / 05e99745
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: Determine real client IP if trusted proxies configured (#5104)

* caddyhttp: Determine real client IP if trusted proxies configured * Support customizing client IP header * Implement client_ip matcher, deprecate remote_ip's forwarded option

Francis Lavoie committed Mar 27, 2023 at 16:22 UTC 05e9974570a08df14b1162a1e98315d4ee9ec2ee
9 files changed +462 -187
caddyconfig/httpcaddyfile/httptype.go
+1
@@ -1328,6 +1328,7 @@ func placeholderShorthands() []string {
1328 "{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
1329 "{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
1330 "{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
1331 + "{client_ip}", "{http.vars.client_ip}",
1332 }
1333 }
1334
caddyconfig/httpcaddyfile/serveroptions.go
+14
@@ -44,6 +44,7 @@ type serverOptions struct {
44 Protocols []string
45 StrictSNIHost *bool
46 TrustedProxiesRaw json.RawMessage
47 + ClientIPHeaders []string
48 ShouldLogCredentials bool
49 Metrics *caddyhttp.Metrics
50 }
@@ -208,6 +209,18 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
209 )
210 serverOpts.TrustedProxiesRaw = jsonSource
211
212 + case "client_ip_headers":
213 + headers := d.RemainingArgs()
214 + for _, header := range headers {
215 + if sliceContains(serverOpts.ClientIPHeaders, header) {
216 + return nil, d.Errf("client IP header %s specified more than once", header)
217 + }
218 + serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
219 + }
220 + if nesting := d.Nesting(); d.NextBlock(nesting) {
221 + return nil, d.ArgErr()
222 + }
223 +
224 case "metrics":
225 if d.NextArg() {
226 return nil, d.ArgErr()
@@ -317,6 +330,7 @@ func applyServerOptions(
330 server.Protocols = opts.Protocols
331 server.StrictSNIHost = opts.StrictSNIHost
332 server.TrustedProxiesRaw = opts.TrustedProxiesRaw
333 + server.ClientIPHeaders = opts.ClientIPHeaders
334 server.Metrics = opts.Metrics
335 if opts.ShouldLogCredentials {
336 if server.Logs == nil {
caddytest/integration/caddyfile_adapt/global_server_options_single.txt
+7
@@ -15,6 +15,8 @@
15 protocols h1 h2 h2c h3
16 strict_sni_host
17 trusted_proxies static private_ranges
18 + client_ip_headers Custom-Real-Client-IP X-Forwarded-For
19 + client_ip_headers A-Third-One
20 }
21 }
22
@@ -67,6 +69,11 @@ foo.com {
69 ],
70 "source": "static"
71 },
72 + "client_ip_headers": [
73 + "Custom-Real-Client-IP",
74 + "X-Forwarded-For",
75 + "A-Third-One"
76 + ],
77 "logs": {
78 "should_log_credentials": true
79 },
caddytest/integration/caddyfile_adapt/matcher_syntax.txt
+25
@@ -43,6 +43,9 @@
43
44 @matcher11 remote_ip private_ranges
45 respond @matcher11 "remote_ip matcher with private ranges"
46 +
47 + @matcher12 client_ip private_ranges
48 + respond @matcher12 "client_ip matcher with private ranges"
49 }
50 ----------
51 {
@@ -250,6 +253,28 @@
253 "handler": "static_response"
254 }
255 ]
256 + },
257 + {
258 + "match": [
259 + {
260 + "client_ip": {
261 + "ranges": [
262 + "192.168.0.0/16",
263 + "172.16.0.0/12",
264 + "10.0.0.0/8",
265 + "127.0.0.1/8",
266 + "fd00::/8",
267 + "::1"
268 + ]
269 + }
270 + }
271 + ],
272 + "handle": [
273 + {
274 + "body": "client_ip matcher with private ranges",
275 + "handler": "static_response"
276 + }
277 + ]
278 }
279 ]
280 }
modules/caddyhttp/app.go
+5
@@ -232,6 +232,11 @@ func (app *App) Provision(ctx caddy.Context) error {
232 srv.trustedProxies = val.(IPRangeSource)
233 }
234
235 + // set the default client IP header to read from
236 + if srv.ClientIPHeaders == nil {
237 + srv.ClientIPHeaders = []string{"X-Forwarded-For"}
238 + }
239 +
240 // process each listener address
241 for i := range srv.Listen {
242 lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
modules/caddyhttp/ip_matchers.go new
+344
@@ -0,0 +1,344 @@
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 + "errors"
19 + "fmt"
20 + "net"
21 + "net/http"
22 + "net/netip"
23 + "reflect"
24 + "strings"
25 +
26 + "github.com/caddyserver/caddy/v2"
27 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
28 + "github.com/google/cel-go/cel"
29 + "github.com/google/cel-go/common/types/ref"
30 + "go.uber.org/zap"
31 +)
32 +
33 +// MatchRemoteIP matches requests by the remote IP address,
34 +// i.e. the IP address of the direct connection to Caddy.
35 +type MatchRemoteIP struct {
36 + // The IPs or CIDR ranges to match.
37 + Ranges []string `json:"ranges,omitempty"`
38 +
39 + // If true, prefer the first IP in the request's X-Forwarded-For
40 + // header, if present, rather than the immediate peer's IP, as
41 + // the reference IP against which to match. Note that it is easy
42 + // to spoof request headers. Default: false
43 + // DEPRECATED: This is insecure, MatchClientIP should be used instead.
44 + Forwarded bool `json:"forwarded,omitempty"`
45 +
46 + // cidrs and zones vars should aligned always in the same
47 + // length and indexes for matching later
48 + cidrs []*netip.Prefix
49 + zones []string
50 + logger *zap.Logger
51 +}
52 +
53 +// MatchClientIP matches requests by the client IP address,
54 +// i.e. the resolved address, considering trusted proxies.
55 +type MatchClientIP struct {
56 + // The IPs or CIDR ranges to match.
57 + Ranges []string `json:"ranges,omitempty"`
58 +
59 + // cidrs and zones vars should aligned always in the same
60 + // length and indexes for matching later
61 + cidrs []*netip.Prefix
62 + zones []string
63 + logger *zap.Logger
64 +}
65 +
66 +func init() {
67 + caddy.RegisterModule(MatchRemoteIP{})
68 + caddy.RegisterModule(MatchClientIP{})
69 +}
70 +
71 +// CaddyModule returns the Caddy module information.
72 +func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
73 + return caddy.ModuleInfo{
74 + ID: "http.matchers.remote_ip",
75 + New: func() caddy.Module { return new(MatchRemoteIP) },
76 + }
77 +}
78 +
79 +// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
80 +func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
81 + for d.Next() {
82 + for d.NextArg() {
83 + if d.Val() == "forwarded" {
84 + if len(m.Ranges) > 0 {
85 + return d.Err("if used, 'forwarded' must be first argument")
86 + }
87 + m.Forwarded = true
88 + continue
89 + }
90 + if d.Val() == "private_ranges" {
91 + m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
92 + continue
93 + }
94 + m.Ranges = append(m.Ranges, d.Val())
95 + }
96 + if d.NextBlock(0) {
97 + return d.Err("malformed remote_ip matcher: blocks are not supported")
98 + }
99 + }
100 + return nil
101 +}
102 +
103 +// CELLibrary produces options that expose this matcher for use in CEL
104 +// expression matchers.
105 +//
106 +// Example:
107 +//
108 +// expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
109 +func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
110 + return CELMatcherImpl(
111 + // name of the macro, this is the function name that users see when writing expressions.
112 + "remote_ip",
113 + // name of the function that the macro will be rewritten to call.
114 + "remote_ip_match_request_list",
115 + // internal data type of the MatchPath value.
116 + []*cel.Type{cel.ListType(cel.StringType)},
117 + // function to convert a constant list of strings to a MatchPath instance.
118 + func(data ref.Val) (RequestMatcher, error) {
119 + refStringList := reflect.TypeOf([]string{})
120 + strList, err := data.ConvertToNative(refStringList)
121 + if err != nil {
122 + return nil, err
123 + }
124 +
125 + m := MatchRemoteIP{}
126 +
127 + for _, input := range strList.([]string) {
128 + if input == "forwarded" {
129 + if len(m.Ranges) > 0 {
130 + return nil, errors.New("if used, 'forwarded' must be first argument")
131 + }
132 + m.Forwarded = true
133 + continue
134 + }
135 + m.Ranges = append(m.Ranges, input)
136 + }
137 +
138 + err = m.Provision(ctx)
139 + return m, err
140 + },
141 + )
142 +}
143 +
144 +// Provision parses m's IP ranges, either from IP or CIDR expressions.
145 +func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
146 + m.logger = ctx.Logger()
147 + cidrs, zones, err := provisionCidrsZonesFromRanges(m.Ranges)
148 + if err != nil {
149 + return err
150 + }
151 + m.cidrs = cidrs
152 + m.zones = zones
153 +
154 + if m.Forwarded {
155 + m.logger.Warn("remote_ip's forwarded mode is deprecated; use the 'client_ip' matcher instead")
156 + }
157 +
158 + return nil
159 +}
160 +
161 +// Match returns true if r matches m.
162 +func (m MatchRemoteIP) Match(r *http.Request) bool {
163 + address := r.RemoteAddr
164 + if m.Forwarded {
165 + if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
166 + address = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
167 + }
168 + }
169 + clientIP, zoneID, err := parseIPZoneFromString(address)
170 + if err != nil {
171 + m.logger.Error("getting remote IP", zap.Error(err))
172 + return false
173 + }
174 + matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
175 + if !matches && !zoneFilter {
176 + m.logger.Debug("zone ID from remote IP did not match", zap.String("zone", zoneID))
177 + }
178 + return matches
179 +}
180 +
181 +// CaddyModule returns the Caddy module information.
182 +func (MatchClientIP) CaddyModule() caddy.ModuleInfo {
183 + return caddy.ModuleInfo{
184 + ID: "http.matchers.client_ip",
185 + New: func() caddy.Module { return new(MatchClientIP) },
186 + }
187 +}
188 +
189 +// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
190 +func (m *MatchClientIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
191 + for d.Next() {
192 + for d.NextArg() {
193 + if d.Val() == "private_ranges" {
194 + m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
195 + continue
196 + }
197 + m.Ranges = append(m.Ranges, d.Val())
198 + }
199 + if d.NextBlock(0) {
200 + return d.Err("malformed client_ip matcher: blocks are not supported")
201 + }
202 + }
203 + return nil
204 +}
205 +
206 +// CELLibrary produces options that expose this matcher for use in CEL
207 +// expression matchers.
208 +//
209 +// Example:
210 +//
211 +// expression client_ip('192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
212 +func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
213 + return CELMatcherImpl(
214 + // name of the macro, this is the function name that users see when writing expressions.
215 + "client_ip",
216 + // name of the function that the macro will be rewritten to call.
217 + "client_ip_match_request_list",
218 + // internal data type of the MatchPath value.
219 + []*cel.Type{cel.ListType(cel.StringType)},
220 + // function to convert a constant list of strings to a MatchPath instance.
221 + func(data ref.Val) (RequestMatcher, error) {
222 + refStringList := reflect.TypeOf([]string{})
223 + strList, err := data.ConvertToNative(refStringList)
224 + if err != nil {
225 + return nil, err
226 + }
227 +
228 + m := MatchClientIP{
229 + Ranges: strList.([]string),
230 + }
231 +
232 + err = m.Provision(ctx)
233 + return m, err
234 + },
235 + )
236 +}
237 +
238 +// Provision parses m's IP ranges, either from IP or CIDR expressions.
239 +func (m *MatchClientIP) Provision(ctx caddy.Context) error {
240 + m.logger = ctx.Logger()
241 + cidrs, zones, err := provisionCidrsZonesFromRanges(m.Ranges)
242 + if err != nil {
243 + return err
244 + }
245 + m.cidrs = cidrs
246 + m.zones = zones
247 + return nil
248 +}
249 +
250 +// Match returns true if r matches m.
251 +func (m MatchClientIP) Match(r *http.Request) bool {
252 + address := GetVar(r.Context(), ClientIPVarKey).(string)
253 + clientIP, zoneID, err := parseIPZoneFromString(address)
254 + if err != nil {
255 + m.logger.Error("getting client IP", zap.Error(err))
256 + return false
257 + }
258 + matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
259 + if !matches && !zoneFilter {
260 + m.logger.Debug("zone ID from client IP did not match", zap.String("zone", zoneID))
261 + }
262 + return matches
263 +}
264 +
265 +func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
266 + cidrs := []*netip.Prefix{}
267 + zones := []string{}
268 + for _, str := range ranges {
269 + // Exclude the zone_id from the IP
270 + if strings.Contains(str, "%") {
271 + split := strings.Split(str, "%")
272 + str = split[0]
273 + // write zone identifiers in m.zones for matching later
274 + zones = append(zones, split[1])
275 + } else {
276 + zones = append(zones, "")
277 + }
278 + if strings.Contains(str, "/") {
279 + ipNet, err := netip.ParsePrefix(str)
280 + if err != nil {
281 + return nil, nil, fmt.Errorf("parsing CIDR expression '%s': %v", str, err)
282 + }
283 + cidrs = append(cidrs, &ipNet)
284 + } else {
285 + ipAddr, err := netip.ParseAddr(str)
286 + if err != nil {
287 + return nil, nil, fmt.Errorf("invalid IP address: '%s': %v", str, err)
288 + }
289 + ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
290 + cidrs = append(cidrs, &ipNew)
291 + }
292 + }
293 + return cidrs, zones, nil
294 +}
295 +
296 +func parseIPZoneFromString(address string) (netip.Addr, string, error) {
297 + ipStr, _, err := net.SplitHostPort(address)
298 + if err != nil {
299 + ipStr = address // OK; probably didn't have a port
300 + }
301 +
302 + // Some IPv6-Adresses can contain zone identifiers at the end,
303 + // which are separated with "%"
304 + zoneID := ""
305 + if strings.Contains(ipStr, "%") {
306 + split := strings.Split(ipStr, "%")
307 + ipStr = split[0]
308 + zoneID = split[1]
309 + }
310 +
311 + ipAddr, err := netip.ParseAddr(ipStr)
312 + if err != nil {
313 + return netip.IPv4Unspecified(), "", err
314 + }
315 +
316 + return ipAddr, zoneID, nil
317 +}
318 +
319 +func matchIPByCidrZones(clientIP netip.Addr, zoneID string, cidrs []*netip.Prefix, zones []string) (bool, bool) {
320 + zoneFilter := true
321 + for i, ipRange := range cidrs {
322 + if ipRange.Contains(clientIP) {
323 + // Check if there are zone filters assigned and if they match.
324 + if zones[i] == "" || zoneID == zones[i] {
325 + return true, false
326 + }
327 + zoneFilter = false
328 + }
329 + }
330 + return false, zoneFilter
331 +}
332 +
333 +// Interface guards
334 +var (
335 + _ RequestMatcher = (*MatchRemoteIP)(nil)
336 + _ caddy.Provisioner = (*MatchRemoteIP)(nil)
337 + _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
338 + _ CELLibraryProducer = (*MatchRemoteIP)(nil)
339 +
340 + _ RequestMatcher = (*MatchClientIP)(nil)
341 + _ caddy.Provisioner = (*MatchClientIP)(nil)
342 + _ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
343 + _ CELLibraryProducer = (*MatchClientIP)(nil)
344 +)
modules/caddyhttp/marshalers.go
+1
@@ -40,6 +40,7 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
40
41 enc.AddString("remote_ip", ip)
42 enc.AddString("remote_port", port)
43 + enc.AddString("client_ip", GetVar(r.Context(), ClientIPVarKey).(string))
44 enc.AddString("proto", r.Proto)
45 enc.AddString("method", r.Method)
46 enc.AddString("host", r.Host)
modules/caddyhttp/matchers.go
-178
@@ -20,7 +20,6 @@ import (
20 "fmt"
21 "net"
22 "net/http"
23 - "net/netip"
23 "net/textproto"
24 "net/url"
25 "path"
@@ -35,7 +34,6 @@ import (
34 "github.com/google/cel-go/cel"
35 "github.com/google/cel-go/common/types"
36 "github.com/google/cel-go/common/types/ref"
38 - "go.uber.org/zap"
37 )
38
39 type (
@@ -176,24 +174,6 @@ type (
174 // "http/2", "http/3", or minimum versions: "http/2+", etc.
175 MatchProtocol string
176
179 - // MatchRemoteIP matches requests by client IP (or CIDR range).
180 - MatchRemoteIP struct {
181 - // The IPs or CIDR ranges to match.
182 - Ranges []string `json:"ranges,omitempty"`
183 -
184 - // If true, prefer the first IP in the request's X-Forwarded-For
185 - // header, if present, rather than the immediate peer's IP, as
186 - // the reference IP against which to match. Note that it is easy
187 - // to spoof request headers. Default: false
188 - Forwarded bool `json:"forwarded,omitempty"`
189 -
190 - // cidrs and zones vars should aligned always in the same
191 - // length and indexes for matching later
192 - cidrs []*netip.Prefix
193 - zones []string
194 - logger *zap.Logger
195 - }
196 -
177 // MatchNot matches requests by negating the results of its matcher
178 // sets. A single "not" matcher takes one or more matcher sets. Each
179 // matcher set is OR'ed; in other words, if any matcher set returns
@@ -229,7 +209,6 @@ func init() {
209 caddy.RegisterModule(MatchHeader{})
210 caddy.RegisterModule(MatchHeaderRE{})
211 caddy.RegisterModule(new(MatchProtocol))
232 - caddy.RegisterModule(MatchRemoteIP{})
212 caddy.RegisterModule(MatchNot{})
213 }
214
@@ -1261,159 +1240,6 @@ func (m MatchNot) Match(r *http.Request) bool {
1240 return true
1241 }
1242
1264 -// CaddyModule returns the Caddy module information.
1265 -func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
1266 - return caddy.ModuleInfo{
1267 - ID: "http.matchers.remote_ip",
1268 - New: func() caddy.Module { return new(MatchRemoteIP) },
1269 - }
1270 -}
1271 -
1272 -// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
1273 -func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1274 - for d.Next() {
1275 - for d.NextArg() {
1276 - if d.Val() == "forwarded" {
1277 - if len(m.Ranges) > 0 {
1278 - return d.Err("if used, 'forwarded' must be first argument")
1279 - }
1280 - m.Forwarded = true
1281 - continue
1282 - }
1283 - if d.Val() == "private_ranges" {
1284 - m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
1285 - continue
1286 - }
1287 - m.Ranges = append(m.Ranges, d.Val())
1288 - }
1289 - if d.NextBlock(0) {
1290 - return d.Err("malformed remote_ip matcher: blocks are not supported")
1291 - }
1292 - }
1293 - return nil
1294 -}
1295 -
1296 -// CELLibrary produces options that expose this matcher for use in CEL
1297 -// expression matchers.
1298 -//
1299 -// Example:
1300 -//
1301 -// expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
1302 -func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
1303 - return CELMatcherImpl(
1304 - // name of the macro, this is the function name that users see when writing expressions.
1305 - "remote_ip",
1306 - // name of the function that the macro will be rewritten to call.
1307 - "remote_ip_match_request_list",
1308 - // internal data type of the MatchPath value.
1309 - []*cel.Type{cel.ListType(cel.StringType)},
1310 - // function to convert a constant list of strings to a MatchPath instance.
1311 - func(data ref.Val) (RequestMatcher, error) {
1312 - refStringList := reflect.TypeOf([]string{})
1313 - strList, err := data.ConvertToNative(refStringList)
1314 - if err != nil {
1315 - return nil, err
1316 - }
1317 -
1318 - m := MatchRemoteIP{}
1319 -
1320 - for _, input := range strList.([]string) {
1321 - if input == "forwarded" {
1322 - if len(m.Ranges) > 0 {
1323 - return nil, errors.New("if used, 'forwarded' must be first argument")
1324 - }
1325 - m.Forwarded = true
1326 - continue
1327 - }
1328 - m.Ranges = append(m.Ranges, input)
1329 - }
1330 -
1331 - err = m.Provision(ctx)
1332 - return m, err
1333 - },
1334 - )
1335 -}
1336 -
1337 -// Provision parses m's IP ranges, either from IP or CIDR expressions.
1338 -func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
1339 - m.logger = ctx.Logger()
1340 - for _, str := range m.Ranges {
1341 - // Exclude the zone_id from the IP
1342 - if strings.Contains(str, "%") {
1343 - split := strings.Split(str, "%")
1344 - str = split[0]
1345 - // write zone identifiers in m.zones for matching later
1346 - m.zones = append(m.zones, split[1])
1347 - } else {
1348 - m.zones = append(m.zones, "")
1349 - }
1350 - if strings.Contains(str, "/") {
1351 - ipNet, err := netip.ParsePrefix(str)
1352 - if err != nil {
1353 - return fmt.Errorf("parsing CIDR expression '%s': %v", str, err)
1354 - }
1355 - m.cidrs = append(m.cidrs, &ipNet)
1356 - } else {
1357 - ipAddr, err := netip.ParseAddr(str)
1358 - if err != nil {
1359 - return fmt.Errorf("invalid IP address: '%s': %v", str, err)
1360 - }
1361 - ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
1362 - m.cidrs = append(m.cidrs, &ipNew)
1363 - }
1364 - }
1365 - return nil
1366 -}
1367 -
1368 -func (m MatchRemoteIP) getClientIP(r *http.Request) (netip.Addr, string, error) {
1369 - remote := r.RemoteAddr
1370 - zoneID := ""
1371 - if m.Forwarded {
1372 - if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
1373 - remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
1374 - }
1375 - }
1376 - ipStr, _, err := net.SplitHostPort(remote)
1377 - if err != nil {
1378 - ipStr = remote // OK; probably didn't have a port
1379 - }
1380 - // Some IPv6-Adresses can contain zone identifiers at the end,
1381 - // which are separated with "%"
1382 - if strings.Contains(ipStr, "%") {
1383 - split := strings.Split(ipStr, "%")
1384 - ipStr = split[0]
1385 - zoneID = split[1]
1386 - }
1387 - ipAddr, err := netip.ParseAddr(ipStr)
1388 - if err != nil {
1389 - return netip.IPv4Unspecified(), "", err
1390 - }
1391 - return ipAddr, zoneID, nil
1392 -}
1393 -
1394 -// Match returns true if r matches m.
1395 -func (m MatchRemoteIP) Match(r *http.Request) bool {
1396 - clientIP, zoneID, err := m.getClientIP(r)
1397 - if err != nil {
1398 - m.logger.Error("getting client IP", zap.Error(err))
1399 - return false
1400 - }
1401 - zoneFilter := true
1402 - for i, ipRange := range m.cidrs {
1403 - if ipRange.Contains(clientIP) {
1404 - // Check if there are zone filters assigned and if they match.
1405 - if m.zones[i] == "" || zoneID == m.zones[i] {
1406 - return true
1407 - }
1408 - zoneFilter = false
1409 - }
1410 - }
1411 - if !zoneFilter {
1412 - m.logger.Debug("zone ID from remote did not match", zap.String("zone", zoneID))
1413 - }
1414 - return false
1415 -}
1416 -
1243 // MatchRegexp is an embedable type for matching
1244 // using regular expressions. It adds placeholders
1245 // to the request's replacer.
@@ -1588,8 +1414,6 @@ var (
1414 _ RequestMatcher = (*MatchHeaderRE)(nil)
1415 _ caddy.Provisioner = (*MatchHeaderRE)(nil)
1416 _ RequestMatcher = (*MatchProtocol)(nil)
1591 - _ RequestMatcher = (*MatchRemoteIP)(nil)
1592 - _ caddy.Provisioner = (*MatchRemoteIP)(nil)
1417 _ RequestMatcher = (*MatchNot)(nil)
1418 _ caddy.Provisioner = (*MatchNot)(nil)
1419 _ caddy.Provisioner = (*MatchRegexp)(nil)
@@ -1602,7 +1426,6 @@ var (
1426 _ caddyfile.Unmarshaler = (*MatchHeader)(nil)
1427 _ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
1428 _ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
1605 - _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
1429 _ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
1430 _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
1431
@@ -1614,7 +1437,6 @@ var (
1437 _ CELLibraryProducer = (*MatchHeader)(nil)
1438 _ CELLibraryProducer = (*MatchHeaderRE)(nil)
1439 _ CELLibraryProducer = (*MatchProtocol)(nil)
1617 - _ CELLibraryProducer = (*MatchRemoteIP)(nil)
1440 // _ CELLibraryProducer = (*VarsMatcher)(nil)
1441 // _ CELLibraryProducer = (*MatchVarsRE)(nil)
1442
modules/caddyhttp/server.go
+65 -9
@@ -130,6 +130,17 @@ type Server struct {
130 // to trust sensitive incoming `X-Forwarded-*` headers.
131 TrustedProxiesRaw json.RawMessage `json:"trusted_proxies,omitempty" caddy:"namespace=http.ip_sources inline_key=source"`
132
133 + // The headers from which the client IP address could be
134 + // read from. These will be considered in order, with the
135 + // first good value being used as the client IP.
136 + // By default, only `X-Forwarded-For` is considered.
137 + //
138 + // This depends on `trusted_proxies` being configured and
139 + // the request being validated as coming from a trusted
140 + // proxy, otherwise the client IP will be set to the direct
141 + // remote IP address.
142 + ClientIPHeaders []string `json:"client_ip_headers,omitempty"`
143 +
144 // Enables access logging and configures how access logs are handled
145 // in this server. To minimally enable access logs, simply set this
146 // to a non-null, empty struct.
@@ -690,10 +701,15 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
701 // set up the context for the request
702 ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
703 ctx = context.WithValue(ctx, ServerCtxKey, s)
704 +
705 + trusted, clientIP := determineTrustedProxy(r, s)
706 ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
694 - TrustedProxyVarKey: determineTrustedProxy(r, s),
707 + TrustedProxyVarKey: trusted,
708 + ClientIPVarKey: clientIP,
709 })
710 +
711 ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{}))
712 +
713 var url2 url.URL // avoid letting this escape to the heap
714 ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
715 r = r.WithContext(ctx)
@@ -724,11 +740,12 @@ func originalRequest(req *http.Request, urlCopy *url.URL) http.Request {
740
741 // determineTrustedProxy parses the remote IP address of
742 // the request, and determines (if the server configured it)
727 -// if the client is a trusted proxy.
728 -func determineTrustedProxy(r *http.Request, s *Server) bool {
743 +// if the client is a trusted proxy. If trusted, also returns
744 +// the real client IP if possible.
745 +func determineTrustedProxy(r *http.Request, s *Server) (bool, string) {
746 // If there's no server, then we can't check anything
747 if s == nil {
731 - return false
748 + return false, ""
749 }
750
751 // Parse the remote IP, ignore the error as non-fatal,
@@ -738,7 +755,7 @@ func determineTrustedProxy(r *http.Request, s *Server) bool {
755 // remote address and used an invalid value.
756 clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
757 if err != nil {
741 - return false
758 + return false, ""
759 }
760
761 // Client IP may contain a zone if IPv6, so we need
@@ -746,20 +763,56 @@ func determineTrustedProxy(r *http.Request, s *Server) bool {
763 clientIP, _, _ = strings.Cut(clientIP, "%")
764 ipAddr, err := netip.ParseAddr(clientIP)
765 if err != nil {
749 - return false
766 + return false, ""
767 }
768
769 // Check if the client is a trusted proxy
770 if s.trustedProxies == nil {
754 - return false
771 + return false, ipAddr.String()
772 }
773 for _, ipRange := range s.trustedProxies.GetIPRanges(r) {
774 if ipRange.Contains(ipAddr) {
758 - return true
775 + // We trust the proxy, so let's try to
776 + // determine the real client IP
777 + return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String())
778 }
779 }
780
762 - return false
781 + return false, ipAddr.String()
782 +}
783 +
784 +// trustedRealClientIP finds the client IP from the request assuming it is
785 +// from a trusted client. If there is no client IP headers, then the
786 +// direct remote address is returned. If there are client IP headers,
787 +// then the first value from those headers is used.
788 +func trustedRealClientIP(r *http.Request, headers []string, clientIP string) string {
789 + // Read all the values of the configured client IP headers, in order
790 + var values []string
791 + for _, field := range headers {
792 + values = append(values, r.Header.Values(field)...)
793 + }
794 +
795 + // If we don't have any values, then give up
796 + if len(values) == 0 {
797 + return clientIP
798 + }
799 +
800 + // Since there can be many header values, we need to
801 + // join them together before splitting to get the full list
802 + allValues := strings.Split(strings.Join(values, ","), ",")
803 +
804 + // Get first valid left-most IP address
805 + for _, ip := range allValues {
806 + ip, _, _ = strings.Cut(strings.TrimSpace(ip), "%")
807 + ipAddr, err := netip.ParseAddr(ip)
808 + if err != nil {
809 + continue
810 + }
811 + return ipAddr.String()
812 + }
813 +
814 + // We didn't find a valid IP
815 + return clientIP
816 }
817
818 // cloneURL makes a copy of r.URL and returns a
@@ -787,4 +840,7 @@ const (
840
841 // For tracking whether the client is a trusted proxy
842 TrustedProxyVarKey string = "trusted_proxy"
843 +
844 + // For tracking the real client IP (affected by trusted_proxy)
845 + ClientIPVarKey string = "client_ip"
846 )