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  }