connection policy: add local_ip matcher (#6074)

* connection policy: add `local_ip`

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
This commit is contained in:
Mohammed Al Sahaf 2024-04-15 21:13:24 +03:00 committed by GitHub
parent b40cacf5ce
commit 26748d06b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 150 additions and 0 deletions

View file

@ -30,6 +30,7 @@ import (
func init() { func init() {
caddy.RegisterModule(MatchServerName{}) caddy.RegisterModule(MatchServerName{})
caddy.RegisterModule(MatchRemoteIP{}) caddy.RegisterModule(MatchRemoteIP{})
caddy.RegisterModule(MatchLocalIP{})
} }
// MatchServerName matches based on SNI. Names in // MatchServerName matches based on SNI. Names in
@ -144,8 +145,85 @@ func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
return false 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 // Interface guards
var ( var (
_ ConnectionMatcher = (*MatchServerName)(nil) _ ConnectionMatcher = (*MatchServerName)(nil)
_ ConnectionMatcher = (*MatchRemoteIP)(nil) _ ConnectionMatcher = (*MatchRemoteIP)(nil)
_ caddy.Provisioner = (*MatchLocalIP)(nil)
_ ConnectionMatcher = (*MatchLocalIP)(nil)
) )

View file

@ -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 { type testConn struct {
*net.TCPConn *net.TCPConn
addr testAddr addr testAddr
} }
func (tc testConn) RemoteAddr() net.Addr { return tc.addr } func (tc testConn) RemoteAddr() net.Addr { return tc.addr }
func (tc testConn) LocalAddr() net.Addr { return tc.addr }
type testAddr string type testAddr string