@marcel91200 / caddy Mirror · read-only / commits / 26748d06
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 12, 14:30 UTC

connection policy: add `local_ip` matcher (#6074)

* connection policy: add `local_ip` Co-authored-by: Matt Holt <mholt@users.noreply.github.com> --------- Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Mohammed Al Sahaf committed Apr 15, 2024 at 21:13 UTC 26748d06b4a39f1e1d02863245573a7ecd1bebc4
2 files changed +150
modules/caddytls/matchers.go
+78
@@ -30,6 +30,7 @@ import (
30 func init() {
31 caddy.RegisterModule(MatchServerName{})
32 caddy.RegisterModule(MatchRemoteIP{})
33 + caddy.RegisterModule(MatchLocalIP{})
34 }
35
36 // MatchServerName matches based on SNI. Names in
@@ -144,8 +145,85 @@ func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
145 return false
146 }
147
148 +// MatchLocalIP matches based on the IP address of the interface
149 +// receiving the connection. Specific IPs or CIDR ranges can be specified.
150 +type MatchLocalIP struct {
151 + // The IPs or CIDR ranges to match.
152 + Ranges []string `json:"ranges,omitempty"`
153 +
154 + cidrs []netip.Prefix
155 + logger *zap.Logger
156 +}
157 +
158 +// CaddyModule returns the Caddy module information.
159 +func (MatchLocalIP) CaddyModule() caddy.ModuleInfo {
160 + return caddy.ModuleInfo{
161 + ID: "tls.handshake_match.local_ip",
162 + New: func() caddy.Module { return new(MatchLocalIP) },
163 + }
164 +}
165 +
166 +// Provision parses m's IP ranges, either from IP or CIDR expressions.
167 +func (m *MatchLocalIP) Provision(ctx caddy.Context) error {
168 + m.logger = ctx.Logger()
169 + for _, str := range m.Ranges {
170 + cidrs, err := m.parseIPRange(str)
171 + if err != nil {
172 + return err
173 + }
174 + m.cidrs = append(m.cidrs, cidrs...)
175 + }
176 + return nil
177 +}
178 +
179 +// Match matches hello based on the connection's remote IP.
180 +func (m MatchLocalIP) Match(hello *tls.ClientHelloInfo) bool {
181 + localAddr := hello.Conn.LocalAddr().String()
182 + ipStr, _, err := net.SplitHostPort(localAddr)
183 + if err != nil {
184 + ipStr = localAddr // weird; maybe no port?
185 + }
186 + ipAddr, err := netip.ParseAddr(ipStr)
187 + if err != nil {
188 + m.logger.Error("invalid local IP addresss", zap.String("ip", ipStr))
189 + return false
190 + }
191 + return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs))
192 +}
193 +
194 +func (MatchLocalIP) parseIPRange(str string) ([]netip.Prefix, error) {
195 + var cidrs []netip.Prefix
196 + if strings.Contains(str, "/") {
197 + ipNet, err := netip.ParsePrefix(str)
198 + if err != nil {
199 + return nil, fmt.Errorf("parsing CIDR expression: %v", err)
200 + }
201 + cidrs = append(cidrs, ipNet)
202 + } else {
203 + ipAddr, err := netip.ParseAddr(str)
204 + if err != nil {
205 + return nil, fmt.Errorf("invalid IP address: '%s': %v", str, err)
206 + }
207 + ip := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
208 + cidrs = append(cidrs, ip)
209 + }
210 + return cidrs, nil
211 +}
212 +
213 +func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
214 + for _, ipRange := range ranges {
215 + if ipRange.Contains(ip) {
216 + return true
217 + }
218 + }
219 + return false
220 +}
221 +
222 // Interface guards
223 var (
224 _ ConnectionMatcher = (*MatchServerName)(nil)
225 _ ConnectionMatcher = (*MatchRemoteIP)(nil)
226 +
227 + _ caddy.Provisioner = (*MatchLocalIP)(nil)
228 + _ ConnectionMatcher = (*MatchLocalIP)(nil)
229 )
modules/caddytls/matchers_test.go
+72
@@ -165,12 +165,84 @@ func TestRemoteIPMatcher(t *testing.T) {
165 }
166 }
167
168 +func TestLocalIPMatcher(t *testing.T) {
169 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
170 + defer cancel()
171 +
172 + for i, tc := range []struct {
173 + ranges []string
174 + input string
175 + expect bool
176 + }{
177 + {
178 + ranges: []string{"127.0.0.1"},
179 + input: "127.0.0.1:12345",
180 + expect: true,
181 + },
182 + {
183 + ranges: []string{"127.0.0.1"},
184 + input: "127.0.0.2:12345",
185 + expect: false,
186 + },
187 + {
188 + ranges: []string{"127.0.0.1/16"},
189 + input: "127.0.1.23:12345",
190 + expect: true,
191 + },
192 + {
193 + ranges: []string{"127.0.0.1", "192.168.1.105"},
194 + input: "192.168.1.105:12345",
195 + expect: true,
196 + },
197 + {
198 + input: "127.0.0.1:12345",
199 + expect: true,
200 + },
201 + {
202 + ranges: []string{"127.0.0.1"},
203 + input: "127.0.0.1:12345",
204 + expect: true,
205 + },
206 + {
207 + ranges: []string{"127.0.0.2"},
208 + input: "127.0.0.3:12345",
209 + expect: false,
210 + },
211 + {
212 + ranges: []string{"127.0.0.2"},
213 + input: "127.0.0.2",
214 + expect: true,
215 + },
216 + {
217 + ranges: []string{"127.0.0.2"},
218 + input: "127.0.0.300",
219 + expect: false,
220 + },
221 + } {
222 + matcher := MatchLocalIP{Ranges: tc.ranges}
223 + err := matcher.Provision(ctx)
224 + if err != nil {
225 + t.Fatalf("Test %d: Provision failed: %v", i, err)
226 + }
227 +
228 + addr := testAddr(tc.input)
229 + chi := &tls.ClientHelloInfo{Conn: testConn{addr: addr}}
230 +
231 + actual := matcher.Match(chi)
232 + if actual != tc.expect {
233 + t.Errorf("Test %d: Expected %t but got %t (input=%s ranges=%v)",
234 + i, tc.expect, actual, tc.input, tc.ranges)
235 + }
236 + }
237 +}
238 +
239 type testConn struct {
240 *net.TCPConn
241 addr testAddr
242 }
243
244 func (tc testConn) RemoteAddr() net.Addr { return tc.addr }
245 +func (tc testConn) LocalAddr() net.Addr { return tc.addr }
246
247 type testAddr string
248