github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4socks/socks4_matcher.go (about)

     1  package l4socks
     2  
     3  import (
     4  	"encoding/binary"
     5  	"fmt"
     6  	"io"
     7  	"net/netip"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/caddyserver/caddy/v2"
    12  	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
    13  	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
    14  	"github.com/mholt/caddy-l4/layer4"
    15  )
    16  
    17  func init() {
    18  	caddy.RegisterModule(&Socks4Matcher{})
    19  }
    20  
    21  // Socks4Matcher matches SOCKSv4 connections according to https://www.openssh.com/txt/socks4.protocol.
    22  // Since the SOCKSv4 header is very short it could produce a lot of false positives.
    23  // To improve the matching use Commands, Ports and Networks to specify to which destinations you expect clients to connect to.
    24  // By default, CONNECT & BIND commands are matched with any destination ip and port.
    25  type Socks4Matcher struct {
    26  	// Only match on these commands. Default: ["CONNECT", "BIND"]
    27  	Commands []string `json:"commands,omitempty"`
    28  	// Only match on requests to one of these destination networks (IP or CIDR). Default: all networks.
    29  	Networks []string `json:"networks,omitempty"`
    30  	// Only match on requests to one of these destination ports. Default: all ports.
    31  	Ports []uint16 `json:"ports,omitempty"`
    32  
    33  	commands []uint8
    34  	cidrs    []netip.Prefix
    35  }
    36  
    37  func (*Socks4Matcher) CaddyModule() caddy.ModuleInfo {
    38  	return caddy.ModuleInfo{
    39  		ID:  "layer4.matchers.socks4",
    40  		New: func() caddy.Module { return new(Socks4Matcher) },
    41  	}
    42  }
    43  
    44  func (m *Socks4Matcher) Provision(_ caddy.Context) error {
    45  	if len(m.Commands) == 0 {
    46  		m.commands = []uint8{1, 2} // CONNECT & BIND
    47  	} else {
    48  		repl := caddy.NewReplacer()
    49  		for _, c := range m.Commands {
    50  			switch strings.ToUpper(repl.ReplaceAll(c, "")) {
    51  			case "CONNECT":
    52  				m.commands = append(m.commands, 1)
    53  			case "BIND":
    54  				m.commands = append(m.commands, 2)
    55  			default:
    56  				return fmt.Errorf("unknown command \"%s\" has to be one of [\"CONNECT\", \"BIND\"]", c)
    57  			}
    58  		}
    59  	}
    60  	repl := caddy.NewReplacer()
    61  	for _, networkAddrOrCIDR := range m.Networks {
    62  		networkAddrOrCIDR = repl.ReplaceAll(networkAddrOrCIDR, "")
    63  		prefix, err := caddyhttp.CIDRExpressionToPrefix(networkAddrOrCIDR)
    64  		if err != nil {
    65  			return err
    66  		}
    67  		m.cidrs = append(m.cidrs, prefix)
    68  	}
    69  	return nil
    70  }
    71  
    72  // Match returns true if the connection looks like it is using the SOCKSv4 protocol.
    73  func (m *Socks4Matcher) Match(cx *layer4.Connection) (bool, error) {
    74  	buf := make([]byte, 8)
    75  	if _, err := io.ReadFull(cx, buf); err != nil {
    76  		return false, err
    77  	}
    78  
    79  	// match version (VN)
    80  	if buf[0] != 4 {
    81  		return false, nil
    82  	}
    83  
    84  	// match commands (CD)
    85  	commandMatched := false
    86  	for _, c := range m.commands {
    87  		if c == buf[1] {
    88  			commandMatched = true
    89  			break
    90  		}
    91  	}
    92  	if !commandMatched {
    93  		return false, nil
    94  	}
    95  
    96  	// match destination port (DSTPORT)
    97  	if len(m.Ports) > 0 {
    98  		port := binary.BigEndian.Uint16(buf[2:4])
    99  		portMatched := false
   100  		for _, p := range m.Ports {
   101  			if p == port {
   102  				portMatched = true
   103  				break
   104  			}
   105  		}
   106  		if !portMatched {
   107  			return false, nil
   108  		}
   109  	}
   110  
   111  	// match destination ipv4 (DSTIP)
   112  	if len(m.cidrs) > 0 {
   113  		ip := netip.AddrFrom4([4]byte(buf[4:8]))
   114  		ipMatched := false
   115  		for _, ipRange := range m.cidrs {
   116  			if ipRange.Contains(ip) {
   117  				ipMatched = true
   118  				break
   119  			}
   120  		}
   121  		if !ipMatched {
   122  			return false, nil
   123  		}
   124  	}
   125  
   126  	return true, nil
   127  }
   128  
   129  // UnmarshalCaddyfile sets up the Socks4Matcher from Caddyfile tokens. Syntax:
   130  //
   131  //	socks4 {
   132  //		commands <commands...>
   133  //		networks <ranges...>
   134  //		ports <ports...>
   135  //	}
   136  //
   137  // socks4
   138  func (m *Socks4Matcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
   139  	_, wrapper := d.Next(), d.Val() // consume wrapper name
   140  
   141  	// No same-line options are supported
   142  	if d.CountRemainingArgs() > 0 {
   143  		return d.ArgErr()
   144  	}
   145  
   146  	for nesting := d.Nesting(); d.NextBlock(nesting); {
   147  		optionName := d.Val()
   148  		switch optionName {
   149  		case "commands":
   150  			if d.CountRemainingArgs() == 0 {
   151  				return d.ArgErr()
   152  			}
   153  			m.Commands = append(m.Commands, d.RemainingArgs()...)
   154  		case "networks":
   155  			if d.CountRemainingArgs() == 0 {
   156  				return d.ArgErr()
   157  			}
   158  			for d.NextArg() {
   159  				val := d.Val()
   160  				if val == "private_ranges" {
   161  					m.Networks = append(m.Networks, caddyhttp.PrivateRangesCIDR()...)
   162  					continue
   163  				}
   164  				m.Networks = append(m.Networks, val)
   165  			}
   166  		case "ports":
   167  			if d.CountRemainingArgs() == 0 {
   168  				return d.ArgErr()
   169  			}
   170  			for d.NextArg() {
   171  				port, err := strconv.ParseUint(d.Val(), 10, 16)
   172  				if err != nil {
   173  					return d.WrapErr(err)
   174  				}
   175  				m.Ports = append(m.Ports, uint16(port))
   176  			}
   177  		default:
   178  			return d.ArgErr()
   179  		}
   180  
   181  		// No nested blocks are supported
   182  		if d.NextBlock(nesting + 1) {
   183  			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
   184  		}
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  var (
   191  	_ layer4.ConnMatcher    = (*Socks4Matcher)(nil)
   192  	_ caddy.Provisioner     = (*Socks4Matcher)(nil)
   193  	_ caddyfile.Unmarshaler = (*Socks4Matcher)(nil)
   194  )