@marcel91200 / caddy Mirror · read-only / commits / 956f0116
Fork 0

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) }