From 26748d06b4a39f1e1d02863245573a7ecd1bebc4 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Mon, 15 Apr 2024 21:13:24 +0300 Subject: connection policy: add `local_ip` matcher (#6074) * connection policy: add `local_ip` Co-authored-by: Matt Holt --------- Co-authored-by: Matt Holt --- modules/caddytls/matchers.go | 78 +++++++++++++++++++++++++++++++++++++++ modules/caddytls/matchers_test.go | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) (limited to 'modules/caddytls') diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index af1f898bb..17bfe2e4c 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -30,6 +30,7 @@ import ( func init() { caddy.RegisterModule(MatchServerName{}) caddy.RegisterModule(MatchRemoteIP{}) + caddy.RegisterModule(MatchLocalIP{}) } // MatchServerName matches based on SNI. Names in @@ -144,8 +145,85 @@ func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool { return false } +// MatchLocalIP matches based on the IP address of the interface +// receiving the connection. Specific IPs or CIDR ranges can be specified. +type MatchLocalIP struct { + // The IPs or CIDR ranges to match. + Ranges []string `json:"ranges,omitempty"` + + cidrs []netip.Prefix + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (MatchLocalIP) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.handshake_match.local_ip", + New: func() caddy.Module { return new(MatchLocalIP) }, + } +} + +// Provision parses m's IP ranges, either from IP or CIDR expressions. +func (m *MatchLocalIP) Provision(ctx caddy.Context) error { + m.logger = ctx.Logger() + for _, str := range m.Ranges { + cidrs, err := m.parseIPRange(str) + if err != nil { + return err + } + m.cidrs = append(m.cidrs, cidrs...) + } + return nil +} + +// Match matches hello based on the connection's remote IP. +func (m MatchLocalIP) Match(hello *tls.ClientHelloInfo) bool { + localAddr := hello.Conn.LocalAddr().String() + ipStr, _, err := net.SplitHostPort(localAddr) + if err != nil { + ipStr = localAddr // weird; maybe no port? + } + ipAddr, err := netip.ParseAddr(ipStr) + if err != nil { + m.logger.Error("invalid local IP addresss", zap.String("ip", ipStr)) + return false + } + return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs)) +} + +func (MatchLocalIP) parseIPRange(str string) ([]netip.Prefix, error) { + var cidrs []netip.Prefix + if strings.Contains(str, "/") { + ipNet, err := netip.ParsePrefix(str) + if err != nil { + return nil, fmt.Errorf("parsing CIDR expression: %v", err) + } + cidrs = append(cidrs, ipNet) + } else { + ipAddr, err := netip.ParseAddr(str) + if err != nil { + return nil, fmt.Errorf("invalid IP address: '%s': %v", str, err) + } + ip := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) + cidrs = append(cidrs, ip) + } + return cidrs, nil +} + +func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool { + for _, ipRange := range ranges { + if ipRange.Contains(ip) { + return true + } + } + return false +} + // Interface guards var ( _ ConnectionMatcher = (*MatchServerName)(nil) _ ConnectionMatcher = (*MatchRemoteIP)(nil) + + _ caddy.Provisioner = (*MatchLocalIP)(nil) + _ ConnectionMatcher = (*MatchLocalIP)(nil) ) diff --git a/modules/caddytls/matchers_test.go b/modules/caddytls/matchers_test.go index 4522b3377..54dfdb9c4 100644 --- a/modules/caddytls/matchers_test.go +++ b/modules/caddytls/matchers_test.go @@ -165,12 +165,84 @@ func TestRemoteIPMatcher(t *testing.T) { } } +func TestLocalIPMatcher(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + for i, tc := range []struct { + ranges []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, + }, + { + input: "127.0.0.1:12345", + expect: true, + }, + { + ranges: []string{"127.0.0.1"}, + input: "127.0.0.1:12345", + expect: true, + }, + { + ranges: []string{"127.0.0.2"}, + input: "127.0.0.3:12345", + expect: false, + }, + { + ranges: []string{"127.0.0.2"}, + input: "127.0.0.2", + expect: true, + }, + { + ranges: []string{"127.0.0.2"}, + input: "127.0.0.300", + expect: false, + }, + } { + matcher := MatchLocalIP{Ranges: tc.ranges} + 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)", + i, tc.expect, actual, tc.input, tc.ranges) + } + } +} + type testConn struct { *net.TCPConn addr testAddr } func (tc testConn) RemoteAddr() net.Addr { return tc.addr } +func (tc testConn) LocalAddr() net.Addr { return tc.addr } type testAddr string -- cgit v1.2.3