Skip to content

Commit

Permalink
connection policy: add local_ip matcher (#6074)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
mohammed90 and mholt authored Apr 15, 2024
1 parent b40cacf commit 26748d0
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 0 deletions.
78 changes: 78 additions & 0 deletions modules/caddytls/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
func init() {
caddy.RegisterModule(MatchServerName{})
caddy.RegisterModule(MatchRemoteIP{})
caddy.RegisterModule(MatchLocalIP{})
}

// MatchServerName matches based on SNI. Names in
Expand Down Expand Up @@ -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)
)
72 changes: 72 additions & 0 deletions modules/caddytls/matchers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 26748d0

Please sign in to comment.