summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorFrancis Lavoie <lavofr@gmail.com>2023-02-06 14:44:11 -0500
committerGitHub <noreply@github.com>2023-02-06 12:44:11 -0700
commit12bcbe2c4924ecbf6730fc340a7a4250bddcc9be (patch)
treeae19f9b5969a5bfec041b1cd3c784135ce073aa8 /modules
parentf6f1d8fc8931ae9ed9ed9b948b559a6149232fbc (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.go23
-rw-r--r--modules/caddyhttp/ip_range.go142
-rw-r--r--modules/caddyhttp/server.go25
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