mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-01 19:17:25 -04:00 
			
		
		
		
	caddytls: Implement remote IP connection matcher (#4123)
* caddytls: Implement remote IP connection matcher * Implement IP range negation If both Ranges and NotRanges are specified, both must match.
This commit is contained in:
		
							parent
							
								
									ff6ca577ec
								
							
						
					
					
						commit
						956f01163d
					
				| @ -430,5 +430,13 @@ func (ctx Context) Storage() certmagic.Storage { | ||||
| 
 | ||||
| // Logger returns a logger that can be used by mod. | ||||
| func (ctx Context) Logger(mod Module) *zap.Logger { | ||||
| 	if ctx.cfg == nil { | ||||
| 		// often the case in tests; just use a dev logger | ||||
| 		l, err := zap.NewDevelopment() | ||||
| 		if err != nil { | ||||
| 			panic("config missing, unable to create dev logger: " + err.Error()) | ||||
| 		} | ||||
| 		return l | ||||
| 	} | ||||
| 	return ctx.cfg.Logging.Logger(mod) | ||||
| } | ||||
|  | ||||
| @ -16,13 +16,18 @@ package caddytls | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/caddyserver/caddy/v2" | ||||
| 	"github.com/caddyserver/certmagic" | ||||
| 	"go.uber.org/zap" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	caddy.RegisterModule(MatchServerName{}) | ||||
| 	caddy.RegisterModule(MatchRemoteIP{}) | ||||
| } | ||||
| 
 | ||||
| // MatchServerName matches based on SNI. Names in | ||||
| @ -48,5 +53,100 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Interface guard | ||||
| var _ ConnectionMatcher = (*MatchServerName)(nil) | ||||
| // MatchRemoteIP matches based on the remote IP of the | ||||
| // connection. Specific IPs or CIDR ranges can be specified. | ||||
| // | ||||
| // Note that IPs can sometimes be spoofed, so do not rely | ||||
| // on this as a replacement for actual authentication. | ||||
| type MatchRemoteIP struct { | ||||
| 	// The IPs or CIDR ranges to match. | ||||
| 	Ranges []string `json:"ranges,omitempty"` | ||||
| 
 | ||||
| 	// The IPs or CIDR ranges to *NOT* match. | ||||
| 	NotRanges []string `json:"not_ranges,omitempty"` | ||||
| 
 | ||||
| 	cidrs    []*net.IPNet | ||||
| 	notCidrs []*net.IPNet | ||||
| 	logger   *zap.Logger | ||||
| } | ||||
| 
 | ||||
| // CaddyModule returns the Caddy module information. | ||||
| func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { | ||||
| 	return caddy.ModuleInfo{ | ||||
| 		ID:  "tls.handshake_match.remote_ip", | ||||
| 		New: func() caddy.Module { return new(MatchRemoteIP) }, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Provision parses m's IP ranges, either from IP or CIDR expressions. | ||||
| func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { | ||||
| 	m.logger = ctx.Logger(m) | ||||
| 	for _, str := range m.Ranges { | ||||
| 		cidrs, err := m.parseIPRange(str) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		m.cidrs = cidrs | ||||
| 	} | ||||
| 	for _, str := range m.NotRanges { | ||||
| 		cidrs, err := m.parseIPRange(str) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		m.notCidrs = cidrs | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Match matches hello based on the connection's remote IP. | ||||
| func (m MatchRemoteIP) Match(hello *tls.ClientHelloInfo) bool { | ||||
| 	remoteAddr := hello.Conn.RemoteAddr().String() | ||||
| 	ipStr, _, err := net.SplitHostPort(remoteAddr) | ||||
| 	if err != nil { | ||||
| 		ipStr = remoteAddr // weird; maybe no port? | ||||
| 	} | ||||
| 	ip := net.ParseIP(ipStr) | ||||
| 	if ip == nil { | ||||
| 		m.logger.Error("invalid client IP addresss", zap.String("ip", ipStr)) | ||||
| 		return false | ||||
| 	} | ||||
| 	return (len(m.cidrs) == 0 || m.matches(ip, m.cidrs)) && | ||||
| 		(len(m.notCidrs) == 0 || !m.matches(ip, m.notCidrs)) | ||||
| } | ||||
| 
 | ||||
| func (MatchRemoteIP) parseIPRange(str string) ([]*net.IPNet, error) { | ||||
| 	var cidrs []*net.IPNet | ||||
| 	if strings.Contains(str, "/") { | ||||
| 		_, ipNet, err := net.ParseCIDR(str) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("parsing CIDR expression: %v", err) | ||||
| 		} | ||||
| 		cidrs = append(cidrs, ipNet) | ||||
| 	} else { | ||||
| 		ip := net.ParseIP(str) | ||||
| 		if ip == nil { | ||||
| 			return nil, fmt.Errorf("invalid IP address: %s", str) | ||||
| 		} | ||||
| 		mask := len(ip) * 8 | ||||
| 		cidrs = append(cidrs, &net.IPNet{ | ||||
| 			IP:   ip, | ||||
| 			Mask: net.CIDRMask(mask, mask), | ||||
| 		}) | ||||
| 	} | ||||
| 	return cidrs, nil | ||||
| } | ||||
| 
 | ||||
| func (MatchRemoteIP) matches(ip net.IP, ranges []*net.IPNet) bool { | ||||
| 	for _, ipRange := range ranges { | ||||
| 		if ipRange.Contains(ip) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Interface guards | ||||
| var ( | ||||
| 	_ ConnectionMatcher = (*MatchServerName)(nil) | ||||
| 	_ ConnectionMatcher = (*MatchRemoteIP)(nil) | ||||
| ) | ||||
|  | ||||
| @ -15,8 +15,12 @@ | ||||
| package caddytls | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"net" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/caddyserver/caddy/v2" | ||||
| ) | ||||
| 
 | ||||
| func TestServerNameMatcher(t *testing.T) { | ||||
| @ -84,3 +88,91 @@ func TestServerNameMatcher(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestRemoteIPMatcher(t *testing.T) { | ||||
| 	ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) | ||||
| 	defer cancel() | ||||
| 
 | ||||
| 	for i, tc := range []struct { | ||||
| 		ranges    []string | ||||
| 		notRanges []string | ||||
| 		input     string | ||||
| 		expect    bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			ranges: []string{"127.0.0.1"}, | ||||
| 			input:  "127.0.0.1:12345", | ||||
| 			expect: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ranges: []string{"127.0.0.1"}, | ||||
| 			input:  "127.0.0.2:12345", | ||||
| 			expect: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ranges: []string{"127.0.0.1/16"}, | ||||
| 			input:  "127.0.1.23:12345", | ||||
| 			expect: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ranges: []string{"127.0.0.1", "192.168.1.105"}, | ||||
| 			input:  "192.168.1.105:12345", | ||||
| 			expect: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			notRanges: []string{"127.0.0.1"}, | ||||
| 			input:     "127.0.0.1:12345", | ||||
| 			expect:    false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			notRanges: []string{"127.0.0.2"}, | ||||
| 			input:     "127.0.0.1:12345", | ||||
| 			expect:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ranges:    []string{"127.0.0.1"}, | ||||
| 			notRanges: []string{"127.0.0.2"}, | ||||
| 			input:     "127.0.0.1:12345", | ||||
| 			expect:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ranges:    []string{"127.0.0.2"}, | ||||
| 			notRanges: []string{"127.0.0.2"}, | ||||
| 			input:     "127.0.0.2:12345", | ||||
| 			expect:    false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ranges:    []string{"127.0.0.2"}, | ||||
| 			notRanges: []string{"127.0.0.2"}, | ||||
| 			input:     "127.0.0.3:12345", | ||||
| 			expect:    false, | ||||
| 		}, | ||||
| 	} { | ||||
| 		matcher := MatchRemoteIP{Ranges: tc.ranges, NotRanges: tc.notRanges} | ||||
| 		err := matcher.Provision(ctx) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Test %d: Provision failed: %v", i, err) | ||||
| 		} | ||||
| 
 | ||||
| 		addr := testAddr(tc.input) | ||||
| 		chi := &tls.ClientHelloInfo{Conn: testConn{addr: addr}} | ||||
| 
 | ||||
| 		actual := matcher.Match(chi) | ||||
| 		if actual != tc.expect { | ||||
| 			t.Errorf("Test %d: Expected %t but got %t (input=%s ranges=%v notRanges=%v)", | ||||
| 				i, tc.expect, actual, tc.input, tc.ranges, tc.notRanges) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type testConn struct { | ||||
| 	*net.TCPConn | ||||
| 	addr testAddr | ||||
| } | ||||
| 
 | ||||
| func (tc testConn) RemoteAddr() net.Addr { return tc.addr } | ||||
| 
 | ||||
| type testAddr string | ||||
| 
 | ||||
| func (testAddr) Network() string   { return "tcp" } | ||||
| func (ta testAddr) String() string { return string(ta) } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user