mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 18:17:08 +08:00 
			
		
		
		
	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
This commit is contained in:
		@ -1328,6 +1328,7 @@ func placeholderShorthands() []string {
 | 
				
			|||||||
		"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
 | 
							"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
 | 
				
			||||||
		"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
 | 
							"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
 | 
				
			||||||
		"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
 | 
							"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
 | 
				
			||||||
 | 
							"{client_ip}", "{http.vars.client_ip}",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -44,6 +44,7 @@ type serverOptions struct {
 | 
				
			|||||||
	Protocols            []string
 | 
						Protocols            []string
 | 
				
			||||||
	StrictSNIHost        *bool
 | 
						StrictSNIHost        *bool
 | 
				
			||||||
	TrustedProxiesRaw    json.RawMessage
 | 
						TrustedProxiesRaw    json.RawMessage
 | 
				
			||||||
 | 
						ClientIPHeaders      []string
 | 
				
			||||||
	ShouldLogCredentials bool
 | 
						ShouldLogCredentials bool
 | 
				
			||||||
	Metrics              *caddyhttp.Metrics
 | 
						Metrics              *caddyhttp.Metrics
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -208,6 +209,18 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
 | 
				
			|||||||
				)
 | 
									)
 | 
				
			||||||
				serverOpts.TrustedProxiesRaw = jsonSource
 | 
									serverOpts.TrustedProxiesRaw = jsonSource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case "client_ip_headers":
 | 
				
			||||||
 | 
									headers := d.RemainingArgs()
 | 
				
			||||||
 | 
									for _, header := range headers {
 | 
				
			||||||
 | 
										if sliceContains(serverOpts.ClientIPHeaders, header) {
 | 
				
			||||||
 | 
											return nil, d.Errf("client IP header %s specified more than once", header)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if nesting := d.Nesting(); d.NextBlock(nesting) {
 | 
				
			||||||
 | 
										return nil, d.ArgErr()
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case "metrics":
 | 
								case "metrics":
 | 
				
			||||||
				if d.NextArg() {
 | 
									if d.NextArg() {
 | 
				
			||||||
					return nil, d.ArgErr()
 | 
										return nil, d.ArgErr()
 | 
				
			||||||
@ -317,6 +330,7 @@ func applyServerOptions(
 | 
				
			|||||||
		server.Protocols = opts.Protocols
 | 
							server.Protocols = opts.Protocols
 | 
				
			||||||
		server.StrictSNIHost = opts.StrictSNIHost
 | 
							server.StrictSNIHost = opts.StrictSNIHost
 | 
				
			||||||
		server.TrustedProxiesRaw = opts.TrustedProxiesRaw
 | 
							server.TrustedProxiesRaw = opts.TrustedProxiesRaw
 | 
				
			||||||
 | 
							server.ClientIPHeaders = opts.ClientIPHeaders
 | 
				
			||||||
		server.Metrics = opts.Metrics
 | 
							server.Metrics = opts.Metrics
 | 
				
			||||||
		if opts.ShouldLogCredentials {
 | 
							if opts.ShouldLogCredentials {
 | 
				
			||||||
			if server.Logs == nil {
 | 
								if server.Logs == nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,8 @@
 | 
				
			|||||||
		protocols h1 h2 h2c h3
 | 
							protocols h1 h2 h2c h3
 | 
				
			||||||
		strict_sni_host
 | 
							strict_sni_host
 | 
				
			||||||
		trusted_proxies static private_ranges
 | 
							trusted_proxies static private_ranges
 | 
				
			||||||
 | 
							client_ip_headers Custom-Real-Client-IP X-Forwarded-For
 | 
				
			||||||
 | 
							client_ip_headers A-Third-One
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -67,6 +69,11 @@ foo.com {
 | 
				
			|||||||
						],
 | 
											],
 | 
				
			||||||
						"source": "static"
 | 
											"source": "static"
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
 | 
										"client_ip_headers": [
 | 
				
			||||||
 | 
											"Custom-Real-Client-IP",
 | 
				
			||||||
 | 
											"X-Forwarded-For",
 | 
				
			||||||
 | 
											"A-Third-One"
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
					"logs": {
 | 
										"logs": {
 | 
				
			||||||
						"should_log_credentials": true
 | 
											"should_log_credentials": true
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	@matcher11 remote_ip private_ranges
 | 
						@matcher11 remote_ip private_ranges
 | 
				
			||||||
	respond @matcher11 "remote_ip matcher with private ranges"
 | 
						respond @matcher11 "remote_ip matcher with private ranges"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@matcher12 client_ip private_ranges
 | 
				
			||||||
 | 
						respond @matcher12 "client_ip matcher with private ranges"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
----------
 | 
					----------
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -250,6 +253,28 @@
 | 
				
			|||||||
									"handler": "static_response"
 | 
														"handler": "static_response"
 | 
				
			||||||
								}
 | 
													}
 | 
				
			||||||
							]
 | 
												]
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
											{
 | 
				
			||||||
 | 
												"match": [
 | 
				
			||||||
 | 
													{
 | 
				
			||||||
 | 
														"client_ip": {
 | 
				
			||||||
 | 
															"ranges": [
 | 
				
			||||||
 | 
																"192.168.0.0/16",
 | 
				
			||||||
 | 
																"172.16.0.0/12",
 | 
				
			||||||
 | 
																"10.0.0.0/8",
 | 
				
			||||||
 | 
																"127.0.0.1/8",
 | 
				
			||||||
 | 
																"fd00::/8",
 | 
				
			||||||
 | 
																"::1"
 | 
				
			||||||
 | 
															]
 | 
				
			||||||
 | 
														}
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
												],
 | 
				
			||||||
 | 
												"handle": [
 | 
				
			||||||
 | 
													{
 | 
				
			||||||
 | 
														"body": "client_ip matcher with private ranges",
 | 
				
			||||||
 | 
														"handler": "static_response"
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
												]
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					]
 | 
										]
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
				
			|||||||
@ -232,6 +232,11 @@ func (app *App) Provision(ctx caddy.Context) error {
 | 
				
			|||||||
			srv.trustedProxies = val.(IPRangeSource)
 | 
								srv.trustedProxies = val.(IPRangeSource)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// set the default client IP header to read from
 | 
				
			||||||
 | 
							if srv.ClientIPHeaders == nil {
 | 
				
			||||||
 | 
								srv.ClientIPHeaders = []string{"X-Forwarded-For"}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// process each listener address
 | 
							// process each listener address
 | 
				
			||||||
		for i := range srv.Listen {
 | 
							for i := range srv.Listen {
 | 
				
			||||||
			lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
 | 
								lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										344
									
								
								modules/caddyhttp/ip_matchers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								modules/caddyhttp/ip_matchers.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,344 @@
 | 
				
			|||||||
 | 
					// Copyright 2015 Matthew Holt and The Caddy Authors
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package caddyhttp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/netip"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/caddyserver/caddy/v2"
 | 
				
			||||||
 | 
						"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 | 
				
			||||||
 | 
						"github.com/google/cel-go/cel"
 | 
				
			||||||
 | 
						"github.com/google/cel-go/common/types/ref"
 | 
				
			||||||
 | 
						"go.uber.org/zap"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MatchRemoteIP matches requests by the remote IP address,
 | 
				
			||||||
 | 
					// i.e. the IP address of the direct connection to Caddy.
 | 
				
			||||||
 | 
					type MatchRemoteIP struct {
 | 
				
			||||||
 | 
						// The IPs or CIDR ranges to match.
 | 
				
			||||||
 | 
						Ranges []string `json:"ranges,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If true, prefer the first IP in the request's X-Forwarded-For
 | 
				
			||||||
 | 
						// header, if present, rather than the immediate peer's IP, as
 | 
				
			||||||
 | 
						// the reference IP against which to match. Note that it is easy
 | 
				
			||||||
 | 
						// to spoof request headers. Default: false
 | 
				
			||||||
 | 
						// DEPRECATED: This is insecure, MatchClientIP should be used instead.
 | 
				
			||||||
 | 
						Forwarded bool `json:"forwarded,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// cidrs and zones vars should aligned always in the same
 | 
				
			||||||
 | 
						// length and indexes for matching later
 | 
				
			||||||
 | 
						cidrs  []*netip.Prefix
 | 
				
			||||||
 | 
						zones  []string
 | 
				
			||||||
 | 
						logger *zap.Logger
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MatchClientIP matches requests by the client IP address,
 | 
				
			||||||
 | 
					// i.e. the resolved address, considering trusted proxies.
 | 
				
			||||||
 | 
					type MatchClientIP struct {
 | 
				
			||||||
 | 
						// The IPs or CIDR ranges to match.
 | 
				
			||||||
 | 
						Ranges []string `json:"ranges,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// cidrs and zones vars should aligned always in the same
 | 
				
			||||||
 | 
						// length and indexes for matching later
 | 
				
			||||||
 | 
						cidrs  []*netip.Prefix
 | 
				
			||||||
 | 
						zones  []string
 | 
				
			||||||
 | 
						logger *zap.Logger
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						caddy.RegisterModule(MatchRemoteIP{})
 | 
				
			||||||
 | 
						caddy.RegisterModule(MatchClientIP{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CaddyModule returns the Caddy module information.
 | 
				
			||||||
 | 
					func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
 | 
				
			||||||
 | 
						return caddy.ModuleInfo{
 | 
				
			||||||
 | 
							ID:  "http.matchers.remote_ip",
 | 
				
			||||||
 | 
							New: func() caddy.Module { return new(MatchRemoteIP) },
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
 | 
				
			||||||
 | 
					func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 | 
				
			||||||
 | 
						for d.Next() {
 | 
				
			||||||
 | 
							for d.NextArg() {
 | 
				
			||||||
 | 
								if d.Val() == "forwarded" {
 | 
				
			||||||
 | 
									if len(m.Ranges) > 0 {
 | 
				
			||||||
 | 
										return d.Err("if used, 'forwarded' must be first argument")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									m.Forwarded = true
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if d.Val() == "private_ranges" {
 | 
				
			||||||
 | 
									m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								m.Ranges = append(m.Ranges, d.Val())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if d.NextBlock(0) {
 | 
				
			||||||
 | 
								return d.Err("malformed remote_ip matcher: blocks are not supported")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CELLibrary produces options that expose this matcher for use in CEL
 | 
				
			||||||
 | 
					// expression matchers.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Example:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
 | 
				
			||||||
 | 
					func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
 | 
				
			||||||
 | 
						return CELMatcherImpl(
 | 
				
			||||||
 | 
							// name of the macro, this is the function name that users see when writing expressions.
 | 
				
			||||||
 | 
							"remote_ip",
 | 
				
			||||||
 | 
							// name of the function that the macro will be rewritten to call.
 | 
				
			||||||
 | 
							"remote_ip_match_request_list",
 | 
				
			||||||
 | 
							// internal data type of the MatchPath value.
 | 
				
			||||||
 | 
							[]*cel.Type{cel.ListType(cel.StringType)},
 | 
				
			||||||
 | 
							// function to convert a constant list of strings to a MatchPath instance.
 | 
				
			||||||
 | 
							func(data ref.Val) (RequestMatcher, error) {
 | 
				
			||||||
 | 
								refStringList := reflect.TypeOf([]string{})
 | 
				
			||||||
 | 
								strList, err := data.ConvertToNative(refStringList)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								m := MatchRemoteIP{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for _, input := range strList.([]string) {
 | 
				
			||||||
 | 
									if input == "forwarded" {
 | 
				
			||||||
 | 
										if len(m.Ranges) > 0 {
 | 
				
			||||||
 | 
											return nil, errors.New("if used, 'forwarded' must be first argument")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										m.Forwarded = true
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									m.Ranges = append(m.Ranges, input)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err = m.Provision(ctx)
 | 
				
			||||||
 | 
								return m, err
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Provision parses m's IP ranges, either from IP or CIDR expressions.
 | 
				
			||||||
 | 
					func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
 | 
				
			||||||
 | 
						m.logger = ctx.Logger()
 | 
				
			||||||
 | 
						cidrs, zones, err := provisionCidrsZonesFromRanges(m.Ranges)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m.cidrs = cidrs
 | 
				
			||||||
 | 
						m.zones = zones
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if m.Forwarded {
 | 
				
			||||||
 | 
							m.logger.Warn("remote_ip's forwarded mode is deprecated; use the 'client_ip' matcher instead")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Match returns true if r matches m.
 | 
				
			||||||
 | 
					func (m MatchRemoteIP) Match(r *http.Request) bool {
 | 
				
			||||||
 | 
						address := r.RemoteAddr
 | 
				
			||||||
 | 
						if m.Forwarded {
 | 
				
			||||||
 | 
							if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
 | 
				
			||||||
 | 
								address = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						clientIP, zoneID, err := parseIPZoneFromString(address)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							m.logger.Error("getting remote IP", zap.Error(err))
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
 | 
				
			||||||
 | 
						if !matches && !zoneFilter {
 | 
				
			||||||
 | 
							m.logger.Debug("zone ID from remote IP did not match", zap.String("zone", zoneID))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return matches
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CaddyModule returns the Caddy module information.
 | 
				
			||||||
 | 
					func (MatchClientIP) CaddyModule() caddy.ModuleInfo {
 | 
				
			||||||
 | 
						return caddy.ModuleInfo{
 | 
				
			||||||
 | 
							ID:  "http.matchers.client_ip",
 | 
				
			||||||
 | 
							New: func() caddy.Module { return new(MatchClientIP) },
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
 | 
				
			||||||
 | 
					func (m *MatchClientIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 | 
				
			||||||
 | 
						for d.Next() {
 | 
				
			||||||
 | 
							for d.NextArg() {
 | 
				
			||||||
 | 
								if d.Val() == "private_ranges" {
 | 
				
			||||||
 | 
									m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								m.Ranges = append(m.Ranges, d.Val())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if d.NextBlock(0) {
 | 
				
			||||||
 | 
								return d.Err("malformed client_ip matcher: blocks are not supported")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CELLibrary produces options that expose this matcher for use in CEL
 | 
				
			||||||
 | 
					// expression matchers.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Example:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	expression client_ip('192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
 | 
				
			||||||
 | 
					func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
 | 
				
			||||||
 | 
						return CELMatcherImpl(
 | 
				
			||||||
 | 
							// name of the macro, this is the function name that users see when writing expressions.
 | 
				
			||||||
 | 
							"client_ip",
 | 
				
			||||||
 | 
							// name of the function that the macro will be rewritten to call.
 | 
				
			||||||
 | 
							"client_ip_match_request_list",
 | 
				
			||||||
 | 
							// internal data type of the MatchPath value.
 | 
				
			||||||
 | 
							[]*cel.Type{cel.ListType(cel.StringType)},
 | 
				
			||||||
 | 
							// function to convert a constant list of strings to a MatchPath instance.
 | 
				
			||||||
 | 
							func(data ref.Val) (RequestMatcher, error) {
 | 
				
			||||||
 | 
								refStringList := reflect.TypeOf([]string{})
 | 
				
			||||||
 | 
								strList, err := data.ConvertToNative(refStringList)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								m := MatchClientIP{
 | 
				
			||||||
 | 
									Ranges: strList.([]string),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err = m.Provision(ctx)
 | 
				
			||||||
 | 
								return m, err
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Provision parses m's IP ranges, either from IP or CIDR expressions.
 | 
				
			||||||
 | 
					func (m *MatchClientIP) Provision(ctx caddy.Context) error {
 | 
				
			||||||
 | 
						m.logger = ctx.Logger()
 | 
				
			||||||
 | 
						cidrs, zones, err := provisionCidrsZonesFromRanges(m.Ranges)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m.cidrs = cidrs
 | 
				
			||||||
 | 
						m.zones = zones
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Match returns true if r matches m.
 | 
				
			||||||
 | 
					func (m MatchClientIP) Match(r *http.Request) bool {
 | 
				
			||||||
 | 
						address := GetVar(r.Context(), ClientIPVarKey).(string)
 | 
				
			||||||
 | 
						clientIP, zoneID, err := parseIPZoneFromString(address)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							m.logger.Error("getting client IP", zap.Error(err))
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
 | 
				
			||||||
 | 
						if !matches && !zoneFilter {
 | 
				
			||||||
 | 
							m.logger.Debug("zone ID from client IP did not match", zap.String("zone", zoneID))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return matches
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
 | 
				
			||||||
 | 
						cidrs := []*netip.Prefix{}
 | 
				
			||||||
 | 
						zones := []string{}
 | 
				
			||||||
 | 
						for _, str := range ranges {
 | 
				
			||||||
 | 
							// Exclude the zone_id from the IP
 | 
				
			||||||
 | 
							if strings.Contains(str, "%") {
 | 
				
			||||||
 | 
								split := strings.Split(str, "%")
 | 
				
			||||||
 | 
								str = split[0]
 | 
				
			||||||
 | 
								// write zone identifiers in m.zones for matching later
 | 
				
			||||||
 | 
								zones = append(zones, split[1])
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								zones = append(zones, "")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if strings.Contains(str, "/") {
 | 
				
			||||||
 | 
								ipNet, err := netip.ParsePrefix(str)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, nil, fmt.Errorf("parsing CIDR expression '%s': %v", str, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								cidrs = append(cidrs, &ipNet)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ipAddr, err := netip.ParseAddr(str)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, nil, fmt.Errorf("invalid IP address: '%s': %v", str, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
 | 
				
			||||||
 | 
								cidrs = append(cidrs, &ipNew)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cidrs, zones, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseIPZoneFromString(address string) (netip.Addr, string, error) {
 | 
				
			||||||
 | 
						ipStr, _, err := net.SplitHostPort(address)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ipStr = address // OK; probably didn't have a port
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Some IPv6-Adresses can contain zone identifiers at the end,
 | 
				
			||||||
 | 
						// which are separated with "%"
 | 
				
			||||||
 | 
						zoneID := ""
 | 
				
			||||||
 | 
						if strings.Contains(ipStr, "%") {
 | 
				
			||||||
 | 
							split := strings.Split(ipStr, "%")
 | 
				
			||||||
 | 
							ipStr = split[0]
 | 
				
			||||||
 | 
							zoneID = split[1]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ipAddr, err := netip.ParseAddr(ipStr)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return netip.IPv4Unspecified(), "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ipAddr, zoneID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func matchIPByCidrZones(clientIP netip.Addr, zoneID string, cidrs []*netip.Prefix, zones []string) (bool, bool) {
 | 
				
			||||||
 | 
						zoneFilter := true
 | 
				
			||||||
 | 
						for i, ipRange := range cidrs {
 | 
				
			||||||
 | 
							if ipRange.Contains(clientIP) {
 | 
				
			||||||
 | 
								// Check if there are zone filters assigned and if they match.
 | 
				
			||||||
 | 
								if zones[i] == "" || zoneID == zones[i] {
 | 
				
			||||||
 | 
									return true, false
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								zoneFilter = false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false, zoneFilter
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Interface guards
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						_ RequestMatcher        = (*MatchRemoteIP)(nil)
 | 
				
			||||||
 | 
						_ caddy.Provisioner     = (*MatchRemoteIP)(nil)
 | 
				
			||||||
 | 
						_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
 | 
				
			||||||
 | 
						_ CELLibraryProducer    = (*MatchRemoteIP)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_ RequestMatcher        = (*MatchClientIP)(nil)
 | 
				
			||||||
 | 
						_ caddy.Provisioner     = (*MatchClientIP)(nil)
 | 
				
			||||||
 | 
						_ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
 | 
				
			||||||
 | 
						_ CELLibraryProducer    = (*MatchClientIP)(nil)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -40,6 +40,7 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	enc.AddString("remote_ip", ip)
 | 
						enc.AddString("remote_ip", ip)
 | 
				
			||||||
	enc.AddString("remote_port", port)
 | 
						enc.AddString("remote_port", port)
 | 
				
			||||||
 | 
						enc.AddString("client_ip", GetVar(r.Context(), ClientIPVarKey).(string))
 | 
				
			||||||
	enc.AddString("proto", r.Proto)
 | 
						enc.AddString("proto", r.Proto)
 | 
				
			||||||
	enc.AddString("method", r.Method)
 | 
						enc.AddString("method", r.Method)
 | 
				
			||||||
	enc.AddString("host", r.Host)
 | 
						enc.AddString("host", r.Host)
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,6 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/netip"
 | 
					 | 
				
			||||||
	"net/textproto"
 | 
						"net/textproto"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
@ -35,7 +34,6 @@ import (
 | 
				
			|||||||
	"github.com/google/cel-go/cel"
 | 
						"github.com/google/cel-go/cel"
 | 
				
			||||||
	"github.com/google/cel-go/common/types"
 | 
						"github.com/google/cel-go/common/types"
 | 
				
			||||||
	"github.com/google/cel-go/common/types/ref"
 | 
						"github.com/google/cel-go/common/types/ref"
 | 
				
			||||||
	"go.uber.org/zap"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type (
 | 
					type (
 | 
				
			||||||
@ -176,24 +174,6 @@ type (
 | 
				
			|||||||
	// "http/2", "http/3", or minimum versions: "http/2+", etc.
 | 
						// "http/2", "http/3", or minimum versions: "http/2+", etc.
 | 
				
			||||||
	MatchProtocol string
 | 
						MatchProtocol string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// MatchRemoteIP matches requests by client IP (or CIDR range).
 | 
					 | 
				
			||||||
	MatchRemoteIP struct {
 | 
					 | 
				
			||||||
		// The IPs or CIDR ranges to match.
 | 
					 | 
				
			||||||
		Ranges []string `json:"ranges,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// If true, prefer the first IP in the request's X-Forwarded-For
 | 
					 | 
				
			||||||
		// header, if present, rather than the immediate peer's IP, as
 | 
					 | 
				
			||||||
		// the reference IP against which to match. Note that it is easy
 | 
					 | 
				
			||||||
		// to spoof request headers. Default: false
 | 
					 | 
				
			||||||
		Forwarded bool `json:"forwarded,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// cidrs and zones vars should aligned always in the same
 | 
					 | 
				
			||||||
		// length and indexes for matching later
 | 
					 | 
				
			||||||
		cidrs  []*netip.Prefix
 | 
					 | 
				
			||||||
		zones  []string
 | 
					 | 
				
			||||||
		logger *zap.Logger
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// MatchNot matches requests by negating the results of its matcher
 | 
						// MatchNot matches requests by negating the results of its matcher
 | 
				
			||||||
	// sets. A single "not" matcher takes one or more matcher sets. Each
 | 
						// sets. A single "not" matcher takes one or more matcher sets. Each
 | 
				
			||||||
	// matcher set is OR'ed; in other words, if any matcher set returns
 | 
						// matcher set is OR'ed; in other words, if any matcher set returns
 | 
				
			||||||
@ -229,7 +209,6 @@ func init() {
 | 
				
			|||||||
	caddy.RegisterModule(MatchHeader{})
 | 
						caddy.RegisterModule(MatchHeader{})
 | 
				
			||||||
	caddy.RegisterModule(MatchHeaderRE{})
 | 
						caddy.RegisterModule(MatchHeaderRE{})
 | 
				
			||||||
	caddy.RegisterModule(new(MatchProtocol))
 | 
						caddy.RegisterModule(new(MatchProtocol))
 | 
				
			||||||
	caddy.RegisterModule(MatchRemoteIP{})
 | 
					 | 
				
			||||||
	caddy.RegisterModule(MatchNot{})
 | 
						caddy.RegisterModule(MatchNot{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1261,159 +1240,6 @@ func (m MatchNot) Match(r *http.Request) bool {
 | 
				
			|||||||
	return true
 | 
						return true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CaddyModule returns the Caddy module information.
 | 
					 | 
				
			||||||
func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
 | 
					 | 
				
			||||||
	return caddy.ModuleInfo{
 | 
					 | 
				
			||||||
		ID:  "http.matchers.remote_ip",
 | 
					 | 
				
			||||||
		New: func() caddy.Module { return new(MatchRemoteIP) },
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
 | 
					 | 
				
			||||||
func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 | 
					 | 
				
			||||||
	for d.Next() {
 | 
					 | 
				
			||||||
		for d.NextArg() {
 | 
					 | 
				
			||||||
			if d.Val() == "forwarded" {
 | 
					 | 
				
			||||||
				if len(m.Ranges) > 0 {
 | 
					 | 
				
			||||||
					return d.Err("if used, 'forwarded' must be first argument")
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				m.Forwarded = true
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if d.Val() == "private_ranges" {
 | 
					 | 
				
			||||||
				m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			m.Ranges = append(m.Ranges, d.Val())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if d.NextBlock(0) {
 | 
					 | 
				
			||||||
			return d.Err("malformed remote_ip matcher: blocks are not supported")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CELLibrary produces options that expose this matcher for use in CEL
 | 
					 | 
				
			||||||
// expression matchers.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Example:
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
//	expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
 | 
					 | 
				
			||||||
func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
 | 
					 | 
				
			||||||
	return CELMatcherImpl(
 | 
					 | 
				
			||||||
		// name of the macro, this is the function name that users see when writing expressions.
 | 
					 | 
				
			||||||
		"remote_ip",
 | 
					 | 
				
			||||||
		// name of the function that the macro will be rewritten to call.
 | 
					 | 
				
			||||||
		"remote_ip_match_request_list",
 | 
					 | 
				
			||||||
		// internal data type of the MatchPath value.
 | 
					 | 
				
			||||||
		[]*cel.Type{cel.ListType(cel.StringType)},
 | 
					 | 
				
			||||||
		// function to convert a constant list of strings to a MatchPath instance.
 | 
					 | 
				
			||||||
		func(data ref.Val) (RequestMatcher, error) {
 | 
					 | 
				
			||||||
			refStringList := reflect.TypeOf([]string{})
 | 
					 | 
				
			||||||
			strList, err := data.ConvertToNative(refStringList)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			m := MatchRemoteIP{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			for _, input := range strList.([]string) {
 | 
					 | 
				
			||||||
				if input == "forwarded" {
 | 
					 | 
				
			||||||
					if len(m.Ranges) > 0 {
 | 
					 | 
				
			||||||
						return nil, errors.New("if used, 'forwarded' must be first argument")
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					m.Forwarded = true
 | 
					 | 
				
			||||||
					continue
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				m.Ranges = append(m.Ranges, input)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			err = m.Provision(ctx)
 | 
					 | 
				
			||||||
			return m, err
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Provision parses m's IP ranges, either from IP or CIDR expressions.
 | 
					 | 
				
			||||||
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
 | 
					 | 
				
			||||||
	m.logger = ctx.Logger()
 | 
					 | 
				
			||||||
	for _, str := range m.Ranges {
 | 
					 | 
				
			||||||
		// Exclude the zone_id from the IP
 | 
					 | 
				
			||||||
		if strings.Contains(str, "%") {
 | 
					 | 
				
			||||||
			split := strings.Split(str, "%")
 | 
					 | 
				
			||||||
			str = split[0]
 | 
					 | 
				
			||||||
			// write zone identifiers in m.zones for matching later
 | 
					 | 
				
			||||||
			m.zones = append(m.zones, split[1])
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			m.zones = append(m.zones, "")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if strings.Contains(str, "/") {
 | 
					 | 
				
			||||||
			ipNet, err := netip.ParsePrefix(str)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return fmt.Errorf("parsing CIDR expression '%s': %v", str, err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			m.cidrs = append(m.cidrs, &ipNet)
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			ipAddr, err := netip.ParseAddr(str)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return fmt.Errorf("invalid IP address: '%s': %v", str, err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
 | 
					 | 
				
			||||||
			m.cidrs = append(m.cidrs, &ipNew)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m MatchRemoteIP) getClientIP(r *http.Request) (netip.Addr, string, error) {
 | 
					 | 
				
			||||||
	remote := r.RemoteAddr
 | 
					 | 
				
			||||||
	zoneID := ""
 | 
					 | 
				
			||||||
	if m.Forwarded {
 | 
					 | 
				
			||||||
		if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
 | 
					 | 
				
			||||||
			remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	ipStr, _, err := net.SplitHostPort(remote)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		ipStr = remote // OK; probably didn't have a port
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// Some IPv6-Adresses can contain zone identifiers at the end,
 | 
					 | 
				
			||||||
	// which are separated with "%"
 | 
					 | 
				
			||||||
	if strings.Contains(ipStr, "%") {
 | 
					 | 
				
			||||||
		split := strings.Split(ipStr, "%")
 | 
					 | 
				
			||||||
		ipStr = split[0]
 | 
					 | 
				
			||||||
		zoneID = split[1]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	ipAddr, err := netip.ParseAddr(ipStr)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return netip.IPv4Unspecified(), "", err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ipAddr, zoneID, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Match returns true if r matches m.
 | 
					 | 
				
			||||||
func (m MatchRemoteIP) Match(r *http.Request) bool {
 | 
					 | 
				
			||||||
	clientIP, zoneID, err := m.getClientIP(r)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		m.logger.Error("getting client IP", zap.Error(err))
 | 
					 | 
				
			||||||
		return false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	zoneFilter := true
 | 
					 | 
				
			||||||
	for i, ipRange := range m.cidrs {
 | 
					 | 
				
			||||||
		if ipRange.Contains(clientIP) {
 | 
					 | 
				
			||||||
			// Check if there are zone filters assigned and if they match.
 | 
					 | 
				
			||||||
			if m.zones[i] == "" || zoneID == m.zones[i] {
 | 
					 | 
				
			||||||
				return true
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			zoneFilter = false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if !zoneFilter {
 | 
					 | 
				
			||||||
		m.logger.Debug("zone ID from remote did not match", zap.String("zone", zoneID))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// MatchRegexp is an embedable type for matching
 | 
					// MatchRegexp is an embedable type for matching
 | 
				
			||||||
// using regular expressions. It adds placeholders
 | 
					// using regular expressions. It adds placeholders
 | 
				
			||||||
// to the request's replacer.
 | 
					// to the request's replacer.
 | 
				
			||||||
@ -1588,8 +1414,6 @@ var (
 | 
				
			|||||||
	_ RequestMatcher    = (*MatchHeaderRE)(nil)
 | 
						_ RequestMatcher    = (*MatchHeaderRE)(nil)
 | 
				
			||||||
	_ caddy.Provisioner = (*MatchHeaderRE)(nil)
 | 
						_ caddy.Provisioner = (*MatchHeaderRE)(nil)
 | 
				
			||||||
	_ RequestMatcher    = (*MatchProtocol)(nil)
 | 
						_ RequestMatcher    = (*MatchProtocol)(nil)
 | 
				
			||||||
	_ RequestMatcher    = (*MatchRemoteIP)(nil)
 | 
					 | 
				
			||||||
	_ caddy.Provisioner = (*MatchRemoteIP)(nil)
 | 
					 | 
				
			||||||
	_ RequestMatcher    = (*MatchNot)(nil)
 | 
						_ RequestMatcher    = (*MatchNot)(nil)
 | 
				
			||||||
	_ caddy.Provisioner = (*MatchNot)(nil)
 | 
						_ caddy.Provisioner = (*MatchNot)(nil)
 | 
				
			||||||
	_ caddy.Provisioner = (*MatchRegexp)(nil)
 | 
						_ caddy.Provisioner = (*MatchRegexp)(nil)
 | 
				
			||||||
@ -1602,7 +1426,6 @@ var (
 | 
				
			|||||||
	_ caddyfile.Unmarshaler = (*MatchHeader)(nil)
 | 
						_ caddyfile.Unmarshaler = (*MatchHeader)(nil)
 | 
				
			||||||
	_ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
 | 
						_ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
 | 
				
			||||||
	_ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
 | 
						_ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
 | 
				
			||||||
	_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
 | 
					 | 
				
			||||||
	_ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
 | 
						_ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
 | 
				
			||||||
	_ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
 | 
						_ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1614,7 +1437,6 @@ var (
 | 
				
			|||||||
	_ CELLibraryProducer = (*MatchHeader)(nil)
 | 
						_ CELLibraryProducer = (*MatchHeader)(nil)
 | 
				
			||||||
	_ CELLibraryProducer = (*MatchHeaderRE)(nil)
 | 
						_ CELLibraryProducer = (*MatchHeaderRE)(nil)
 | 
				
			||||||
	_ CELLibraryProducer = (*MatchProtocol)(nil)
 | 
						_ CELLibraryProducer = (*MatchProtocol)(nil)
 | 
				
			||||||
	_ CELLibraryProducer = (*MatchRemoteIP)(nil)
 | 
					 | 
				
			||||||
	// _ CELLibraryProducer = (*VarsMatcher)(nil)
 | 
						// _ CELLibraryProducer = (*VarsMatcher)(nil)
 | 
				
			||||||
	// _ CELLibraryProducer = (*MatchVarsRE)(nil)
 | 
						// _ CELLibraryProducer = (*MatchVarsRE)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -130,6 +130,17 @@ type Server struct {
 | 
				
			|||||||
	// to trust sensitive incoming `X-Forwarded-*` headers.
 | 
						// to trust sensitive incoming `X-Forwarded-*` headers.
 | 
				
			||||||
	TrustedProxiesRaw json.RawMessage `json:"trusted_proxies,omitempty" caddy:"namespace=http.ip_sources inline_key=source"`
 | 
						TrustedProxiesRaw json.RawMessage `json:"trusted_proxies,omitempty" caddy:"namespace=http.ip_sources inline_key=source"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The headers from which the client IP address could be
 | 
				
			||||||
 | 
						// read from. These will be considered in order, with the
 | 
				
			||||||
 | 
						// first good value being used as the client IP.
 | 
				
			||||||
 | 
						// By default, only `X-Forwarded-For` is considered.
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// This depends on `trusted_proxies` being configured and
 | 
				
			||||||
 | 
						// the request being validated as coming from a trusted
 | 
				
			||||||
 | 
						// proxy, otherwise the client IP will be set to the direct
 | 
				
			||||||
 | 
						// remote IP address.
 | 
				
			||||||
 | 
						ClientIPHeaders []string `json:"client_ip_headers,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Enables access logging and configures how access logs are handled
 | 
						// Enables access logging and configures how access logs are handled
 | 
				
			||||||
	// in this server. To minimally enable access logs, simply set this
 | 
						// in this server. To minimally enable access logs, simply set this
 | 
				
			||||||
	// to a non-null, empty struct.
 | 
						// to a non-null, empty struct.
 | 
				
			||||||
@ -690,10 +701,15 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
 | 
				
			|||||||
	// set up the context for the request
 | 
						// set up the context for the request
 | 
				
			||||||
	ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
 | 
						ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
 | 
				
			||||||
	ctx = context.WithValue(ctx, ServerCtxKey, s)
 | 
						ctx = context.WithValue(ctx, ServerCtxKey, s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						trusted, clientIP := determineTrustedProxy(r, s)
 | 
				
			||||||
	ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
 | 
						ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
 | 
				
			||||||
		TrustedProxyVarKey: determineTrustedProxy(r, s),
 | 
							TrustedProxyVarKey: trusted,
 | 
				
			||||||
 | 
							ClientIPVarKey:     clientIP,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{}))
 | 
						ctx = context.WithValue(ctx, routeGroupCtxKey, make(map[string]struct{}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var url2 url.URL // avoid letting this escape to the heap
 | 
						var url2 url.URL // avoid letting this escape to the heap
 | 
				
			||||||
	ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
 | 
						ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
 | 
				
			||||||
	r = r.WithContext(ctx)
 | 
						r = r.WithContext(ctx)
 | 
				
			||||||
@ -724,11 +740,12 @@ func originalRequest(req *http.Request, urlCopy *url.URL) http.Request {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// determineTrustedProxy parses the remote IP address of
 | 
					// determineTrustedProxy parses the remote IP address of
 | 
				
			||||||
// the request, and determines (if the server configured it)
 | 
					// the request, and determines (if the server configured it)
 | 
				
			||||||
// if the client is a trusted proxy.
 | 
					// if the client is a trusted proxy. If trusted, also returns
 | 
				
			||||||
func determineTrustedProxy(r *http.Request, s *Server) bool {
 | 
					// the real client IP if possible.
 | 
				
			||||||
 | 
					func determineTrustedProxy(r *http.Request, s *Server) (bool, string) {
 | 
				
			||||||
	// If there's no server, then we can't check anything
 | 
						// If there's no server, then we can't check anything
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		return false
 | 
							return false, ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse the remote IP, ignore the error as non-fatal,
 | 
						// Parse the remote IP, ignore the error as non-fatal,
 | 
				
			||||||
@ -738,7 +755,7 @@ func determineTrustedProxy(r *http.Request, s *Server) bool {
 | 
				
			|||||||
	// remote address and used an invalid value.
 | 
						// remote address and used an invalid value.
 | 
				
			||||||
	clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
 | 
						clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return false
 | 
							return false, ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Client IP may contain a zone if IPv6, so we need
 | 
						// Client IP may contain a zone if IPv6, so we need
 | 
				
			||||||
@ -746,20 +763,56 @@ func determineTrustedProxy(r *http.Request, s *Server) bool {
 | 
				
			|||||||
	clientIP, _, _ = strings.Cut(clientIP, "%")
 | 
						clientIP, _, _ = strings.Cut(clientIP, "%")
 | 
				
			||||||
	ipAddr, err := netip.ParseAddr(clientIP)
 | 
						ipAddr, err := netip.ParseAddr(clientIP)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return false
 | 
							return false, ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if the client is a trusted proxy
 | 
						// Check if the client is a trusted proxy
 | 
				
			||||||
	if s.trustedProxies == nil {
 | 
						if s.trustedProxies == nil {
 | 
				
			||||||
		return false
 | 
							return false, ipAddr.String()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	for _, ipRange := range s.trustedProxies.GetIPRanges(r) {
 | 
						for _, ipRange := range s.trustedProxies.GetIPRanges(r) {
 | 
				
			||||||
		if ipRange.Contains(ipAddr) {
 | 
							if ipRange.Contains(ipAddr) {
 | 
				
			||||||
			return true
 | 
								// We trust the proxy, so let's try to
 | 
				
			||||||
 | 
								// determine the real client IP
 | 
				
			||||||
 | 
								return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return false
 | 
						return false, ipAddr.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// trustedRealClientIP finds the client IP from the request assuming it is
 | 
				
			||||||
 | 
					// from a trusted client. If there is no client IP headers, then the
 | 
				
			||||||
 | 
					// direct remote address is returned. If there are client IP headers,
 | 
				
			||||||
 | 
					// then the first value from those headers is used.
 | 
				
			||||||
 | 
					func trustedRealClientIP(r *http.Request, headers []string, clientIP string) string {
 | 
				
			||||||
 | 
						// Read all the values of the configured client IP headers, in order
 | 
				
			||||||
 | 
						var values []string
 | 
				
			||||||
 | 
						for _, field := range headers {
 | 
				
			||||||
 | 
							values = append(values, r.Header.Values(field)...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we don't have any values, then give up
 | 
				
			||||||
 | 
						if len(values) == 0 {
 | 
				
			||||||
 | 
							return clientIP
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Since there can be many header values, we need to
 | 
				
			||||||
 | 
						// join them together before splitting to get the full list
 | 
				
			||||||
 | 
						allValues := strings.Split(strings.Join(values, ","), ",")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get first valid left-most IP address
 | 
				
			||||||
 | 
						for _, ip := range allValues {
 | 
				
			||||||
 | 
							ip, _, _ = strings.Cut(strings.TrimSpace(ip), "%")
 | 
				
			||||||
 | 
							ipAddr, err := netip.ParseAddr(ip)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return ipAddr.String()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We didn't find a valid IP
 | 
				
			||||||
 | 
						return clientIP
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// cloneURL makes a copy of r.URL and returns a
 | 
					// cloneURL makes a copy of r.URL and returns a
 | 
				
			||||||
@ -787,4 +840,7 @@ const (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// For tracking whether the client is a trusted proxy
 | 
						// For tracking whether the client is a trusted proxy
 | 
				
			||||||
	TrustedProxyVarKey string = "trusted_proxy"
 | 
						TrustedProxyVarKey string = "trusted_proxy"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// For tracking the real client IP (affected by trusted_proxy)
 | 
				
			||||||
 | 
						ClientIPVarKey string = "client_ip"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user