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)