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 )