Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 115 additions & 12 deletions internal/socks/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,109 @@ func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net
}()
}

var addr net.Addr
if d.version() == Version4 {
addr, ctxErr = d.connect4(ctx, c, host, port)
} else {
addr, ctxErr = d.connect5(ctx, c, host, port)
}
return addr, ctxErr
}

// maxSocks4aDomainLen is the maximum domain name length accepted for SOCKS4a.
// Matches the practical FQDN limit used by SOCKS5 in this package.
const maxSocks4aDomainLen = 255

// connect4 implements the SOCKS4 and SOCKS4a CONNECT handshake.
func (d *Dialer) connect4(ctx context.Context, c net.Conn, host string, port int) (net.Addr, error) {
if err := validateSocks4CString(d.UserID, "user ID"); err != nil {
return nil, err
}

var (
ip net.IP
domain string
)
if parsed := net.ParseIP(host); parsed != nil {
ip4 := parsed.To4()
if ip4 == nil {
return nil, errors.New("SOCKS4 does not support IPv6 addresses")
}
ip = ip4
} else if d.Socks4A {
// SOCKS4a: use invalid IP 0.0.0.x (x != 0) and append domain after userid.
if host == "" {
return nil, errors.New("SOCKS4a domain name is empty")
}
if len(host) > maxSocks4aDomainLen {
return nil, errors.New("SOCKS4a domain name too long")
}
if err := validateSocks4CString(host, "domain name"); err != nil {
return nil, err
}
ip = net.IPv4(0, 0, 0, 1).To4()
domain = host
} else {
ips, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, errors.New("no IPv4 address found for host")
}
ip = ips[0].To4()
if ip == nil {
return nil, errors.New("no IPv4 address found for host")
}
}

// VN | CD | DSTPORT | DSTIP | USERID | NULL [| DOMAIN | NULL]
b := make([]byte, 0, 9+len(d.UserID)+len(domain)+1)
b = append(b, Version4, byte(d.cmd))
b = append(b, byte(port>>8), byte(port))
b = append(b, ip...)
b = append(b, d.UserID...)
b = append(b, 0)
if domain != "" {
b = append(b, domain...)
b = append(b, 0)
}
if _, err := c.Write(b); err != nil {
return nil, err
}

// Reply is always 8 bytes: VN | CD | DSTPORT | DSTIP
var resp [8]byte
if _, err := io.ReadFull(c, resp[:]); err != nil {
return nil, err
}
// Spec says VN should be 0; some servers incorrectly echo 4.
if resp[0] != 0 && resp[0] != Version4 {
return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(resp[0])))
}
if code := Reply(resp[1]); code != Status4Granted {
return nil, errors.New(code.String())
}

a := &Addr{
IP: net.IPv4(resp[4], resp[5], resp[6], resp[7]),
Port: int(resp[2])<<8 | int(resp[3]),
}
return a, nil
}

// validateSocks4CString rejects strings that would truncate a SOCKS4 C-string field.
func validateSocks4CString(s, field string) error {
for i := 0; i < len(s); i++ {
if s[i] == 0 {
return errors.New("invalid SOCKS4 " + field + ": contains NUL")
}
}
return nil
}

// connect5 implements the SOCKS5 CONNECT handshake.
func (d *Dialer) connect5(ctx context.Context, c net.Conn, host string, port int) (net.Addr, error) {
b := make([]byte, 0, 6+len(host)) // the size here is just an estimate
b = append(b, Version5)
if len(d.AuthMethods) == 0 || d.Authenticate == nil {
Expand All @@ -61,12 +164,12 @@ func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net
b = append(b, byte(am))
}
}
if _, ctxErr = c.Write(b); ctxErr != nil {
return
if _, err := c.Write(b); err != nil {
return nil, err
}

if _, ctxErr = io.ReadFull(c, b[:2]); ctxErr != nil {
return
if _, err := io.ReadFull(c, b[:2]); err != nil {
return nil, err
}
if b[0] != Version5 {
return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0])))
Expand All @@ -76,8 +179,8 @@ func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net
return nil, errors.New("no acceptable authentication methods")
}
if d.Authenticate != nil {
if ctxErr = d.Authenticate(ctx, c, am); ctxErr != nil {
return
if err := d.Authenticate(ctx, c, am); err != nil {
return nil, err
}
}

Expand All @@ -102,12 +205,12 @@ func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net
b = append(b, host...)
}
b = append(b, byte(port>>8), byte(port))
if _, ctxErr = c.Write(b); ctxErr != nil {
return
if _, err := c.Write(b); err != nil {
return nil, err
}

if _, ctxErr = io.ReadFull(c, b[:4]); ctxErr != nil {
return
if _, err := io.ReadFull(c, b[:4]); err != nil {
return nil, err
}
if b[0] != Version5 {
return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0])))
Expand Down Expand Up @@ -140,8 +243,8 @@ func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net
} else {
b = b[:l]
}
if _, ctxErr = io.ReadFull(c, b); ctxErr != nil {
return
if _, err := io.ReadFull(c, b); err != nil {
return nil, err
}
if a.IP != nil {
copy(a.IP, b)
Expand Down
61 changes: 57 additions & 4 deletions internal/socks/socks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package socks provides a SOCKS version 5 client implementation.
// Package socks provides SOCKS version 4/4a and 5 client implementations.
//
// SOCKS protocol version 4 is described in:
//
// https://www.openssh.org/txt/socks4.protocol
//
// SOCKS protocol version 4a (domain name extension) is described in:
//
// https://www.openssh.org/txt/socks4a.protocol
//
// SOCKS protocol version 5 is defined in RFC 1928.
// Username/Password authentication for SOCKS version 5 is defined in
Expand Down Expand Up @@ -39,6 +47,7 @@ type Reply int

func (code Reply) String() string {
switch code {
// SOCKS5 reply codes
case StatusSucceeded:
return "succeeded"
case 0x01:
Expand All @@ -57,13 +66,23 @@ func (code Reply) String() string {
return "command not supported"
case 0x08:
return "address type not supported"
// SOCKS4 reply codes
case Status4Granted:
return "request granted"
case Status4Rejected:
return "request rejected or failed"
case Status4IdentdFailed:
return "request rejected because SOCKS server cannot connect to identd on the client"
case Status4IdentdMismatch:
return "request rejected because the client program and identd report different user-ids"
default:
return "unknown code: " + strconv.Itoa(int(code))
}
}

// Wire protocol constants.
const (
Version4 = 0x04
Version5 = 0x05

AddrTypeIPv4 = 0x01
Expand All @@ -77,7 +96,14 @@ const (
AuthMethodUsernamePassword AuthMethod = 0x02 // use username/password
AuthMethodNoAcceptableMethods AuthMethod = 0xff // no acceptable authentication methods

// SOCKS5 reply codes
StatusSucceeded Reply = 0x00

// SOCKS4 reply codes
Status4Granted Reply = 90
Status4Rejected Reply = 91
Status4IdentdFailed Reply = 92
Status4IdentdMismatch Reply = 93
)

// An Addr represents a SOCKS-specific address.
Expand Down Expand Up @@ -124,21 +150,43 @@ type Dialer struct {
proxyNetwork string // network between a proxy server and a client
proxyAddress string // proxy server address

// Version specifies the SOCKS protocol version.
// Supported values are Version4 and Version5.
// If zero, Version5 is used.
Version int

// Socks4A enables the SOCKS4a extension so domain names are
// resolved by the proxy instead of the client.
// Only used when Version is Version4.
Socks4A bool

// UserID is the SOCKS4 user identity string sent to the proxy.
// Only used when Version is Version4. Empty UserID is allowed.
UserID string

// ProxyDial specifies the optional dial function for
// establishing the transport connection.
ProxyDial func(context.Context, string, string) (net.Conn, error)

// AuthMethods specifies the list of request authentication
// methods.
// methods. Only used when Version is Version5.
// If empty, SOCKS client requests only AuthMethodNotRequired.
AuthMethods []AuthMethod

// Authenticate specifies the optional authentication
// function. It must be non-nil when AuthMethods is not empty.
// It must return an error when the authentication is failed.
// Only used when Version is Version5.
Authenticate func(context.Context, io.ReadWriter, AuthMethod) error
}

func (d *Dialer) version() int {
if d.Version == 0 {
return Version5
}
return d.Version
}

// DialContext connects to the provided address on the provided
// network.
//
Expand Down Expand Up @@ -237,9 +285,14 @@ func (d *Dialer) pathAddrs(address string) (proxy, dst net.Addr, err error) {
}

// NewDialer returns a new Dialer that dials through the provided
// proxy server's network and address.
// proxy server's network and address using SOCKS5 by default.
func NewDialer(network, address string) *Dialer {
return &Dialer{proxyNetwork: network, proxyAddress: address, cmd: CmdConnect}
return &Dialer{
proxyNetwork: network,
proxyAddress: address,
cmd: CmdConnect,
Version: Version5,
}
}

const (
Expand Down
Loading
Loading