mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-24 23:39:19 -04:00 
			
		
		
		
	
		
			Some checks failed
		
		
	
	Tests / test (./cmd/caddy/caddy, ~1.22.3, ubuntu-latest, 0, 1.22, linux) (push) Failing after 2m43s
				
			Tests / test (./cmd/caddy/caddy, ~1.23.0, ubuntu-latest, 0, 1.23, linux) (push) Failing after 2m26s
				
			Tests / test (s390x on IBM Z) (push) Has been skipped
				
			Tests / goreleaser-check (push) Successful in 40s
				
			Cross-Build / build (~1.22.3, 1.22, aix) (push) Successful in 2m55s
				
			Cross-Build / build (~1.22.3, 1.22, dragonfly) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, freebsd) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, netbsd) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, windows) (push) Successful in 3m1s
				
			Cross-Build / build (~1.22.3, 1.22, darwin) (push) Failing after 13m23s
				
			Cross-Build / build (~1.23.0, 1.23, aix) (push) Successful in 2m40s
				
			Cross-Build / build (~1.22.3, 1.22, illumos) (push) Failing after 11m51s
				
			Cross-Build / build (~1.22.3, 1.22, linux) (push) Failing after 11m47s
				
			Cross-Build / build (~1.23.0, 1.23, darwin) (push) Successful in 2m29s
				
			Cross-Build / build (~1.23.0, 1.23, linux) (push) Successful in 2m39s
				
			Cross-Build / build (~1.22.3, 1.22, openbsd) (push) Failing after 13m27s
				
			Cross-Build / build (~1.22.3, 1.22, solaris) (push) Failing after 13m27s
				
			Cross-Build / build (~1.23.0, 1.23, netbsd) (push) Successful in 2m41s
				
			Cross-Build / build (~1.23.0, 1.23, openbsd) (push) Successful in 2m54s
				
			Cross-Build / build (~1.23.0, 1.23, solaris) (push) Successful in 2m40s
				
			Cross-Build / build (~1.23.0, 1.23, dragonfly) (push) Failing after 14m19s
				
			Cross-Build / build (~1.23.0, 1.23, freebsd) (push) Failing after 14m10s
				
			Cross-Build / build (~1.23.0, 1.23, illumos) (push) Failing after 14m2s
				
			Cross-Build / build (~1.23.0, 1.23, windows) (push) Failing after 11m58s
				
			Lint / lint (ubuntu-latest, linux) (push) Failing after 3m44s
				
			Lint / govulncheck (push) Successful in 2m18s
				
			Tests / test (./cmd/caddy/caddy, ~1.22.3, macos-14, 0, 1.22, mac) (push) Has been cancelled
				
			Tests / test (./cmd/caddy/caddy, ~1.23.0, macos-14, 0, 1.23, mac) (push) Has been cancelled
				
			Tests / test (./cmd/caddy/caddy.exe, ~1.22.3, windows-latest, True, 1.22, windows) (push) Has been cancelled
				
			Tests / test (./cmd/caddy/caddy.exe, ~1.23.0, windows-latest, True, 1.23, windows) (push) Has been cancelled
				
			Lint / lint (macos-14, mac) (push) Has been cancelled
				
			Lint / lint (windows-latest, windows) (push) Has been cancelled
				
			* perf: use zap's Check() to prevent useless allocs * fix * fix * fix * fix * restore previous replacer behavior * fix linter
		
			
				
	
	
		
			339 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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/google/cel-go/cel"
 | |
| 	"github.com/google/cel-go/common/types/ref"
 | |
| 	"go.uber.org/zap"
 | |
| 	"go.uber.org/zap/zapcore"
 | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 | |
| 	"github.com/caddyserver/caddy/v2/internal"
 | |
| )
 | |
| 
 | |
| // 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"`
 | |
| 
 | |
| 	// 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 {
 | |
| 	// iterate to merge multiple matchers into one
 | |
| 	for d.Next() {
 | |
| 		for d.NextArg() {
 | |
| 			if d.Val() == "forwarded" {
 | |
| 				return d.Err("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead")
 | |
| 			}
 | |
| 			if d.Val() == "private_ranges" {
 | |
| 				m.Ranges = append(m.Ranges, internal.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('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" {
 | |
| 					return nil, errors.New("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead")
 | |
| 				}
 | |
| 				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
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Match returns true if r matches m.
 | |
| func (m MatchRemoteIP) Match(r *http.Request) bool {
 | |
| 	if r.TLS != nil && !r.TLS.HandshakeComplete {
 | |
| 		return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
 | |
| 	}
 | |
| 	address := r.RemoteAddr
 | |
| 	clientIP, zoneID, err := parseIPZoneFromString(address)
 | |
| 	if err != nil {
 | |
| 		if c := m.logger.Check(zapcore.ErrorLevel, "getting remote "); c != nil {
 | |
| 			c.Write(zap.Error(err))
 | |
| 		}
 | |
| 
 | |
| 		return false
 | |
| 	}
 | |
| 	matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
 | |
| 	if !matches && !zoneFilter {
 | |
| 		if c := m.logger.Check(zapcore.DebugLevel, "zone ID from remote IP did not match"); c != nil {
 | |
| 			c.Write(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 {
 | |
| 	// iterate to merge multiple matchers into one
 | |
| 	for d.Next() {
 | |
| 		for d.NextArg() {
 | |
| 			if d.Val() == "private_ranges" {
 | |
| 				m.Ranges = append(m.Ranges, internal.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 {
 | |
| 	if r.TLS != nil && !r.TLS.HandshakeComplete {
 | |
| 		return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
 | |
| 	}
 | |
| 	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{}
 | |
| 	repl := caddy.NewReplacer()
 | |
| 	for _, str := range ranges {
 | |
| 		str = repl.ReplaceAll(str, "")
 | |
| 		// 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-Addresses 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)
 | |
| )
 |