github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/layer4/matchers.go (about) 1 // Copyright 2020 Matthew Holt 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package layer4 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "net" 21 "net/netip" 22 23 "github.com/caddyserver/caddy/v2" 24 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 25 "github.com/caddyserver/caddy/v2/modules/caddyhttp" 26 "go.uber.org/zap" 27 ) 28 29 func init() { 30 caddy.RegisterModule(&MatchRemoteIP{}) 31 caddy.RegisterModule(&MatchLocalIP{}) 32 caddy.RegisterModule(&MatchNot{}) 33 } 34 35 // ConnMatcher is a type that can match a connection. 36 type ConnMatcher interface { 37 // Match returns true if the given connection matches. 38 // It should read from the connection as little as possible: 39 // only as much as necessary to determine a match. 40 Match(*Connection) (bool, error) 41 } 42 43 // MatcherSet is a set of matchers which 44 // must all match in order for the request 45 // to be matched successfully. 46 type MatcherSet []ConnMatcher 47 48 // Match returns true if the connection matches all matchers in mset 49 // or if there are no matchers. Any error terminates matching. 50 func (mset MatcherSet) Match(cx *Connection) (matched bool, err error) { 51 for _, m := range mset { 52 cx.freeze() 53 matched, err = m.Match(cx) 54 cx.unfreeze() 55 if cx.Logger.Core().Enabled(zap.DebugLevel) { 56 matcher := "unknown" 57 if cm, ok := m.(caddy.Module); ok { 58 matcher = cm.CaddyModule().String() 59 } 60 cx.Logger.Debug("matching", 61 zap.String("remote", cx.RemoteAddr().String()), 62 zap.Error(err), 63 zap.String("matcher", matcher), 64 zap.Bool("matched", matched), 65 ) 66 } 67 if !matched || err != nil { 68 return 69 } 70 } 71 matched = true 72 return 73 } 74 75 // RawMatcherSets is a group of matcher sets in their 76 // raw JSON form. 77 type RawMatcherSets []caddy.ModuleMap 78 79 // MatcherSets is a group of matcher sets capable of checking 80 // whether a connection matches any of the sets. 81 type MatcherSets []MatcherSet 82 83 // AnyMatch returns true if the connection matches any of the matcher sets 84 // in mss or if there are no matchers, in which case the request always 85 // matches. Any error terminates matching. 86 func (mss *MatcherSets) AnyMatch(cx *Connection) (matched bool, err error) { 87 for _, m := range *mss { 88 matched, err = m.Match(cx) 89 if matched || err != nil { 90 return 91 } 92 } 93 matched = len(*mss) == 0 94 return 95 } 96 97 // FromInterface fills ms from an interface{} value obtained from LoadModule. 98 func (mss *MatcherSets) FromInterface(matcherSets interface{}) error { 99 for _, matcherSetIfaces := range matcherSets.([]map[string]interface{}) { 100 var matcherSet MatcherSet 101 for _, matcher := range matcherSetIfaces { 102 connMatcher, ok := matcher.(ConnMatcher) 103 if !ok { 104 return fmt.Errorf("decoded module is not a ConnMatcher: %#v", matcher) 105 } 106 matcherSet = append(matcherSet, connMatcher) 107 } 108 *mss = append(*mss, matcherSet) 109 } 110 return nil 111 } 112 113 // MatchRemoteIP matches requests by remote IP (or CIDR range). 114 type MatchRemoteIP struct { 115 Ranges []string `json:"ranges,omitempty"` 116 cidrs []netip.Prefix 117 } 118 119 // CaddyModule returns the Caddy module information. 120 func (*MatchRemoteIP) CaddyModule() caddy.ModuleInfo { 121 return caddy.ModuleInfo{ 122 ID: "layer4.matchers.remote_ip", 123 New: func() caddy.Module { return new(MatchRemoteIP) }, 124 } 125 } 126 127 // Provision parses m's IP ranges, either from IP or CIDR expressions. 128 func (m *MatchRemoteIP) Provision(_ caddy.Context) error { 129 repl := caddy.NewReplacer() 130 for _, addrOrCIDR := range m.Ranges { 131 addrOrCIDR = repl.ReplaceAll(addrOrCIDR, "") 132 prefix, err := caddyhttp.CIDRExpressionToPrefix(addrOrCIDR) 133 if err != nil { 134 return err 135 } 136 m.cidrs = append(m.cidrs, prefix) 137 } 138 return nil 139 } 140 141 // Match returns true if the connection is from one of the designated IP ranges. 142 func (m *MatchRemoteIP) Match(cx *Connection) (bool, error) { 143 clientIP, err := m.getRemoteIP(cx) 144 if err != nil { 145 return false, fmt.Errorf("getting remote IP: %v", err) 146 } 147 for _, ipRange := range m.cidrs { 148 if ipRange.Contains(clientIP) { 149 return true, nil 150 } 151 } 152 return false, nil 153 } 154 155 func (m *MatchRemoteIP) getRemoteIP(cx *Connection) (netip.Addr, error) { 156 remote := cx.Conn.RemoteAddr().String() 157 158 ipStr, _, err := net.SplitHostPort(remote) 159 if err != nil { 160 ipStr = remote // OK; probably didn't have a port 161 } 162 163 ip, err := netip.ParseAddr(ipStr) 164 if err != nil { 165 return netip.Addr{}, fmt.Errorf("invalid remote IP address: %s", ipStr) 166 } 167 return ip, nil 168 } 169 170 // UnmarshalCaddyfile sets up the MatchRemoteIP from Caddyfile tokens. Syntax: 171 // 172 // remote_ip <ranges...> 173 func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 174 _, wrapper := d.Next(), d.Val() // consume wrapper name 175 176 // At least one same-line option must be provided 177 if d.CountRemainingArgs() == 0 { 178 return d.ArgErr() 179 } 180 181 for d.NextArg() { 182 val := d.Val() 183 if val == "private_ranges" { 184 m.Ranges = append(m.Ranges, caddyhttp.PrivateRangesCIDR()...) 185 continue 186 } 187 m.Ranges = append(m.Ranges, val) 188 } 189 190 // No blocks are supported 191 if d.NextBlock(d.Nesting()) { 192 return d.Errf("malformed layer4 connection matcher '%s': blocks are not supported", wrapper) 193 } 194 195 return nil 196 } 197 198 // MatchLocalIP matches requests by local IP (or CIDR range). 199 type MatchLocalIP struct { 200 Ranges []string `json:"ranges,omitempty"` 201 202 cidrs []netip.Prefix 203 } 204 205 // CaddyModule returns the Caddy module information. 206 func (*MatchLocalIP) CaddyModule() caddy.ModuleInfo { 207 return caddy.ModuleInfo{ 208 ID: "layer4.matchers.local_ip", 209 New: func() caddy.Module { return new(MatchLocalIP) }, 210 } 211 } 212 213 // Provision parses m's IP ranges, either from IP or CIDR expressions. 214 func (m *MatchLocalIP) Provision(_ caddy.Context) error { 215 repl := caddy.NewReplacer() 216 for _, addrOrCIDR := range m.Ranges { 217 addrOrCIDR = repl.ReplaceAll(addrOrCIDR, "") 218 prefix, err := caddyhttp.CIDRExpressionToPrefix(addrOrCIDR) 219 if err != nil { 220 return err 221 } 222 m.cidrs = append(m.cidrs, prefix) 223 } 224 return nil 225 } 226 227 // Match returns true if the connection is from one of the designated IP ranges. 228 func (m *MatchLocalIP) Match(cx *Connection) (bool, error) { 229 localIP, err := m.getLocalIP(cx) 230 if err != nil { 231 return false, fmt.Errorf("getting local IP: %v", err) 232 } 233 for _, ipRange := range m.cidrs { 234 if ipRange.Contains(localIP) { 235 return true, nil 236 } 237 } 238 return false, nil 239 } 240 241 func (m *MatchLocalIP) getLocalIP(cx *Connection) (netip.Addr, error) { 242 remote := cx.Conn.LocalAddr().String() 243 244 ipStr, _, err := net.SplitHostPort(remote) 245 if err != nil { 246 ipStr = remote // OK; probably didn't have a port 247 } 248 249 ip, err := netip.ParseAddr(ipStr) 250 if err != nil { 251 return netip.Addr{}, fmt.Errorf("invalid local IP address: %s", ipStr) 252 } 253 return ip, nil 254 } 255 256 // UnmarshalCaddyfile sets up the MatchLocalIP from Caddyfile tokens. Syntax: 257 // 258 // local_ip <ranges...> 259 func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 260 _, wrapper := d.Next(), d.Val() // consume wrapper name 261 262 // At least one same-line option must be provided 263 if d.CountRemainingArgs() == 0 { 264 return d.ArgErr() 265 } 266 267 for d.NextArg() { 268 val := d.Val() 269 if val == "private_ranges" { 270 m.Ranges = append(m.Ranges, caddyhttp.PrivateRangesCIDR()...) 271 continue 272 } 273 m.Ranges = append(m.Ranges, val) 274 } 275 276 // No blocks are supported 277 if d.NextBlock(d.Nesting()) { 278 return d.Errf("malformed layer4 connection matcher '%s': blocks are not supported", wrapper) 279 } 280 281 return nil 282 } 283 284 // MatchNot matches requests by negating the results of its matcher 285 // sets. A single "not" matcher takes one or more matcher sets. Each 286 // matcher set is OR'ed; in other words, if any matcher set returns 287 // true, the final result of the "not" matcher is false. Individual 288 // matchers within a set work the same (i.e. different matchers in 289 // the same set are AND'ed). 290 // 291 // NOTE: The generated docs which describe the structure of this 292 // module are wrong because of how this type unmarshals JSON in a 293 // custom way. The correct structure is: 294 // 295 // ```json 296 // [ 297 // 298 // {}, 299 // {} 300 // 301 // ] 302 // ``` 303 // 304 // where each of the array elements is a matcher set, i.e. an 305 // object keyed by matcher name. 306 type MatchNot struct { 307 MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=layer4.matchers"` 308 MatcherSets []MatcherSet `json:"-"` 309 } 310 311 // CaddyModule implements caddy.Module. 312 func (*MatchNot) CaddyModule() caddy.ModuleInfo { 313 return caddy.ModuleInfo{ 314 ID: "layer4.matchers.not", 315 New: func() caddy.Module { return new(MatchNot) }, 316 } 317 } 318 319 // UnmarshalJSON satisfies json.Unmarshaler. It puts the JSON 320 // bytes directly into m's MatcherSetsRaw field. 321 func (m *MatchNot) UnmarshalJSON(data []byte) error { 322 return json.Unmarshal(data, &m.MatcherSetsRaw) 323 } 324 325 // MarshalJSON satisfies json.Marshaler by marshaling 326 // m's raw matcher sets. 327 func (m *MatchNot) MarshalJSON() ([]byte, error) { 328 return json.Marshal(m.MatcherSetsRaw) 329 } 330 331 // Provision loads the matcher modules to be negated. 332 func (m *MatchNot) Provision(ctx caddy.Context) error { 333 matcherSets, err := ctx.LoadModule(m, "MatcherSetsRaw") 334 if err != nil { 335 return fmt.Errorf("loading matcher sets: %v", err) 336 } 337 for _, modMap := range matcherSets.([]map[string]any) { 338 var ms MatcherSet 339 for _, modIface := range modMap { 340 ms = append(ms, modIface.(ConnMatcher)) 341 } 342 m.MatcherSets = append(m.MatcherSets, ms) 343 } 344 return nil 345 } 346 347 // Match returns true if r matches m. Since this matcher negates 348 // the embedded matchers, false is returned if any of its matcher 349 // sets return true. 350 func (m *MatchNot) Match(r *Connection) (bool, error) { 351 for _, ms := range m.MatcherSets { 352 match, err := ms.Match(r) 353 if err != nil { 354 return false, err 355 } 356 if match { 357 return false, nil 358 } 359 } 360 return true, nil 361 } 362 363 // UnmarshalCaddyfile sets up the MatchNot from Caddyfile tokens. Syntax: 364 // 365 // not { 366 // <matcher> { 367 // <submatcher> [<args...>] 368 // } 369 // <matcher> 370 // } 371 // not <matcher> { 372 // <submatcher> [<args...>] 373 // } 374 // not <matcher> 375 // 376 // Note: all matchers inside a not block are parsed into a single matcher set, i.e. they are ANDed. Multiple matcher 377 // sets, that are ORed, aren't supported. Instead, use multiple named matcher sets, each containing a not matcher. 378 func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 379 d.Next() // consume wrapper name 380 381 matcherSet, err := ParseCaddyfileNestedMatcherSet(d) 382 if err != nil { 383 return err 384 } 385 m.MatcherSetsRaw = append(m.MatcherSetsRaw, matcherSet) 386 387 return nil 388 } 389 390 // Interface guards 391 var ( 392 _ caddy.Module = (*MatchRemoteIP)(nil) 393 _ ConnMatcher = (*MatchRemoteIP)(nil) 394 _ caddy.Provisioner = (*MatchRemoteIP)(nil) 395 _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) 396 _ caddy.Module = (*MatchLocalIP)(nil) 397 _ ConnMatcher = (*MatchLocalIP)(nil) 398 _ caddy.Provisioner = (*MatchLocalIP)(nil) 399 _ caddyfile.Unmarshaler = (*MatchLocalIP)(nil) 400 _ caddy.Module = (*MatchNot)(nil) 401 _ caddy.Provisioner = (*MatchNot)(nil) 402 _ ConnMatcher = (*MatchNot)(nil) 403 _ caddyfile.Unmarshaler = (*MatchNot)(nil) 404 )