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  )