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