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

package phobos

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net"
	"net/netip"
	"sync"
	"sync/atomic"
)

var (
	ErrSocks5Refused = errors.New("phobos: SOCKS5 server refused the request")
	errAuthFailed    = errors.New("phobos: SOCKS5 authentication failed")
)

type Socks5Config struct {
	Target     netip.AddrPort
	Key        []byte
	Masking    Masking
	Media      MediaParams
	Login      string
	Password   string
	ListenPort uint16
	Control    SocketControl
	Logf       func(format string, args ...any)
}

type Socks5Client struct {
	config   Socks5Config
	dialer   net.Dialer
	listener net.Listener

	running atomic.Bool
	wait    sync.WaitGroup

	mu    sync.Mutex
	serve map[net.Conn]struct{}
}

func NewSocks5Client(config Socks5Config) *Socks5Client {
	if config.Logf == nil {
		config.Logf = func(string, ...any) {}
	}
	return &Socks5Client{
		config: config,
		dialer: net.Dialer{Control: config.Control},
		serve:  make(map[net.Conn]struct{}),
	}
}

func (c *Socks5Client) trackServed(conn net.Conn) bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if !c.running.Load() {
		return false
	}
	c.serve[conn] = struct{}{}
	return true
}

func (c *Socks5Client) forgetServed(conn net.Conn) {
	c.mu.Lock()
	delete(c.serve, conn)
	c.mu.Unlock()
}

func (c *Socks5Client) closeServed() {
	c.mu.Lock()
	served := make([]net.Conn, 0, len(c.serve))
	for conn := range c.serve {
		served = append(served, conn)
	}
	c.serve = make(map[net.Conn]struct{})
	c.mu.Unlock()
	for _, conn := range served {
		conn.Close()
	}
}

func (c *Socks5Client) hasCredentials() bool {
	return len(c.config.Login) > 0 && len(c.config.Password) > 0
}

func (c *Socks5Client) open(ctx context.Context) (*obfConn, error) {
	if len(c.config.Key) == 0 {
		return nil, errors.New("phobos: obfuscation key is empty")
	}
	if !c.config.Target.IsValid() {
		return nil, errors.New("phobos: obfuscator target is not resolved")
	}
	conn, err := c.dialer.DialContext(ctx, "tcp", c.config.Target.String())
	if err != nil {
		return nil, err
	}
	if tcp, ok := conn.(*net.TCPConn); ok {
		tcp.SetNoDelay(true)
	}
	obfuscated := newObfConn(conn, c.config.Key, c.config.Masking, c.config.Media)
	if err := c.negotiate(obfuscated); err != nil {
		conn.Close()
		return nil, err
	}
	return obfuscated, nil
}

func (c *Socks5Client) negotiate(conn *obfConn) error {
	methods := []byte{methodNoAuth}
	if c.hasCredentials() {
		methods = []byte{methodNoAuth, methodUserPass}
	}
	if _, err := conn.Write(buildGreeting(methods...)); err != nil {
		return err
	}

	var response [2]byte
	if _, err := io.ReadFull(conn, response[:]); err != nil {
		return err
	}
	if response[0] != socks5Version {
		return errNotSocks5
	}
	switch response[1] {
	case methodNoAuth:
		return nil
	case methodUserPass:
		if !c.hasCredentials() {
			return errAuthFailed
		}
		auth, err := buildUserPass(c.config.Login, c.config.Password)
		if err != nil {
			return err
		}
		if _, err := conn.Write(auth); err != nil {
			return err
		}
		if _, err := io.ReadFull(conn, response[:]); err != nil {
			return err
		}
		if response[1] != 0x00 {
			return errAuthFailed
		}
		return nil
	default:
		return errAuthFailed
	}
}

func (c *Socks5Client) request(ctx context.Context, command byte, target socks5Target) (*obfConn, socks5Target, error) {
	conn, err := c.open(ctx)
	if err != nil {
		return nil, socks5Target{}, err
	}
	if _, err := conn.Write(buildRequest(command, target)); err != nil {
		conn.Close()
		return nil, socks5Target{}, err
	}
	buf := make([]byte, 262)
	reply, bound, err := readReply(conn, buf)
	if err != nil {
		conn.Close()
		return nil, socks5Target{}, err
	}
	if reply != replySucceeded {
		conn.Close()
		return nil, socks5Target{}, fmt.Errorf("%w: code %d", ErrSocks5Refused, reply)
	}
	return conn, bound, nil
}

func (c *Socks5Client) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
	conn, _, err := c.request(ctx, cmdConnect, targetFromHostPort(host, port))
	if err != nil {
		return nil, err
	}
	return conn, nil
}

type Socks5UDPSession struct {
	conn   *obfConn
	reader *bufio.Reader

	writeMu sync.Mutex
	frame   []byte
}

func (c *Socks5Client) DialUDP(ctx context.Context) (*Socks5UDPSession, error) {
	conn, _, err := c.request(ctx, cmdUDPAssociate, socks5Target{atyp: atypIPv4, addr: make([]byte, 4)})
	if err != nil {
		return nil, err
	}
	return &Socks5UDPSession{
		conn:   conn,
		reader: bufio.NewReaderSize(conn, s5BufferSize),
		frame:  make([]byte, s5AccMax),
	}, nil
}

func (s *Socks5UDPSession) WriteTo(payload []byte, target netip.AddrPort) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	frame, err := buildUDPFrame(targetFromAddrPort(target), payload, s.frame)
	if err != nil {
		return err
	}
	s.frame = frame[:0]
	_, err = s.conn.Write(frame)
	return err
}

func (s *Socks5UDPSession) ReadFrom(buf []byte) (int, netip.AddrPort, error) {
	var header [2]byte
	if _, err := io.ReadFull(s.reader, header[:]); err != nil {
		return 0, netip.AddrPort{}, err
	}
	length := int(binary.BigEndian.Uint16(header[:]))
	if length < 4 || length > s5AccMax {
		return 0, netip.AddrPort{}, errFrameCorrupt
	}
	frame := make([]byte, length)
	if _, err := io.ReadFull(s.reader, frame); err != nil {
		return 0, netip.AddrPort{}, err
	}
	target, offset, err := parseUDPHeader(frame)
	if err != nil {
		return 0, netip.AddrPort{}, err
	}
	source, ok := target.addrPort()
	if !ok {
		return 0, netip.AddrPort{}, errUnsupportedATYP
	}
	return copy(buf, frame[offset:]), source, nil
}

func (s *Socks5UDPSession) Close() error {
	return s.conn.Close()
}

func (c *Socks5Client) ListenPort() uint16 {
	if c.listener == nil {
		return 0
	}
	return uint16(c.listener.Addr().(*net.TCPAddr).Port)
}

func (c *Socks5Client) Start() error {
	if c.config.ListenPort == 0 {
		return nil
	}
	listener, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: int(c.config.ListenPort)})
	if err != nil {
		return fmt.Errorf("unable to open the local SOCKS5 listener: %w", err)
	}
	c.listener = listener
	c.running.Store(true)
	c.wait.Add(1)
	go c.acceptLoop()
	c.config.Logf("SOCKS5 proxy listening on 127.0.0.1:%d -> %v (masking %v)", c.ListenPort(), c.config.Target, c.config.Masking)
	return nil
}

func (c *Socks5Client) Stop() {
	if !c.running.Swap(false) {
		return
	}
	c.listener.Close()
	c.closeServed()
	c.wait.Wait()
	c.config.Logf("SOCKS5 proxy stopped")
}

func (c *Socks5Client) acceptLoop() {
	defer c.wait.Done()
	for {
		conn, err := c.listener.Accept()
		if err != nil {
			return
		}
		if !c.trackServed(conn) {
			conn.Close()
			return
		}
		c.wait.Add(1)
		go func() {
			defer c.wait.Done()
			defer c.forgetServed(conn)
			defer conn.Close()
			if err := c.handle(conn); err != nil && c.running.Load() {
				c.config.Logf("SOCKS5 proxy: %v", err)
			}
		}()
	}
}

func (c *Socks5Client) handle(conn net.Conn) error {
	reader := bufio.NewReader(conn)
	if err := c.serveHandshake(conn, reader); err != nil {
		return err
	}

	header := make([]byte, 262)
	if _, err := io.ReadFull(reader, header[:4]); err != nil {
		return err
	}
	if header[0] != socks5Version {
		return errNotSocks5
	}
	command := header[1]
	target, err := readTarget(reader, header[3:])
	if err != nil {
		return err
	}

	switch command {
	case cmdConnect:
		return c.serveConnect(conn, reader, target)
	case cmdUDPAssociate:
		return c.serveUDPAssociate(conn, reader)
	default:
		conn.Write(buildReply(replyCommandNotSupported, netip.AddrPort{}))
		return fmt.Errorf("phobos: unsupported SOCKS5 command %d", command)
	}
}

func (c *Socks5Client) serveHandshake(conn net.Conn, reader *bufio.Reader) error {
	var greeting [2]byte
	if _, err := io.ReadFull(reader, greeting[:]); err != nil {
		return err
	}
	if greeting[0] != socks5Version {
		return errNotSocks5
	}
	methods := make([]byte, greeting[1])
	if _, err := io.ReadFull(reader, methods); err != nil {
		return err
	}

	required := byte(methodNoAuth)
	if c.hasCredentials() {
		required = methodUserPass
	}
	offered := false
	for _, method := range methods {
		if method == required {
			offered = true
		}
	}
	if !offered {
		conn.Write([]byte{socks5Version, methodNone})
		return errAuthFailed
	}
	if _, err := conn.Write([]byte{socks5Version, required}); err != nil {
		return err
	}
	if required == methodNoAuth {
		return nil
	}
	return c.serveUserPass(conn, reader)
}

func (c *Socks5Client) serveUserPass(conn net.Conn, reader *bufio.Reader) error {
	var head [2]byte
	if _, err := io.ReadFull(reader, head[:]); err != nil {
		return err
	}
	if head[0] != userPassVersion {
		return errNotSocks5
	}
	login := make([]byte, head[1])
	if _, err := io.ReadFull(reader, login); err != nil {
		return err
	}
	var passwordLength [1]byte
	if _, err := io.ReadFull(reader, passwordLength[:]); err != nil {
		return err
	}
	password := make([]byte, passwordLength[0])
	if _, err := io.ReadFull(reader, password); err != nil {
		return err
	}

	if string(login) != c.config.Login || string(password) != c.config.Password {
		conn.Write([]byte{userPassVersion, 0x01})
		return errAuthFailed
	}
	_, err := conn.Write([]byte{userPassVersion, 0x00})
	return err
}

func readTarget(reader *bufio.Reader, buf []byte) (socks5Target, error) {
	switch buf[0] {
	case atypIPv4:
		if _, err := io.ReadFull(reader, buf[1:1+4+2]); err != nil {
			return socks5Target{}, err
		}
		target, _, err := parseTarget(buf[:1+4+2])
		return target, err
	case atypIPv6:
		if _, err := io.ReadFull(reader, buf[1:1+16+2]); err != nil {
			return socks5Target{}, err
		}
		target, _, err := parseTarget(buf[:1+16+2])
		return target, err
	case atypDomain:
		if _, err := io.ReadFull(reader, buf[1:2]); err != nil {
			return socks5Target{}, err
		}
		length := int(buf[1])
		if _, err := io.ReadFull(reader, buf[2:2+length+2]); err != nil {
			return socks5Target{}, err
		}
		target, _, err := parseTarget(buf[:2+length+2])
		return target, err
	default:
		return socks5Target{}, errUnsupportedATYP
	}
}

func (c *Socks5Client) serveConnect(conn net.Conn, reader *bufio.Reader, target socks5Target) error {
	upstream, _, err := c.request(context.Background(), cmdConnect, target)
	if err != nil {
		conn.Write(buildReply(replyGeneralFailure, netip.AddrPort{}))
		return err
	}
	defer upstream.Close()

	if _, err := conn.Write(buildReply(replySucceeded, netip.AddrPort{})); err != nil {
		return err
	}
	relay(conn, reader, upstream)
	return nil
}

func relay(downstream net.Conn, buffered *bufio.Reader, upstream net.Conn) {
	var wait sync.WaitGroup
	wait.Add(2)
	go func() {
		defer wait.Done()
		io.Copy(upstream, buffered)
		upstream.Close()
	}()
	go func() {
		defer wait.Done()
		io.Copy(downstream, upstream)
		downstream.Close()
	}()
	wait.Wait()
}

func (c *Socks5Client) serveUDPAssociate(conn net.Conn, reader *bufio.Reader) error {
	session, err := c.DialUDP(context.Background())
	if err != nil {
		conn.Write(buildReply(replyGeneralFailure, netip.AddrPort{}))
		return err
	}
	defer session.Close()

	relaySocket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
	if err != nil {
		conn.Write(buildReply(replyGeneralFailure, netip.AddrPort{}))
		return err
	}
	defer relaySocket.Close()

	bound := relaySocket.LocalAddr().(*net.UDPAddr).AddrPort()
	if _, err := conn.Write(buildReply(replySucceeded, bound)); err != nil {
		return err
	}

	var client atomic.Pointer[netip.AddrPort]
	var wait sync.WaitGroup
	wait.Add(2)
	go func() {
		defer wait.Done()
		defer session.Close()
		buf := make([]byte, s5AccMax)
		for {
			n, source, err := relaySocket.ReadFromUDPAddrPort(buf)
			if err != nil {
				return
			}
			target, offset, err := parseUDPHeader(buf[:n])
			if err != nil {
				continue
			}
			destination, ok := target.addrPort()
			if !ok {
				continue
			}
			client.Store(&source)
			if err := session.WriteTo(buf[offset:n], destination); err != nil {
				return
			}
		}
	}()
	go func() {
		defer wait.Done()
		defer relaySocket.Close()
		payload := make([]byte, s5AccMax)
		out := make([]byte, s5AccMax)
		for {
			n, source, err := session.ReadFrom(payload)
			if err != nil {
				return
			}
			destination := client.Load()
			if destination == nil {
				continue
			}
			frame, err := buildUDPFrame(targetFromAddrPort(source), payload[:n], out)
			if err != nil {
				continue
			}
			if _, err := relaySocket.WriteToUDPAddrPort(frame[2:], *destination); err != nil {
				return
			}
		}
	}()

	io.Copy(io.Discard, reader)
	relaySocket.Close()
	session.Close()
	wait.Wait()
	return nil
}