aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMohammed Al Sahaf <[email protected]>2024-04-15 21:13:24 +0300
committerGitHub <[email protected]>2024-04-15 21:13:24 +0300
commit26748d06b4a39f1e1d02863245573a7ecd1bebc4 (patch)
treee06f921129e90161badb7d4837cf1f05b6d1b759
parentb40cacf5ce8f6a190a002fa44e973f1aa5bac2b0 (diff)
downloadcaddy-26748d06b4a39f1e1d02863245573a7ecd1bebc4.tar.gz
caddy-26748d06b4a39f1e1d02863245573a7ecd1bebc4.zip
connection policy: add `local_ip` matcher (#6074)
* connection policy: add `local_ip` Co-authored-by: Matt Holt <[email protected]> --------- Co-authored-by: Matt Holt <[email protected]>
-rw-r--r--modules/caddytls/matchers.go78
-rw-r--r--modules/caddytls/matchers_test.go72
2 files changed, 150 insertions, 0 deletions
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