From 956f01163d150b70aa3d5fc418ba9f150bd25c24 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 30 Apr 2021 10:14:52 -0600 Subject: caddytls: Implement remote IP connection matcher (#4123) * caddytls: Implement remote IP connection matcher * Implement IP range negation If both Ranges and NotRanges are specified, both must match. --- modules/caddytls/matchers.go | 104 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) (limited to 'modules/caddytls/matchers.go') 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) +) -- cgit v1.2.3