diff options
Diffstat (limited to 'modules/caddytls')
| -rw-r--r-- | modules/caddytls/matchers.go | 104 | ||||
| -rw-r--r-- | modules/caddytls/matchers_test.go | 92 | 
2 files changed, 194 insertions, 2 deletions
diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index 50da609..aee0e72 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -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) +) diff --git a/modules/caddytls/matchers_test.go b/modules/caddytls/matchers_test.go index 24a015a..4522b33 100644 --- a/modules/caddytls/matchers_test.go +++ b/modules/caddytls/matchers_test.go @@ -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) }  | 
