diff options
| author | Francis Lavoie <lavofr@gmail.com> | 2023-02-06 14:44:11 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-06 12:44:11 -0700 | 
| commit | 12bcbe2c4924ecbf6730fc340a7a4250bddcc9be (patch) | |
| tree | ae19f9b5969a5bfec041b1cd3c784135ce073aa8 /modules | |
| parent | f6f1d8fc8931ae9ed9ed9b948b559a6149232fbc (diff) | |
caddyhttp: Pluggable trusted proxy IP range sources (#5328)
* caddyhttp: Pluggable trusted proxy IP range sources
* Add request to the IPRangeSource interface
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/caddyhttp/app.go | 23 | ||||
| -rw-r--r-- | modules/caddyhttp/ip_range.go | 142 | ||||
| -rw-r--r-- | modules/caddyhttp/server.go | 25 | 
3 files changed, 155 insertions, 35 deletions
| diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index da25d37..0ec80ce 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -20,9 +20,7 @@ import (  	"fmt"  	"net"  	"net/http" -	"net/netip"  	"strconv" -	"strings"  	"sync"  	"time" @@ -224,22 +222,13 @@ func (app *App) Provision(ctx caddy.Context) error {  			srv.StrictSNIHost = &trueBool  		} -		// parse trusted proxy CIDRs ahead of time -		for _, str := range srv.TrustedProxies { -			if strings.Contains(str, "/") { -				ipNet, err := netip.ParsePrefix(str) -				if err != nil { -					return fmt.Errorf("parsing CIDR expression: '%s': %v", str, err) -				} -				srv.trustedProxies = append(srv.trustedProxies, 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()) -				srv.trustedProxies = append(srv.trustedProxies, ipNew) +		// set up the trusted proxies source +		for srv.TrustedProxiesRaw != nil { +			val, err := ctx.LoadModule(srv, "TrustedProxiesRaw") +			if err != nil { +				return fmt.Errorf("loading trusted proxies modules: %v", err)  			} +			srv.trustedProxies = val.(IPRangeSource)  		}  		// process each listener address diff --git a/modules/caddyhttp/ip_range.go b/modules/caddyhttp/ip_range.go new file mode 100644 index 0000000..b1db254 --- /dev/null +++ b/modules/caddyhttp/ip_range.go @@ -0,0 +1,142 @@ +// 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 ( +	"fmt" +	"net/http" +	"net/netip" +	"strings" + +	"github.com/caddyserver/caddy/v2" +	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func init() { +	caddy.RegisterModule(StaticIPRange{}) +} + +// IPRangeSource gets a list of IP ranges. +// +// The request is passed as an argument to allow plugin implementations +// to have more flexibility. But, a plugin MUST NOT modify the request. +// The caller will have read the `r.RemoteAddr` before getting IP ranges. +// +// This should be a very fast function -- instant if possible. +// The list of IP ranges should be sourced as soon as possible if loaded +// from an external source (i.e. initially loaded during Provisioning), +// so that it's ready to be used when requests start getting handled. +// A read lock should probably be used to get the cached value if the +// ranges can change at runtime (e.g. periodically refreshed). +// Using a `caddy.UsagePool` may be a good idea to avoid having refetch +// the values when a config reload occurs, which would waste time. +// +// If the list of IP ranges cannot be sourced, then provisioning SHOULD +// fail. Getting the IP ranges at runtime MUST NOT fail, because it would +// cancel incoming requests. If refreshing the list fails, then the +// previous list of IP ranges should continue to be returned so that the +// server can continue to operate normally. +type IPRangeSource interface { +	GetIPRanges(*http.Request) []netip.Prefix +} + +// StaticIPRange provides a static range of IP address prefixes (CIDRs). +type StaticIPRange struct { +	// A static list of IP ranges (supports CIDR notation). +	Ranges []string `json:"ranges,omitempty"` + +	// Holds the parsed CIDR ranges from Ranges. +	ranges []netip.Prefix +} + +// CaddyModule returns the Caddy module information. +func (StaticIPRange) CaddyModule() caddy.ModuleInfo { +	return caddy.ModuleInfo{ +		ID:  "http.ip_sources.static", +		New: func() caddy.Module { return new(StaticIPRange) }, +	} +} + +func (s *StaticIPRange) Provision(ctx caddy.Context) error { +	for _, str := range s.Ranges { +		prefix, err := CIDRExpressionToPrefix(str) +		if err != nil { +			return err +		} +		s.ranges = append(s.ranges, prefix) +	} + +	return nil +} + +func (s *StaticIPRange) GetIPRanges(_ *http.Request) []netip.Prefix { +	return s.ranges +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *StaticIPRange) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { +	if !d.Next() { +		return nil +	} +	for d.NextArg() { +		if d.Val() == "private_ranges" { +			m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) +			continue +		} +		m.Ranges = append(m.Ranges, d.Val()) +	} +	return nil +} + +// CIDRExpressionToPrefix takes a string which could be either a +// CIDR expression or a single IP address, and returns a netip.Prefix. +func CIDRExpressionToPrefix(expr string) (netip.Prefix, error) { +	// Having a slash means it should be a CIDR expression +	if strings.Contains(expr, "/") { +		prefix, err := netip.ParsePrefix(expr) +		if err != nil { +			return netip.Prefix{}, fmt.Errorf("parsing CIDR expression: '%s': %v", expr, err) +		} +		return prefix, nil +	} + +	// Otherwise it's likely a single IP address +	parsed, err := netip.ParseAddr(expr) +	if err != nil { +		return netip.Prefix{}, fmt.Errorf("invalid IP address: '%s': %v", expr, err) +	} +	prefix := netip.PrefixFrom(parsed, parsed.BitLen()) +	return prefix, nil +} + +// PrivateRangesCIDR returns a list of private CIDR range +// strings, which can be used as a configuration shortcut. +func PrivateRangesCIDR() []string { +	return []string{ +		"192.168.0.0/16", +		"172.16.0.0/12", +		"10.0.0.0/8", +		"127.0.0.1/8", +		"fd00::/8", +		"::1", +	} +} + +// Interface guards +var ( +	_ caddy.Provisioner     = (*StaticIPRange)(nil) +	_ caddyfile.Unmarshaler = (*StaticIPRange)(nil) +	_ IPRangeSource         = (*StaticIPRange)(nil) +) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 420cc4a..13ebbe6 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -118,7 +118,7 @@ type Server struct {  	// client authentication.  	StrictSNIHost *bool `json:"strict_sni_host,omitempty"` -	// A list of IP ranges (supports CIDR notation) from which +	// A module which provides a source of IP ranges, from which  	// requests should be trusted. By default, no proxies are  	// trusted.  	// @@ -128,7 +128,7 @@ type Server struct {  	// of needing to configure each of them. See the  	// `reverse_proxy` handler for example, which uses this  	// to trust sensitive incoming `X-Forwarded-*` headers. -	TrustedProxies []string `json:"trusted_proxies,omitempty"` +	TrustedProxiesRaw json.RawMessage `json:"trusted_proxies,omitempty" caddy:"namespace=http.ip_sources inline_key=source"`  	// Enables access logging and configures how access logs are handled  	// in this server. To minimally enable access logs, simply set this @@ -188,8 +188,7 @@ type Server struct {  	h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create  	addresses   []caddy.NetworkAddress -	// Holds the parsed CIDR ranges from TrustedProxies -	trustedProxies []netip.Prefix +	trustedProxies IPRangeSource  	shutdownAt   time.Time  	shutdownAtMu *sync.RWMutex @@ -751,7 +750,10 @@ func determineTrustedProxy(r *http.Request, s *Server) bool {  	}  	// Check if the client is a trusted proxy -	for _, ipRange := range s.trustedProxies { +	if s.trustedProxies == nil { +		return false +	} +	for _, ipRange := range s.trustedProxies.GetIPRanges(r) {  		if ipRange.Contains(ipAddr) {  			return true  		} @@ -771,19 +773,6 @@ func cloneURL(from, to *url.URL) {  	}  } -// PrivateRangesCIDR returns a list of private CIDR range -// strings, which can be used as a configuration shortcut. -func PrivateRangesCIDR() []string { -	return []string{ -		"192.168.0.0/16", -		"172.16.0.0/12", -		"10.0.0.0/8", -		"127.0.0.1/8", -		"fd00::/8", -		"::1", -	} -} -  // Context keys for HTTP request context values.  const (  	// For referencing the server instance | 
