github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4socks/socks5_matcher.go (about) 1 package l4socks 2 3 import ( 4 "io" 5 "strconv" 6 7 "github.com/caddyserver/caddy/v2" 8 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 9 10 "github.com/mholt/caddy-l4/layer4" 11 ) 12 13 func init() { 14 caddy.RegisterModule(&Socks5Matcher{}) 15 } 16 17 // Socks5Matcher matches SOCKSv5 connections according to RFC 1928 (https://www.rfc-editor.org/rfc/rfc1928.html). 18 // Since the SOCKSv5 header is very short it could produce a lot of false positives, 19 // use AuthMethods to exactly specify which METHODS you expect your clients to send. 20 // By default, only the most common methods are matched NO AUTH, GSSAPI & USERNAME/PASSWORD. 21 type Socks5Matcher struct { 22 AuthMethods []uint16 `json:"auth_methods,omitempty"` 23 } 24 25 func (*Socks5Matcher) CaddyModule() caddy.ModuleInfo { 26 return caddy.ModuleInfo{ 27 ID: "layer4.matchers.socks5", 28 New: func() caddy.Module { return new(Socks5Matcher) }, 29 } 30 } 31 32 func (m *Socks5Matcher) Provision(_ caddy.Context) error { 33 if len(m.AuthMethods) == 0 { 34 m.AuthMethods = []uint16{0, 1, 2} // NO AUTH, GSSAPI, USERNAME/PASSWORD 35 } 36 return nil 37 } 38 39 // Match returns true if the connection looks like it is using the SOCKSv5 protocol. 40 func (m *Socks5Matcher) Match(cx *layer4.Connection) (bool, error) { 41 // read the version byte 42 buf := []byte{0} 43 if _, err := io.ReadFull(cx, buf); err != nil { 44 return false, err 45 } 46 if buf[0] != 5 { 47 return false, nil 48 } 49 50 // read number of auth methods 51 if _, err := io.ReadFull(cx, buf); err != nil { 52 return false, err 53 } 54 55 // read auth methods 56 methods := make([]byte, buf[0]) 57 _, err := io.ReadFull(cx, methods) 58 if err != nil { 59 return false, err 60 } 61 62 // match auth methods 63 for _, requestedMethod := range methods { 64 if !contains(m.AuthMethods, uint16(requestedMethod)) { 65 return false, nil 66 } 67 } 68 69 return true, nil 70 } 71 72 // UnmarshalCaddyfile sets up the Socks5Matcher from Caddyfile tokens. Syntax: 73 // 74 // socks5 { 75 // auth_methods <auth_methods...> 76 // } 77 // 78 // socks5 79 func (m *Socks5Matcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 80 _, wrapper := d.Next(), d.Val() // consume wrapper name 81 82 // No same-line options are supported 83 if d.CountRemainingArgs() > 0 { 84 return d.ArgErr() 85 } 86 87 for nesting := d.Nesting(); d.NextBlock(nesting); { 88 optionName := d.Val() 89 switch optionName { 90 case "auth_methods": 91 if d.CountRemainingArgs() == 0 { 92 return d.ArgErr() 93 } 94 for d.NextArg() { 95 authMethod, err := strconv.ParseUint(d.Val(), 10, 8) 96 if err != nil { 97 return d.WrapErr(err) 98 } 99 m.AuthMethods = append(m.AuthMethods, uint16(authMethod)) 100 } 101 default: 102 return d.ArgErr() 103 } 104 105 // No nested blocks are supported 106 if d.NextBlock(nesting + 1) { 107 return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName) 108 } 109 } 110 111 return nil 112 } 113 114 var ( 115 _ layer4.ConnMatcher = (*Socks5Matcher)(nil) 116 _ caddy.Provisioner = (*Socks5Matcher)(nil) 117 _ caddyfile.Unmarshaler = (*Socks5Matcher)(nil) 118 ) 119 120 func contains(values []uint16, search uint16) bool { 121 for _, value := range values { 122 if value == search { 123 return true 124 } 125 } 126 return false 127 }