mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 05:11:04 +00: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:
		
							parent
							
								
									330be2d8c7
								
							
						
					
					
						commit
						05e9974570
					
				
					 9 changed files with 462 additions and 187 deletions
				
			
		|  | @ -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" | ||||||
| ) | ) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Francis Lavoie
						Francis Lavoie