Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
5.2 KB go
/* SPDX-License-Identifier: MIT
 *
 * Phobos
 */

package phobos

import (
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net/netip"
	"slices"
	"unsafe"

	"golang.zx2c4.com/wireguard/windows/phobos/cobf"
)

const (
	socks5Version = 0x05

	cmdConnect      = 0x01
	cmdUDPAssociate = 0x03

	atypIPv4   = 0x01
	atypDomain = 0x03
	atypIPv6   = 0x04

	methodNoAuth   = 0x00
	methodUserPass = 0x02
	methodNone     = 0xFF

	replySucceeded             = 0x00
	replyGeneralFailure        = 0x01
	replyCommandNotSupported   = 0x07
	replyAddressTypeNotAllowed = 0x08

	userPassVersion = 0x01
	credentialMax   = 255
)

var (
	errNotSocks5       = errors.New("phobos: not a SOCKS5 message")
	errUnsupportedATYP = errors.New("phobos: unsupported SOCKS5 address type")
)

type socks5Target struct {
	atyp   byte
	addr   []byte
	domain string
	port   uint16
}

func targetFromAddrPort(addr netip.AddrPort) socks5Target {
	if addr.Addr().Is4() || addr.Addr().Is4In6() {
		ip := addr.Addr().As4()
		return socks5Target{atyp: atypIPv4, addr: ip[:], port: addr.Port()}
	}
	ip := addr.Addr().As16()
	return socks5Target{atyp: atypIPv6, addr: ip[:], port: addr.Port()}
}

func targetFromHostPort(host string, port uint16) socks5Target {
	if addr, err := netip.ParseAddr(host); err == nil {
		return targetFromAddrPort(netip.AddrPortFrom(addr, port))
	}
	return socks5Target{atyp: atypDomain, domain: host, port: port}
}

func (t socks5Target) size() int {
	switch t.atyp {
	case atypIPv4:
		return 1 + 4 + 2
	case atypIPv6:
		return 1 + 16 + 2
	default:
		return 1 + 1 + len(t.domain) + 2
	}
}

func (t socks5Target) addrBytes() []byte {
	if t.atyp == atypDomain {
		return unsafe.Slice(unsafe.StringData(t.domain), len(t.domain))
	}
	return t.addr
}

func (t socks5Target) appendTo(out []byte) []byte {
	base := len(out)
	out = slices.Grow(out, t.size())
	n := cobf.BuildTarget(out[base:base+t.size()], t.atyp, t.addrBytes(), t.port)
	if n < 0 {
		return out[:base]
	}
	return out[:base+n]
}

func (t socks5Target) addrPort() (netip.AddrPort, bool) {
	addr, ok := netip.AddrFromSlice(t.addr)
	if !ok {
		return netip.AddrPort{}, false
	}
	return netip.AddrPortFrom(addr, t.port), true
}

func parseTarget(buf []byte) (socks5Target, int, error) {
	atyp, addr, port, consumed := cobf.ParseTarget(buf)
	switch {
	case consumed == 0:
		return socks5Target{}, 0, io.ErrUnexpectedEOF
	case consumed < 0:
		return socks5Target{atyp: buf[0]}, 0, errUnsupportedATYP
	}
	target := socks5Target{atyp: atyp, port: port}
	if atyp == atypDomain {
		target.domain = string(addr)
	} else {
		target.addr = addr
	}
	return target, consumed, nil
}

func buildGreeting(methods ...byte) []byte {
	out := make([]byte, 0, 2+len(methods))
	out = append(out, socks5Version, byte(len(methods)))
	return append(out, methods...)
}

func buildUserPass(login, password string) ([]byte, error) {
	if len(login) > credentialMax || len(password) > credentialMax {
		return nil, errors.New("phobos: SOCKS5 credentials are too long")
	}
	out := make([]byte, 0, 3+len(login)+len(password))
	out = append(out, userPassVersion, byte(len(login)))
	out = append(out, login...)
	out = append(out, byte(len(password)))
	return append(out, password...), nil
}

func buildRequest(command byte, target socks5Target) []byte {
	out := make([]byte, 0, 3+target.size())
	out = append(out, socks5Version, command, 0x00)
	return target.appendTo(out)
}

func buildReply(reply byte, bound netip.AddrPort) []byte {
	target := socks5Target{atyp: atypIPv4, addr: make([]byte, 4)}
	if bound.IsValid() {
		target = targetFromAddrPort(bound)
	}
	out := make([]byte, 0, 3+target.size())
	out = append(out, socks5Version, reply, 0x00)
	return target.appendTo(out)
}

func readReply(reader io.Reader, buf []byte) (byte, socks5Target, error) {
	if _, err := io.ReadFull(reader, buf[:4]); err != nil {
		return 0, socks5Target{}, err
	}
	if buf[0] != socks5Version {
		return 0, socks5Target{}, errNotSocks5
	}
	reply := buf[1]

	var addressLength int
	switch buf[3] {
	case atypIPv4:
		addressLength = 4
	case atypIPv6:
		addressLength = 16
	case atypDomain:
		if _, err := io.ReadFull(reader, buf[4:5]); err != nil {
			return reply, socks5Target{}, err
		}
		addressLength = 1 + int(buf[4])
		if _, err := io.ReadFull(reader, buf[5:4+addressLength+2]); err != nil {
			return reply, socks5Target{}, err
		}
		target, _, err := parseTarget(buf[3 : 4+addressLength+2])
		return reply, target, err
	default:
		return reply, socks5Target{}, errUnsupportedATYP
	}

	if _, err := io.ReadFull(reader, buf[4:4+addressLength+2]); err != nil {
		return reply, socks5Target{}, err
	}
	target, _, err := parseTarget(buf[3 : 4+addressLength+2])
	return reply, target, err
}

func buildUDPFrame(target socks5Target, payload []byte, out []byte) ([]byte, error) {
	body := 3 + target.size() + len(payload)
	if body > s5AccMax-2 {
		return nil, fmt.Errorf("phobos: UDP payload of %d bytes does not fit a tunnel frame", len(payload))
	}
	out = binary.BigEndian.AppendUint16(out[:0], uint16(body))
	out = append(out, 0x00, 0x00, 0x00)
	out = target.appendTo(out)
	return append(out, payload...), nil
}

func parseUDPHeader(frame []byte) (socks5Target, int, error) {
	if len(frame) < 4 || frame[2] != 0 {
		return socks5Target{}, 0, errNotSocks5
	}
	target, consumed, err := parseTarget(frame[3:])
	if err != nil {
		return target, 0, err
	}
	return target, 3 + consumed, nil
}