github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4socks/socks5_handler.go (about) 1 package l4socks 2 3 import ( 4 "fmt" 5 "net" 6 "strings" 7 8 "github.com/caddyserver/caddy/v2" 9 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 10 "github.com/things-go/go-socks5" 11 "go.uber.org/zap" 12 13 "github.com/mholt/caddy-l4/layer4" 14 ) 15 16 func init() { 17 caddy.RegisterModule(&Socks5Handler{}) 18 } 19 20 // Socks5Handler is a connection handler that terminates SOCKSv5 connection. 21 type Socks5Handler struct { 22 // Controls which socks5 methods are allowed. Possible values CONNECT, ASSOCIATE, BIND. Default: ["CONNECT", "ASSOCIATE"]. 23 Commands []string `json:"commands,omitempty"` 24 // IP address used for bind during BIND or UDP ASSOCIATE. 25 BindIP string `json:"bind_ip,omitempty"` 26 // Map of username:password to active authentication. Default: no authentication. 27 Credentials map[string]string `json:"credentials,omitempty"` 28 29 server *socks5.Server 30 } 31 32 func (*Socks5Handler) CaddyModule() caddy.ModuleInfo { 33 return caddy.ModuleInfo{ 34 ID: "layer4.handlers.socks5", 35 New: func() caddy.Module { return new(Socks5Handler) }, 36 } 37 } 38 39 func (h *Socks5Handler) Provision(ctx caddy.Context) error { 40 repl := caddy.NewReplacer() 41 42 rule := &socks5.PermitCommand{EnableConnect: false, EnableAssociate: false, EnableBind: false} 43 if len(h.Commands) == 0 { 44 rule.EnableConnect = true 45 rule.EnableAssociate = true 46 // BIND is currently not supported, so we don't allow it by default 47 } else { 48 for _, c := range h.Commands { 49 switch strings.ToUpper(repl.ReplaceAll(c, "")) { 50 case "CONNECT": 51 rule.EnableConnect = true 52 case "ASSOCIATE": 53 rule.EnableAssociate = true 54 case "BIND": 55 rule.EnableBind = true 56 default: 57 return fmt.Errorf("unknown command \"%s\" has to be one of [\"CONNECT\", \"ASSOCIATE\", \"BIND\"]", c) 58 } 59 } 60 } 61 62 credentials := make(map[string]string, len(h.Credentials)) 63 for k, v := range h.Credentials { 64 k, v = repl.ReplaceAll(k, ""), repl.ReplaceAll(v, "") 65 if len(k) > 0 { 66 credentials[k] = v 67 } 68 } 69 70 authMethods := []socks5.Authenticator{socks5.NoAuthAuthenticator{}} 71 if len(h.Credentials) > 0 { 72 authMethods = []socks5.Authenticator{ 73 socks5.UserPassAuthenticator{ 74 Credentials: socks5.StaticCredentials(credentials), 75 }, 76 } 77 } 78 79 h.server = socks5.NewServer( 80 socks5.WithLogger(&socks5Logger{l: ctx.Logger(h)}), 81 socks5.WithRule(rule), 82 socks5.WithBindIP(net.ParseIP(caddy.NewReplacer().ReplaceAll(h.BindIP, ""))), 83 socks5.WithAuthMethods(authMethods), 84 ) 85 86 return nil 87 } 88 89 // Handle handles the SOCKSv5 connection. 90 func (h *Socks5Handler) Handle(cx *layer4.Connection, _ layer4.Handler) error { 91 return h.server.ServeConn(cx) 92 } 93 94 // UnmarshalCaddyfile sets up the Socks5Handler from Caddyfile tokens. Syntax: 95 // 96 // socks5 { 97 // bind_ip <address> 98 // commands <values...> 99 // credentials <username> <password> [<username> <password>] 100 // } 101 // 102 // Note: multiple commands and credentials options are supported, but bind_ip option can only be provided once. 103 // Only plain text passwords are currently supported. 104 func (h *Socks5Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 105 _, wrapper := d.Next(), d.Val() // consume wrapper name 106 107 // No same-line options are supported 108 if d.CountRemainingArgs() > 0 { 109 return d.ArgErr() 110 } 111 112 var hasBindIP bool 113 for nesting := d.Nesting(); d.NextBlock(nesting); { 114 optionName := d.Val() 115 switch optionName { 116 case "bind_ip": 117 if hasBindIP { 118 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 119 } 120 if d.CountRemainingArgs() != 1 { 121 return d.ArgErr() 122 } 123 _, h.BindIP, hasBindIP = d.NextArg(), d.Val(), true 124 case "commands": 125 if d.CountRemainingArgs() == 0 { 126 return d.ArgErr() 127 } 128 h.Commands = append(h.Commands, d.RemainingArgs()...) 129 case "credentials": 130 if d.CountRemainingArgs() == 0 || d.CountRemainingArgs()%2 != 0 { 131 return d.ArgErr() 132 } 133 if h.Credentials == nil { 134 h.Credentials = make(map[string]string) 135 } 136 for d.NextArg() { 137 username := d.Val() 138 if d.NextArg() { 139 h.Credentials[username] = d.Val() 140 } 141 } 142 default: 143 return d.ArgErr() 144 } 145 146 // No nested blocks are supported 147 if d.NextBlock(nesting + 1) { 148 return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName) 149 } 150 } 151 152 return nil 153 } 154 155 var ( 156 _ caddy.Provisioner = (*Socks5Handler)(nil) 157 _ caddyfile.Unmarshaler = (*Socks5Handler)(nil) 158 _ layer4.NextHandler = (*Socks5Handler)(nil) 159 ) 160 161 type socks5Logger struct { 162 l *zap.Logger 163 } 164 165 func (s *socks5Logger) Errorf(format string, arg ...interface{}) { 166 s.l.Error(fmt.Sprintf(format, arg...)) 167 } 168 169 var _ socks5.Logger = (*socks5Logger)(nil)