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  }