github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4dns/matcher.go (about) 1 // Copyright 2024 VNXME 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 l4dns 16 17 import ( 18 "context" 19 "encoding/binary" 20 "io" 21 "net" 22 "regexp" 23 "strings" 24 25 "github.com/caddyserver/caddy/v2" 26 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 27 "github.com/miekg/dns" 28 29 "github.com/mholt/caddy-l4/layer4" 30 ) 31 32 func init() { 33 caddy.RegisterModule(&MatchDNS{}) 34 } 35 36 // MatchDNS is able to match connections that look like DNS protocol. 37 // Note: DNS messages sent via TCP are 2 bytes longer then those sent via UDP. Consequently, if Caddy listens on TCP, 38 // it has to proxy DNS messages to TCP upstreams only. The same is true for UDP. No TCP/UDP mixing is allowed. 39 // However, it's technically possible: an intermediary handler is required to add/strip 2 bytes before/after proxy. 40 // Please open a feature request and describe your use case if you need TCP/UDP mixing. 41 type MatchDNS struct { 42 // Allow contains an optional list of rules to match the question section of the DNS request message against. 43 // The matcher returns false if not matched by any of them (in the absence of any deny rules). 44 Allow MatchDNSRules `json:"allow,omitempty"` 45 // Deny contains an optional list of rules to match the question section of the DNS request message against. 46 // The matcher returns false if matched by any of them (in the absence of any allow rules). 47 Deny MatchDNSRules `json:"deny,omitempty"` 48 49 // If DefaultDeny is true, DNS request messages that haven't been matched by any allow and deny rules are denied. 50 // The default action is allow. Use it to make the filter more restrictive when the rules aren't exhaustive. 51 DefaultDeny bool `json:"default_deny,omitempty"` 52 // If PreferAllow is true, DNS request messages that have been matched by both allow and deny rules are allowed. 53 // The default action is deny. Use it to make the filter less restrictive when the rules are mutually exclusive. 54 PreferAllow bool `json:"prefer_allow,omitempty"` 55 } 56 57 // CaddyModule returns the Caddy module information. 58 func (m *MatchDNS) CaddyModule() caddy.ModuleInfo { 59 return caddy.ModuleInfo{ 60 ID: "layer4.matchers.dns", 61 New: func() caddy.Module { return new(MatchDNS) }, 62 } 63 } 64 65 // Match returns true if the connection bytes represent a valid DNS request message. 66 func (m *MatchDNS) Match(cx *layer4.Connection) (bool, error) { 67 var ( 68 msgBuf []byte 69 msgBytes uint16 70 ) 71 72 // Detect the connection protocol: TCP or UDP. 73 // Note: all non-TCP connections are treated as UDP, so no TCP packets could be matched while testing 74 // with net.Pipe() unless a valid cx.LocalAddr() response is provided using a fakeTCPConn wrapper. 75 if _, ok := cx.LocalAddr().(*net.TCPAddr); ok { 76 // Read the first 2 bytes, validate them and adjust the DNS message length 77 // Note: these 2 bytes represent the length of the remaining part of the packet 78 // as a big endian uint16 number. 79 err := binary.Read(cx, binary.BigEndian, &msgBytes) 80 if err != nil || msgBytes < dnsHeaderBytes || msgBytes > dns.MaxMsgSize { 81 return false, err 82 } 83 84 // Read the remaining bytes 85 msgBuf = make([]byte, msgBytes) 86 _, err = io.ReadFull(cx, msgBuf) 87 if err != nil { 88 return false, err 89 } 90 91 // Validate the remaining connection buffer 92 // Note: if at least 1 byte remains, we can technically be sure, the protocol isn't DNS. 93 // This behaviour may be changed in the future if there are many false negative matches. 94 extraBuf := make([]byte, 1) 95 _, err = io.ReadFull(cx, extraBuf) 96 if err == nil { 97 return false, nil 98 } 99 } else { 100 // Read a minimum number of bytes 101 msgBuf = make([]byte, dnsHeaderBytes) 102 n, err := io.ReadAtLeast(cx, msgBuf, int(dnsHeaderBytes)) 103 if err != nil { 104 return false, err 105 } 106 107 // Read the remaining bytes and validate their length 108 var nn int 109 tmpBuf := make([]byte, dns.MinMsgSize) 110 for err == nil { 111 nn, err = io.ReadAtLeast(cx, tmpBuf, 1) 112 msgBuf = append(msgBuf, tmpBuf[:nn]...) 113 n += nn 114 } 115 if n > dns.MaxMsgSize { 116 return false, nil 117 } 118 msgBytes = uint16(n) 119 } 120 121 // Unpack the DNS message with a third-party library 122 // Note: it doesn't return an error if there are any bytes remaining in the buffer after parsing has completed. 123 msg := new(dns.Msg) 124 if err := msg.Unpack(msgBuf); err != nil { 125 return false, nil 126 } 127 128 // Ensure there are no extra bytes in the packet 129 if msg.Len() != int(msgBytes) { 130 return false, nil 131 } 132 133 // Filter out invalid DNS request messages 134 if len(msg.Question) == 0 || msg.Response || msg.Rcode != dns.RcodeSuccess || msg.Zero { 135 return false, nil 136 } 137 138 // Apply the allow and deny rules to the question section of the DNS request message 139 hasNoAllow, hasNoDeny := len(m.Allow) == 0, len(m.Deny) == 0 140 if !(hasNoAllow && hasNoDeny) { 141 for _, q := range msg.Question { 142 // Filter out DNS request messages with invalid question classes 143 classValue, classFound := dns.ClassToString[q.Qclass] 144 if !classFound { 145 return false, nil 146 } 147 148 // Filter out DNS request messages with invalid question types 149 typeValue, typeFound := dns.TypeToString[q.Qtype] 150 if !typeFound { 151 return false, nil 152 } 153 154 denied := m.Deny.Match(cx.Context, classValue, typeValue, q.Name) 155 // If only deny rules are provided, filter out DNS request messages with denied question sections. 156 // In other words, allow all unless explicitly denied. 157 if hasNoAllow && !hasNoDeny && denied { 158 return false, nil 159 } 160 161 allowed := m.Allow.Match(cx.Context, classValue, typeValue, q.Name) 162 // If only allow rules are provided, filter out DNS request messages with not allowed question sections. 163 // In other words, deny all unless explicitly allowed. 164 if hasNoDeny && !hasNoAllow && !allowed { 165 return false, nil 166 } 167 168 // If both rules are provided and the question section is both allowed and denied, deny rules prevail 169 // unless the PreferAllow is set to true. If both rules are provided and the question section is 170 // neither allowed nor denied, it is allowed unless the DefaultDeny flag is set to true. 171 if denied { 172 if !allowed || !m.PreferAllow { 173 return false, nil 174 } 175 } else { 176 if !allowed && m.DefaultDeny { 177 return false, nil 178 } 179 } 180 } 181 } 182 183 // Append the current DNS message to the messages list (it might be useful for other matchers or handlers) 184 appendMessage(cx, msg) 185 186 return true, nil 187 } 188 189 // Provision prepares m's allow and deny rules. 190 func (m *MatchDNS) Provision(cx caddy.Context) error { 191 err := m.Allow.Provision(cx) 192 if err != nil { 193 return err 194 } 195 err = m.Deny.Provision(cx) 196 if err != nil { 197 return err 198 } 199 return nil 200 } 201 202 // UnmarshalCaddyfile sets up the MatchDNS from Caddyfile tokens. Syntax: 203 // 204 // dns { 205 // <allow|deny> <*|name> [<*|type> [<*|class>]] 206 // <allow_regexp|deny_regexp> <*|name_pattern> [<*|type_pattern> [<*|class_pattern>]] 207 // default_deny 208 // prefer_allow 209 // } 210 // dns 211 // 212 // Note: multiple allow and deny options are allowed. If default_deny is set, DNS request messages that haven't been 213 // matched by any allow and deny rules are denied (the default action is allow). If prefer_allow is set, DNS request 214 // messages that have been matched by both allow and deny rules are allowed (the default action is deny). An asterisk 215 // should be used to skip filtering the corresponding question section field, i.e. it will match any value provided. 216 func (m *MatchDNS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 217 _, wrapper := d.Next(), d.Val() // consume wrapper name 218 219 // No same-line arguments are supported 220 if d.CountRemainingArgs() != 0 { 221 return d.ArgErr() 222 } 223 224 var hasDefaultDeny, hasPreferAllow bool 225 for nesting := d.Nesting(); d.NextBlock(nesting); { 226 optionName := d.Val() 227 switch optionName { 228 case "allow", "allow_regexp", "deny", "deny_regexp": 229 if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 3 { 230 return d.ArgErr() 231 } 232 isRegexp := strings.HasSuffix(optionName, "regexp") 233 r := new(MatchDNSRule) 234 _, val := d.NextArg(), d.Val() 235 if val != dnsSpecialAny { 236 if isRegexp { 237 r.NameRegexp = val 238 } else { 239 r.Name = val 240 } 241 } 242 if d.NextArg() { 243 val = d.Val() 244 if val != dnsSpecialAny { 245 if isRegexp { 246 r.TypeRegexp = val 247 } else { 248 r.Type = val 249 } 250 } 251 } 252 if d.NextArg() { 253 val = d.Val() 254 if val != dnsSpecialAny { 255 if isRegexp { 256 r.ClassRegexp = val 257 } else { 258 r.Class = val 259 } 260 } 261 } 262 if strings.HasPrefix(optionName, "deny") { 263 m.Deny = append(m.Deny, r) 264 } else { 265 m.Allow = append(m.Allow, r) 266 } 267 case "default_deny": 268 if hasDefaultDeny { 269 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 270 } 271 if d.CountRemainingArgs() > 0 { 272 return d.ArgErr() 273 } 274 m.DefaultDeny, hasDefaultDeny = true, true 275 case "prefer_allow": 276 if hasPreferAllow { 277 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 278 } 279 if d.CountRemainingArgs() > 0 { 280 return d.ArgErr() 281 } 282 m.PreferAllow, hasPreferAllow = true, true 283 default: 284 return d.ArgErr() 285 } 286 287 // No nested blocks are supported 288 if d.NextBlock(nesting + 1) { 289 return d.Errf("malformed %s option %s: nested blocks are not supported", wrapper, optionName) 290 } 291 } 292 293 return nil 294 } 295 296 // MatchDNSRules may contain a number of MatchDNSRule instances. An empty MatchDNSRules instance won't match anything. 297 type MatchDNSRules []*MatchDNSRule 298 299 func (rs *MatchDNSRules) Match(cx context.Context, qClass string, qType string, qName string) bool { 300 for _, r := range *rs { 301 if r.Match(cx, qClass, qType, qName) { 302 return true 303 } 304 } 305 return false 306 } 307 308 func (rs *MatchDNSRules) Provision(cx caddy.Context) error { 309 for _, r := range *rs { 310 if err := r.Provision(cx); err != nil { 311 return err 312 } 313 } 314 return nil 315 } 316 317 // MatchDNSRule represents a set of filters to match against the question section of a DNS request message. 318 // Full and regular expression matching filters are supported. If both filters are provided for a single field, 319 // the full matcher is evaluated first. An empty MatchDNSRule will match anything. 320 type MatchDNSRule struct { 321 // Class may contain a value to match the question class. Use upper case letters, e.g. "IN", "CH", "ANY". 322 // See the full list of valid class values in dns.StringToClass. 323 Class string `json:"class,omitempty"` 324 // ClassRegexp may contain a regular expression to match the question class. E.g. "^(IN|CH)$". 325 // See the full list of valid class values in dns.StringToClass. 326 ClassRegexp string `json:"class_regexp,omitempty"` 327 // Name may contain a value to match the question domain name. E.g. "example.com.". 328 // The domain name is provided in lower case ending with a dot. 329 Name string `json:"name,omitempty"` 330 // NameRegexp may contain a regular expression to match the question domain name. 331 // E.g. "^(|[-0-9a-z]+\.)example\.com\.$". The domain name is provided in lower case ending with a dot. 332 NameRegexp string `json:"name_regexp,omitempty"` 333 // Type may contain a value to match the question type. Use upper case letters, e.g. "A", "MX", "NS". 334 // See the full list of valid type values in dns.StringToType. 335 Type string `json:"type,omitempty"` 336 // TypeRegexp may contain a regular expression to match the question type. E.g. "^(MX|NS)$". 337 // See the full list of valid type values in dns.StringToType. 338 TypeRegexp string `json:"type_regexp,omitempty"` 339 340 classRegexp *regexp.Regexp 341 nameRegexp *regexp.Regexp 342 typeRegexp *regexp.Regexp 343 } 344 345 func (r *MatchDNSRule) Match(cx context.Context, qClass string, qType string, qName string) bool { 346 repl := cx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) 347 348 // Validate the question class 349 classFilter := repl.ReplaceAll(r.Class, "") 350 if (len(classFilter) > 0 && qClass != classFilter) || 351 len(r.ClassRegexp) > 0 && !r.classRegexp.MatchString(qClass) { 352 return false 353 } 354 355 // Validate the question type 356 typeFilter := repl.ReplaceAll(r.Type, "") 357 if (len(typeFilter) > 0 && qType != typeFilter) || 358 len(r.TypeRegexp) > 0 && !r.typeRegexp.MatchString(qType) { 359 return false 360 } 361 362 // Validate the question domain name 363 nameFilter := repl.ReplaceAll(r.Name, "") 364 if (len(nameFilter) > 0 && qName != nameFilter) || 365 (len(r.NameRegexp) > 0 && !r.nameRegexp.MatchString(qName)) { 366 return false 367 } 368 369 return true 370 } 371 372 func (r *MatchDNSRule) Provision(_ caddy.Context) (err error) { 373 repl := caddy.NewReplacer() 374 r.classRegexp, err = regexp.Compile(repl.ReplaceAll(r.ClassRegexp, "")) 375 if err != nil { 376 return err 377 } 378 r.typeRegexp, err = regexp.Compile(repl.ReplaceAll(r.TypeRegexp, "")) 379 if err != nil { 380 return err 381 } 382 r.nameRegexp, err = regexp.Compile(repl.ReplaceAll(r.NameRegexp, "")) 383 if err != nil { 384 return err 385 } 386 return nil 387 } 388 389 // Interface guards 390 var ( 391 _ caddy.Provisioner = (*MatchDNS)(nil) 392 _ caddyfile.Unmarshaler = (*MatchDNS)(nil) 393 _ layer4.ConnMatcher = (*MatchDNS)(nil) 394 395 _ caddy.Provisioner = (*MatchDNSRules)(nil) 396 _ caddy.Provisioner = (*MatchDNSRule)(nil) 397 ) 398 399 const ( 400 dnsHeaderBytes uint16 = 12 // read this many bytes to parse a DNS message header (equals dns.headerSize) 401 dnsMessagesKey = "dns_messages" 402 dnsSpecialAny = "*" 403 ) 404 405 func appendMessage(cx *layer4.Connection, msg *dns.Msg) { 406 var messages []*dns.Msg 407 if val := cx.GetVar(dnsMessagesKey); val != nil { 408 messages = val.([]*dns.Msg) 409 } 410 messages = append(messages, msg) 411 cx.SetVar(dnsMessagesKey, messages) 412 }