Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

connection policy: add local_ip matcher #6074

Merged
merged 6 commits into from
Apr 15, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions modules/caddytls/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package caddytls

import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/netip"
Expand All @@ -30,6 +31,7 @@ import (
func init() {
caddy.RegisterModule(MatchServerName{})
caddy.RegisterModule(MatchRemoteIP{})
caddy.RegisterModule(MatchNot{})
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
}

// MatchServerName matches based on SNI. Names in
Expand Down Expand Up @@ -65,6 +67,7 @@ type MatchRemoteIP struct {
Ranges []string `json:"ranges,omitempty"`

// The IPs or CIDR ranges to *NOT* match.
// Deprecated: Use `not` matcher instead.
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
NotRanges []string `json:"not_ranges,omitempty"`

cidrs []netip.Prefix
Expand Down Expand Up @@ -144,8 +147,179 @@ 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
}

// RawMatcherSets is a group of matcher sets
// in their raw, JSON form.
type (
RawMatcherSets []caddy.ModuleMap
MatcherSet []ConnectionMatcher
MatcherSets []MatcherSet
)

func (mset MatcherSet) Match(hello *tls.ClientHelloInfo) bool {
for _, m := range mset {
if !m.Match(hello) {
return false
}
}
return true
}

func (ms MatcherSets) AnyMatch(hello *tls.ClientHelloInfo) bool {
for _, mset := range ms {
if mset.Match(hello) {
return true
}
}
return false
}

type MatchNot struct {
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
MatcherSetRaw RawMatcherSets `json:"match,omitempty" caddy:"namespace=tls.handshake_match"`
matchers MatcherSet `json:"-"`
}

func (ms *MatcherSets) FromInterface(matcherSets any) error {
for _, matcherSetIfaces := range matcherSets.([]map[string]any) {
var matcherSet MatcherSet
for _, matcher := range matcherSetIfaces {
reqMatcher, ok := matcher.(ConnectionMatcher)
if !ok {
return fmt.Errorf("decoded module is not a ConnectionMatcher: %#v", matcher)
}
matcherSet = append(matcherSet, reqMatcher)
}
*ms = append(*ms, matcherSet)
}
return nil
}

// UnmarshalJSON satisfies json.Unmarshaler. It puts the JSON
// bytes directly into m's MatcherSetsRaw field.
func (m *MatchNot) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &m.MatcherSetRaw)
}

// MarshalJSON satisfies json.Marshaler by marshaling
// m's raw matcher sets.
func (m MatchNot) MarshalJSON() ([]byte, error) {
return json.Marshal(m.MatcherSetRaw)
}

// CaddyModule implements caddy.Module.
func (MatchNot) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.handshake_match.not",
New: func() caddy.Module {
return new(MatchNot)
},
}
}

// Provision implements caddy.Provisioner.
func (m *MatchNot) Provision(ctx caddy.Context) error {
matcherSets, err := ctx.LoadModule(m, "MatcherSetRaw")
if err != nil {
return fmt.Errorf("loading matcher sets: %v", err)
}
for _, modMap := range matcherSets.([]map[string]any) {
var ms MatcherSet
for _, modIface := range modMap {
ms = append(ms, modIface.(ConnectionMatcher))
}
m.matchers = append(m.matchers, ms)
}
return nil
}

// Match implements ConnectionMatcher.
func (mc MatchNot) Match(hello *tls.ClientHelloInfo) bool {
for _, ms := range mc.matchers {
if ms.Match(hello) {
return false
}
}
return true
}

// Interface guards
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
var (
_ ConnectionMatcher = (*MatchServerName)(nil)
_ ConnectionMatcher = (*MatchRemoteIP)(nil)

_ caddy.Provisioner = (*MatchNot)(nil)
_ ConnectionMatcher = (*MatchNot)(nil)
)
Loading