Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
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.
Matt Holt committed
Apr 30, 2021 at 10:14 UTC
956f01163d150b70aa3d5fc418ba9f150bd25c24
3 files changed
+202
-2
context.go
+8
@@ -430,5 +430,13 @@ func (ctx Context) Storage() certmagic.Storage {
430
431
// Logger returns a logger that can be used by mod.
432
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
+ }
441
return ctx.cfg.Logging.Logger(mod)
442
}
modules/caddytls/matchers.go
+102
-2
@@ -16,13 +16,18 @@ package caddytls
16
17
import (
18
"crypto/tls"
19
+ "fmt"
20
+ "net"
21
+ "strings"
22
23
"github.com/caddyserver/caddy/v2"
24
"github.com/caddyserver/certmagic"
25
+ "go.uber.org/zap"
26
)
27
28
func init() {
29
caddy.RegisterModule(MatchServerName{})
30
+ caddy.RegisterModule(MatchRemoteIP{})
31
}
32
33
// MatchServerName matches based on SNI. Names in
@@ -48,5 +53,100 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
53
return false
54
}
55
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
+92
@@ -15,8 +15,12 @@
15
package caddytls
16
17
import (
18
+ "context"
19
"crypto/tls"
20
+ "net"
21
"testing"
22
+
23
+ "github.com/caddyserver/caddy/v2"
24
)
25
26
func TestServerNameMatcher(t *testing.T) {
@@ -84,3 +88,91 @@ func TestServerNameMatcher(t *testing.T) {
88
}
89
}
90
}
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) }