summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/caddytls/matchers.go104
-rw-r--r--modules/caddytls/matchers_test.go92
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) }