Skip to content

Commit 956f011

Browse files
authored
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.
1 parent ff6ca57 commit 956f011

File tree

3 files changed

+202
-2
lines changed

3 files changed

+202
-2
lines changed

context.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,5 +430,13 @@ func (ctx Context) Storage() certmagic.Storage {
430430

431431
// Logger returns a logger that can be used by mod.
432432
func (ctx Context) Logger(mod Module) *zap.Logger {
433+
if ctx.cfg == nil {
434+
// often the case in tests; just use a dev logger
435+
l, err := zap.NewDevelopment()
436+
if err != nil {
437+
panic("config missing, unable to create dev logger: " + err.Error())
438+
}
439+
return l
440+
}
433441
return ctx.cfg.Logging.Logger(mod)
434442
}

modules/caddytls/matchers.go

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@ package caddytls
1616

1717
import (
1818
"crypto/tls"
19+
"fmt"
20+
"net"
21+
"strings"
1922

2023
"github.com/caddyserver/caddy/v2"
2124
"github.com/caddyserver/certmagic"
25+
"go.uber.org/zap"
2226
)
2327

2428
func init() {
2529
caddy.RegisterModule(MatchServerName{})
30+
caddy.RegisterModule(MatchRemoteIP{})
2631
}
2732

2833
// MatchServerName matches based on SNI. Names in
@@ -48,5 +53,100 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
4853
return false
4954
}
5055

51-
// Interface guard
52-
var _ ConnectionMatcher = (*MatchServerName)(nil)
56+
// MatchRemoteIP matches based on the remote IP of the
57+
// connection. Specific IPs or CIDR ranges can be specified.
58+
//
59+
// Note that IPs can sometimes be spoofed, so do not rely
60+
// on this as a replacement for actual authentication.
61+
type MatchRemoteIP struct {
62+
// The IPs or CIDR ranges to match.
63+
Ranges []string `json:"ranges,omitempty"`
64+
65+
// The IPs or CIDR ranges to *NOT* match.
66+
NotRanges []string `json:"not_ranges,omitempty"`
67+
68+
cidrs []*net.IPNet
69+
notCidrs []*net.IPNet
70+
logger *zap.Logger
71+
}
72+
73+
// CaddyModule returns the Caddy module information.
74+
func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
75+
return caddy.ModuleInfo{
76+
ID: "tls.handshake_match.remote_ip",
77+
New: func() caddy.Module { return new(MatchRemoteIP) },
78+
}
79+
}
80+
81+
// Provision parses m's IP ranges, either from IP or CIDR expressions.
82+
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
83+
m.logger = ctx.Logger(m)
84+
for _, str := range m.Ranges {
85+
cidrs, err := m.parseIPRange(str)
86+
if err != nil {
87+
return err
88+
}
89+
m.cidrs = cidrs
90+
}
91+
for _, str := range m.NotRanges {
92+
cidrs, err := m.parseIPRange(str)
93+
if err != nil {
94+
return err
95+
}
96+
m.notCidrs = cidrs
97+
}
98+
return nil
99+
}
100+
101+
// Match matches hello based on the connection's remote IP.
102+
func (m MatchRemoteIP) Match(hello *tls.ClientHelloInfo) bool {
103+
remoteAddr := hello.Conn.RemoteAddr().String()
104+
ipStr, _, err := net.SplitHostPort(remoteAddr)
105+
if err != nil {
106+
ipStr = remoteAddr // weird; maybe no port?
107+
}
108+
ip := net.ParseIP(ipStr)
109+
if ip == nil {
110+
m.logger.Error("invalid client IP addresss", zap.String("ip", ipStr))
111+
return false
112+
}
113+
return (len(m.cidrs) == 0 || m.matches(ip, m.cidrs)) &&
114+
(len(m.notCidrs) == 0 || !m.matches(ip, m.notCidrs))
115+
}
116+
117+
func (MatchRemoteIP) parseIPRange(str string) ([]*net.IPNet, error) {
118+
var cidrs []*net.IPNet
119+
if strings.Contains(str, "/") {
120+
_, ipNet, err := net.ParseCIDR(str)
121+
if err != nil {
122+
return nil, fmt.Errorf("parsing CIDR expression: %v", err)
123+
}
124+
cidrs = append(cidrs, ipNet)
125+
} else {
126+
ip := net.ParseIP(str)
127+
if ip == nil {
128+
return nil, fmt.Errorf("invalid IP address: %s", str)
129+
}
130+
mask := len(ip) * 8
131+
cidrs = append(cidrs, &net.IPNet{
132+
IP: ip,
133+
Mask: net.CIDRMask(mask, mask),
134+
})
135+
}
136+
return cidrs, nil
137+
}
138+
139+
func (MatchRemoteIP) matches(ip net.IP, ranges []*net.IPNet) bool {
140+
for _, ipRange := range ranges {
141+
if ipRange.Contains(ip) {
142+
return true
143+
}
144+
}
145+
return false
146+
}
147+
148+
// Interface guards
149+
var (
150+
_ ConnectionMatcher = (*MatchServerName)(nil)
151+
_ ConnectionMatcher = (*MatchRemoteIP)(nil)
152+
)

modules/caddytls/matchers_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
package caddytls
1616

1717
import (
18+
"context"
1819
"crypto/tls"
20+
"net"
1921
"testing"
22+
23+
"github.com/caddyserver/caddy/v2"
2024
)
2125

2226
func TestServerNameMatcher(t *testing.T) {
@@ -84,3 +88,91 @@ func TestServerNameMatcher(t *testing.T) {
8488
}
8589
}
8690
}
91+
92+
func TestRemoteIPMatcher(t *testing.T) {
93+
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
94+
defer cancel()
95+
96+
for i, tc := range []struct {
97+
ranges []string
98+
notRanges []string
99+
input string
100+
expect bool
101+
}{
102+
{
103+
ranges: []string{"127.0.0.1"},
104+
input: "127.0.0.1:12345",
105+
expect: true,
106+
},
107+
{
108+
ranges: []string{"127.0.0.1"},
109+
input: "127.0.0.2:12345",
110+
expect: false,
111+
},
112+
{
113+
ranges: []string{"127.0.0.1/16"},
114+
input: "127.0.1.23:12345",
115+
expect: true,
116+
},
117+
{
118+
ranges: []string{"127.0.0.1", "192.168.1.105"},
119+
input: "192.168.1.105:12345",
120+
expect: true,
121+
},
122+
{
123+
notRanges: []string{"127.0.0.1"},
124+
input: "127.0.0.1:12345",
125+
expect: false,
126+
},
127+
{
128+
notRanges: []string{"127.0.0.2"},
129+
input: "127.0.0.1:12345",
130+
expect: true,
131+
},
132+
{
133+
ranges: []string{"127.0.0.1"},
134+
notRanges: []string{"127.0.0.2"},
135+
input: "127.0.0.1:12345",
136+
expect: true,
137+
},
138+
{
139+
ranges: []string{"127.0.0.2"},
140+
notRanges: []string{"127.0.0.2"},
141+
input: "127.0.0.2:12345",
142+
expect: false,
143+
},
144+
{
145+
ranges: []string{"127.0.0.2"},
146+
notRanges: []string{"127.0.0.2"},
147+
input: "127.0.0.3:12345",
148+
expect: false,
149+
},
150+
} {
151+
matcher := MatchRemoteIP{Ranges: tc.ranges, NotRanges: tc.notRanges}
152+
err := matcher.Provision(ctx)
153+
if err != nil {
154+
t.Fatalf("Test %d: Provision failed: %v", i, err)
155+
}
156+
157+
addr := testAddr(tc.input)
158+
chi := &tls.ClientHelloInfo{Conn: testConn{addr: addr}}
159+
160+
actual := matcher.Match(chi)
161+
if actual != tc.expect {
162+
t.Errorf("Test %d: Expected %t but got %t (input=%s ranges=%v notRanges=%v)",
163+
i, tc.expect, actual, tc.input, tc.ranges, tc.notRanges)
164+
}
165+
}
166+
}
167+
168+
type testConn struct {
169+
*net.TCPConn
170+
addr testAddr
171+
}
172+
173+
func (tc testConn) RemoteAddr() net.Addr { return tc.addr }
174+
175+
type testAddr string
176+
177+
func (testAddr) Network() string { return "tcp" }
178+
func (ta testAddr) String() string { return string(ta) }

0 commit comments

Comments
 (0)