Files
drover-go/internal/socks5/client_test.go
T
root a45c1c0ab7
Build / test (push) Failing after 31s
Build / build-windows (push) Has been skipped
internal/socks5: production TCP CONNECT client
Separate from internal/checker/socks5.go (different requirements: no
hex dumps, no diagnostic-friendly errors, faster path). Single Dial
entry point that handles greet + optional auth + CONNECT and returns
a ready-to-use net.Conn. UDP support deferred to P2.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:50:03 +03:00

200 lines
4.4 KiB
Go

package socks5
import (
"context"
"io"
"net"
"strconv"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fakeProxy is a minimal SOCKS5 server that accepts greet+CONNECT
// (and optional auth) and then splices the connection to a target
// listener supplied by the test.
type fakeProxy struct {
addr string
target string
useAuth bool
login string
password string
}
func startFakeProxy(t *testing.T, target string, useAuth bool, login, password string) *fakeProxy {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { ln.Close() })
p := &fakeProxy{
addr: ln.Addr().String(),
target: target,
useAuth: useAuth, login: login, password: password,
}
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
go p.handle(c)
}
}()
return p
}
func (p *fakeProxy) handle(c net.Conn) {
defer c.Close()
buf := make([]byte, 256)
// Greeting: 05 N method...
io.ReadFull(c, buf[:2])
nmethods := int(buf[1])
io.ReadFull(c, buf[:nmethods])
if p.useAuth {
c.Write([]byte{0x05, 0x02})
io.ReadFull(c, buf[:2])
ulen := int(buf[1])
io.ReadFull(c, buf[:ulen])
login := string(buf[:ulen])
io.ReadFull(c, buf[:1])
plen := int(buf[0])
io.ReadFull(c, buf[:plen])
pwd := string(buf[:plen])
if login != p.login || pwd != p.password {
c.Write([]byte{0x01, 0x01})
return
}
c.Write([]byte{0x01, 0x00})
} else {
c.Write([]byte{0x05, 0x00})
}
// CONNECT request: 05 01 00 ATYP ...
io.ReadFull(c, buf[:4])
atyp := buf[3]
var host string
switch atyp {
case 1:
io.ReadFull(c, buf[:4])
host = net.IPv4(buf[0], buf[1], buf[2], buf[3]).String()
case 3:
io.ReadFull(c, buf[:1])
hlen := int(buf[0])
io.ReadFull(c, buf[:hlen])
host = string(buf[:hlen])
}
io.ReadFull(c, buf[:2])
port := int(buf[0])<<8 | int(buf[1])
// Reply REP=0
c.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
// Splice to target
target, err := net.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return
}
defer target.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); io.Copy(target, c) }()
go func() { defer wg.Done(); io.Copy(c, target) }()
wg.Wait()
}
func TestDial_NoAuth_HappyPath(t *testing.T) {
target, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer target.Close()
go func() {
c, err := target.Accept()
if err != nil {
return
}
defer c.Close()
c.Write([]byte("hello"))
}()
p := startFakeProxy(t, target.Addr().String(), false, "", "")
host, port, _ := net.SplitHostPort(target.Addr().String())
portU, _ := atoiU16(port)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
conn, err := Dial(ctx, Config{
ProxyAddr: p.addr,
}, host, portU)
require.NoError(t, err)
defer conn.Close()
buf := make([]byte, 5)
io.ReadFull(conn, buf)
assert.Equal(t, "hello", string(buf))
}
func TestDial_WithAuth_HappyPath(t *testing.T) {
target, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer target.Close()
go func() { c, _ := target.Accept(); if c != nil { c.Write([]byte("auth-ok")); c.Close() } }()
p := startFakeProxy(t, target.Addr().String(), true, "user", "pass")
host, port, _ := net.SplitHostPort(target.Addr().String())
portU, _ := atoiU16(port)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
conn, err := Dial(ctx, Config{
ProxyAddr: p.addr,
UseAuth: true,
Login: "user",
Password: "pass",
}, host, portU)
require.NoError(t, err)
defer conn.Close()
buf := make([]byte, 7)
io.ReadFull(conn, buf)
assert.Equal(t, "auth-ok", string(buf))
}
func TestDial_BadAuth(t *testing.T) {
target, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer target.Close()
p := startFakeProxy(t, target.Addr().String(), true, "user", "pass")
host, port, _ := net.SplitHostPort(target.Addr().String())
portU, _ := atoiU16(port)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, err = Dial(ctx, Config{
ProxyAddr: p.addr,
UseAuth: true,
Login: "wrong",
Password: "wrong",
}, host, portU)
require.Error(t, err)
}
func atoiU16(s string) (uint16, error) {
var n int
for _, c := range s {
if c < '0' || c > '9' {
return 0, &net.AddrError{Err: "invalid port", Addr: s}
}
n = n*10 + int(c-'0')
}
return uint16(n), nil
}