/* 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
}