github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4tls/matcher.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 l4tls 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "io" 21 22 "github.com/caddyserver/caddy/v2" 23 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 24 "github.com/caddyserver/caddy/v2/modules/caddyhttp" 25 "github.com/caddyserver/caddy/v2/modules/caddytls" 26 "go.uber.org/zap" 27 28 "github.com/mholt/caddy-l4/layer4" 29 ) 30 31 func init() { 32 caddy.RegisterModule(&MatchTLS{}) 33 } 34 35 // MatchTLS is able to match TLS connections. Its structure 36 // is different from the auto-generated documentation. This 37 // value should be a map of matcher names to their values. 38 type MatchTLS struct { 39 MatchersRaw caddy.ModuleMap `json:"-" caddy:"namespace=tls.handshake_match"` 40 41 matchers []caddytls.ConnectionMatcher 42 logger *zap.Logger 43 } 44 45 // CaddyModule returns the Caddy module information. 46 func (*MatchTLS) CaddyModule() caddy.ModuleInfo { 47 return caddy.ModuleInfo{ 48 ID: "layer4.matchers.tls", 49 New: func() caddy.Module { return new(MatchTLS) }, 50 } 51 } 52 53 // UnmarshalJSON satisfies the json.Unmarshaler interface. 54 func (m *MatchTLS) UnmarshalJSON(b []byte) error { 55 return json.Unmarshal(b, &m.MatchersRaw) 56 } 57 58 // MarshalJSON satisfies the json.Marshaler interface. 59 func (m *MatchTLS) MarshalJSON() ([]byte, error) { 60 return json.Marshal(m.MatchersRaw) 61 } 62 63 // Provision sets up the handler. 64 func (m *MatchTLS) Provision(ctx caddy.Context) error { 65 m.logger = ctx.Logger(m) 66 mods, err := ctx.LoadModule(m, "MatchersRaw") 67 if err != nil { 68 return fmt.Errorf("loading TLS matchers: %v", err) 69 } 70 for _, modIface := range mods.(map[string]interface{}) { 71 m.matchers = append(m.matchers, modIface.(caddytls.ConnectionMatcher)) 72 } 73 return nil 74 } 75 76 // Match returns true if the connection is a TLS handshake. 77 func (m *MatchTLS) Match(cx *layer4.Connection) (bool, error) { 78 // read the header bytes 79 const recordHeaderLen = 5 80 hdr := make([]byte, recordHeaderLen) 81 _, err := io.ReadFull(cx, hdr) 82 if err != nil { 83 return false, err 84 } 85 86 const recordTypeHandshake = 0x16 87 if hdr[0] != recordTypeHandshake { 88 return false, nil 89 } 90 91 // get length of the ClientHello message and read it 92 length := int(uint16(hdr[3])<<8 | uint16(hdr[4])) // ignoring version in hdr[1:3] - like https://github.com/inetaf/tcpproxy/blob/master/sni.go#L170 93 rawHello := make([]byte, length) 94 _, err = io.ReadFull(cx, rawHello) 95 if err != nil { 96 return false, err 97 } 98 99 // parse the ClientHello 100 chi := parseRawClientHello(rawHello) 101 chi.Conn = cx 102 103 // also add values to the replacer 104 repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer) 105 repl.Set("l4.tls.server_name", chi.ClientHelloInfo.ServerName) 106 repl.Set("l4.tls.version", chi.Version) 107 108 for _, matcher := range m.matchers { 109 // TODO: even though we have more data than the standard lib's 110 // ClientHelloInfo lets us fill, the matcher modules we use do 111 // not accept our own type; but the advantage of this is that 112 // we can reuse TLS connection matchers from the tls app - but 113 // it would be nice if we found a way to give matchers all 114 // the infoz 115 if !matcher.Match(&chi.ClientHelloInfo) { 116 return false, nil 117 } 118 } 119 120 m.logger.Debug("matched", 121 zap.String("remote", cx.RemoteAddr().String()), 122 zap.String("server_name", chi.ClientHelloInfo.ServerName), 123 ) 124 125 return true, nil 126 } 127 128 // UnmarshalCaddyfile sets up the MatchTLS from Caddyfile tokens. Syntax: 129 // 130 // tls { 131 // matcher [<args...>] 132 // matcher [<args...>] 133 // } 134 // tls matcher [<args...>] 135 // tls 136 func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 137 d.Next() // consume wrapper name 138 139 matcherSet, err := ParseCaddyfileNestedMatcherSet(d) 140 if err != nil { 141 return err 142 } 143 m.MatchersRaw = matcherSet 144 145 return nil 146 } 147 148 // Interface guards 149 var ( 150 _ layer4.ConnMatcher = (*MatchTLS)(nil) 151 _ caddy.Provisioner = (*MatchTLS)(nil) 152 _ caddyfile.Unmarshaler = (*MatchTLS)(nil) 153 _ json.Marshaler = (*MatchTLS)(nil) 154 _ json.Unmarshaler = (*MatchTLS)(nil) 155 ) 156 157 // ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested 158 // matcher set, and returns its raw module map value. 159 func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) { 160 matcherMap := make(map[string]caddytls.ConnectionMatcher) 161 162 tokensByMatcherName := make(map[string][]caddyfile.Token) 163 for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { 164 matcherName := d.Val() 165 tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) 166 } 167 168 for matcherName, tokens := range tokensByMatcherName { 169 dd := caddyfile.NewDispenser(tokens) 170 dd.Next() // consume wrapper name 171 // TODO: delete this workaround when the corresponding matchers implement caddyfile.Unmarshaler interface 172 if matcherName == "local_ip" { 173 cm, err := unmarshalCaddyfileMatchLocalIP(dd.NewFromNextSegment()) 174 if err != nil { 175 return nil, err 176 } 177 matcherMap[matcherName] = cm 178 } else if matcherName == "remote_ip" { 179 cm, err := unmarshalCaddyfileMatchRemoteIP(dd.NewFromNextSegment()) 180 if err != nil { 181 return nil, err 182 } 183 matcherMap[matcherName] = cm 184 } else if matcherName == "sni" { 185 cm, err := unmarshalCaddyfileMatchServerName(dd.NewFromNextSegment()) 186 if err != nil { 187 return nil, err 188 } 189 matcherMap[matcherName] = cm 190 } else { 191 mod, err := caddy.GetModule("tls.handshake_match." + matcherName) 192 if err != nil { 193 return nil, d.Errf("getting matcher module '%s': %v", matcherName, err) 194 } 195 unm, ok := mod.New().(caddyfile.Unmarshaler) 196 if !ok { 197 return nil, d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) 198 } 199 err = unm.UnmarshalCaddyfile(dd.NewFromNextSegment()) 200 if err != nil { 201 return nil, err 202 } 203 cm, ok := unm.(caddytls.ConnectionMatcher) 204 if !ok { 205 return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName) 206 } 207 matcherMap[matcherName] = cm 208 } 209 } 210 211 matcherSet := make(caddy.ModuleMap) 212 for name, matcher := range matcherMap { 213 jsonBytes, err := json.Marshal(matcher) 214 if err != nil { 215 return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err) 216 } 217 matcherSet[name] = jsonBytes 218 } 219 220 return matcherSet, nil 221 } 222 223 // TODO: move to https://github.com/caddyserver/caddy/tree/master/modules/caddytls/matchers.go 224 // unmarshalCaddyfileMatchLocalIP sets up the MatchLocalIP from Caddyfile tokens. Syntax: 225 // 226 // local_ip <ranges...> 227 func unmarshalCaddyfileMatchLocalIP(d *caddyfile.Dispenser) (*caddytls.MatchLocalIP, error) { 228 m := caddytls.MatchLocalIP{} 229 230 for d.Next() { 231 wrapper := d.Val() 232 233 // At least one same-line option must be provided 234 if d.CountRemainingArgs() == 0 { 235 return nil, d.ArgErr() 236 } 237 238 for d.NextArg() { 239 val := d.Val() 240 if val == "private_ranges" { 241 m.Ranges = append(m.Ranges, caddyhttp.PrivateRangesCIDR()...) 242 continue 243 } 244 m.Ranges = append(m.Ranges, val) 245 } 246 247 // No blocks are supported 248 if d.NextBlock(d.Nesting()) { 249 return nil, d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper) 250 } 251 } 252 253 return &m, nil 254 } 255 256 // TODO: move to https://github.com/caddyserver/caddy/tree/master/modules/caddytls/matchers.go 257 // unmarshalCaddyfileMatchRemoteIP sets up the MatchRemoteIP from Caddyfile tokens. Syntax: 258 // 259 // remote_ip <ranges...> 260 // 261 // Note: IPs and CIDRs starting with ! symbol are treated as not_ranges 262 func unmarshalCaddyfileMatchRemoteIP(d *caddyfile.Dispenser) (*caddytls.MatchRemoteIP, error) { 263 m := caddytls.MatchRemoteIP{} 264 265 for d.Next() { 266 wrapper := d.Val() 267 268 // At least one same-line option must be provided 269 if d.CountRemainingArgs() == 0 { 270 return nil, d.ArgErr() 271 } 272 273 for d.NextArg() { 274 val := d.Val() 275 var exclamation bool 276 if len(val) > 1 && val[0] == '!' { 277 exclamation, val = true, val[1:] 278 } 279 ranges := []string{val} 280 if val == "private_ranges" { 281 ranges = caddyhttp.PrivateRangesCIDR() 282 } 283 if exclamation { 284 m.NotRanges = append(m.NotRanges, ranges...) 285 } else { 286 m.Ranges = append(m.Ranges, ranges...) 287 } 288 } 289 290 // No blocks are supported 291 if d.NextBlock(d.Nesting()) { 292 return nil, d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper) 293 } 294 } 295 296 return &m, nil 297 } 298 299 // TODO: move to https://github.com/caddyserver/caddy/tree/master/modules/caddytls/matchers.go 300 // unmarshalCaddyfileMatchServerName sets up the MatchServerName from Caddyfile tokens. Syntax: 301 // 302 // sni <domains...> 303 func unmarshalCaddyfileMatchServerName(d *caddyfile.Dispenser) (*caddytls.MatchServerName, error) { 304 m := caddytls.MatchServerName{} 305 306 for d.Next() { 307 wrapper := d.Val() 308 309 // At least one same-line option must be provided 310 if d.CountRemainingArgs() == 0 { 311 return nil, d.ArgErr() 312 } 313 314 m = append(m, d.RemainingArgs()...) 315 316 // No blocks are supported 317 if d.NextBlock(d.Nesting()) { 318 return nil, d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper) 319 } 320 } 321 322 return &m, nil 323 }