181 lines
5.0 KiB
Go
181 lines
5.0 KiB
Go
package probe
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
socks5Version = 0x05
|
|
socks5NoAuth = 0x00
|
|
socks5UserPassword = 0x02
|
|
socks5NoAcceptable = 0xff
|
|
socks5ConnectCommand = 0x01
|
|
socks5AddressIPv4 = 0x01
|
|
socks5AddressDomain = 0x03
|
|
socks5AddressIPv6 = 0x04
|
|
)
|
|
|
|
var errSOCKS5Authentication = errors.New("SOCKS5 authentication failed")
|
|
|
|
type socks5Dialer struct {
|
|
proxyAddress string
|
|
username string
|
|
password string
|
|
dialer net.Dialer
|
|
}
|
|
|
|
func newSOCKS5Dialer(proxyURL *url.URL) socks5Dialer {
|
|
username := ""
|
|
password := ""
|
|
if proxyURL.User != nil {
|
|
username = proxyURL.User.Username()
|
|
password, _ = proxyURL.User.Password()
|
|
}
|
|
return socks5Dialer{proxyAddress: proxyURL.Host, username: username, password: password}
|
|
}
|
|
|
|
func (dialer socks5Dialer) DialContext(ctx context.Context, _ string, address string) (net.Conn, error) {
|
|
connection, err := dialer.dialer.DialContext(ctx, "tcp", dialer.proxyAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
|
|
if err := connection.SetDeadline(deadline); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
defer connection.SetDeadline(time.Time{})
|
|
}
|
|
if err := dialer.connect(connection, address); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
return connection, nil
|
|
}
|
|
|
|
func (dialer socks5Dialer) connect(connection net.Conn, address string) error {
|
|
method := byte(socks5NoAuth)
|
|
if dialer.username != "" || dialer.password != "" {
|
|
method = socks5UserPassword
|
|
}
|
|
if _, err := connection.Write([]byte{socks5Version, 0x01, method}); err != nil {
|
|
return err
|
|
}
|
|
selection := make([]byte, 2)
|
|
if _, err := io.ReadFull(connection, selection); err != nil {
|
|
return err
|
|
}
|
|
if selection[0] != socks5Version || selection[1] == socks5NoAcceptable {
|
|
return fmt.Errorf("invalid SOCKS5 method selection")
|
|
}
|
|
if selection[1] == socks5UserPassword {
|
|
if method != socks5UserPassword {
|
|
return errSOCKS5Authentication
|
|
}
|
|
if err := socks5Authenticate(connection, dialer.username, dialer.password); err != nil {
|
|
return err
|
|
}
|
|
} else if selection[1] != method {
|
|
return fmt.Errorf("unsupported SOCKS5 authentication method %d", selection[1])
|
|
}
|
|
request, err := socks5ConnectRequest(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := connection.Write(request); err != nil {
|
|
return err
|
|
}
|
|
return readSOCKS5ConnectReply(connection)
|
|
}
|
|
|
|
func socks5Authenticate(connection net.Conn, username, password string) error {
|
|
if len(username) == 0 || len(username) > 255 || len(password) == 0 || len(password) > 255 {
|
|
return errSOCKS5Authentication
|
|
}
|
|
request := make([]byte, 0, len(username)+len(password)+3)
|
|
request = append(request, 0x01, byte(len(username)))
|
|
request = append(request, username...)
|
|
request = append(request, byte(len(password)))
|
|
request = append(request, password...)
|
|
if _, err := connection.Write(request); err != nil {
|
|
return err
|
|
}
|
|
reply := make([]byte, 2)
|
|
if _, err := io.ReadFull(connection, reply); err != nil {
|
|
return err
|
|
}
|
|
if reply[0] != 0x01 || reply[1] != 0x00 {
|
|
return errSOCKS5Authentication
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func socks5ConnectRequest(address string) ([]byte, error) {
|
|
host, rawPort, err := net.SplitHostPort(address)
|
|
if err != nil || host == "" {
|
|
return nil, errors.New("invalid SOCKS5 target address")
|
|
}
|
|
port, err := strconv.ParseUint(rawPort, 10, 16)
|
|
if err != nil || port == 0 {
|
|
return nil, errors.New("invalid SOCKS5 target port")
|
|
}
|
|
request := []byte{socks5Version, socks5ConnectCommand, 0x00}
|
|
if parsed := net.ParseIP(host); parsed != nil {
|
|
if ipv4 := parsed.To4(); ipv4 != nil {
|
|
request = append(request, socks5AddressIPv4)
|
|
request = append(request, ipv4...)
|
|
} else {
|
|
request = append(request, socks5AddressIPv6)
|
|
request = append(request, parsed.To16()...)
|
|
}
|
|
} else {
|
|
if len(host) > 255 {
|
|
return nil, errors.New("SOCKS5 target hostname is too long")
|
|
}
|
|
request = append(request, socks5AddressDomain, byte(len(host)))
|
|
request = append(request, host...)
|
|
}
|
|
encodedPort := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(encodedPort, uint16(port))
|
|
return append(request, encodedPort...), nil
|
|
}
|
|
|
|
func readSOCKS5ConnectReply(reader io.Reader) error {
|
|
header := make([]byte, 4)
|
|
if _, err := io.ReadFull(reader, header); err != nil {
|
|
return err
|
|
}
|
|
if header[0] != socks5Version || header[1] != 0x00 || header[2] != 0x00 {
|
|
return fmt.Errorf("SOCKS5 CONNECT failed with status %d", header[1])
|
|
}
|
|
addressLength := 0
|
|
switch header[3] {
|
|
case socks5AddressIPv4:
|
|
addressLength = net.IPv4len
|
|
case socks5AddressIPv6:
|
|
addressLength = net.IPv6len
|
|
case socks5AddressDomain:
|
|
length := make([]byte, 1)
|
|
if _, err := io.ReadFull(reader, length); err != nil {
|
|
return err
|
|
}
|
|
addressLength = int(length[0])
|
|
if addressLength == 0 {
|
|
return errors.New("invalid SOCKS5 bound hostname")
|
|
}
|
|
default:
|
|
return errors.New("invalid SOCKS5 bound address type")
|
|
}
|
|
boundAddressAndPort := make([]byte, addressLength+2)
|
|
_, err := io.ReadFull(reader, boundAddressAndPort)
|
|
return err
|
|
}
|