mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 08:15:33 +00:00
feat(socks5): add Tor RESOLVE/RESOLVE_PTR extension command support (gost#524)
Adds SOCKS5 handler and connector support for Tor-specific RESOLVE (0xF0) and RESOLVE_PTR (0xF1) commands, enabling tools like torsocks and tor-resolve to work through GOST when forwarding to a Tor SOCKS5 proxy. - Define CmdResolve (0xF0) and CmdResolvePTR (0xF1) in x/internal/util/socks - Add ContextWithSocks5Cmd/Socks5CmdFromContext for per-request command override - Connector reads context command, sends it upstream, returns resolveConn - resolveConn wraps net.Conn and exposes resolved addr via Metadata() - Handler dispatches 0xF0/0xF1 to handleResolve/handleResolvePTR - Gate behind enableTor metadata (tor=true), reject with CmdUnsupported by default - 9 unit tests across ctx/, connector/, handler/ packages
This commit is contained in:
@@ -137,7 +137,12 @@ func (c *socks5Connector) Connect(ctx context.Context, conn net.Conn, network, a
|
||||
addr.Host = "127.0.0.1"
|
||||
}
|
||||
|
||||
req := gosocks5.NewRequest(gosocks5.CmdConnect, &addr)
|
||||
cmd := gosocks5.CmdConnect
|
||||
if c, ok := ctxvalue.Socks5CmdFromContext(ctx); ok {
|
||||
cmd = c
|
||||
}
|
||||
|
||||
req := gosocks5.NewRequest(cmd, &addr)
|
||||
log.Trace(req)
|
||||
if err := req.Write(conn); err != nil {
|
||||
log.Error(err)
|
||||
@@ -157,6 +162,10 @@ func (c *socks5Connector) Connect(ctx context.Context, conn net.Conn, network, a
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cmd == socks.CmdResolve || cmd == socks.CmdResolvePTR {
|
||||
return &resolveConn{Conn: conn, resolvedAddr: reply.Addr}, nil
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
md "github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/gosocks5"
|
||||
xmd "github.com/go-gost/x/metadata"
|
||||
)
|
||||
|
||||
// resolveConn wraps a raw net.Conn and carries the resolved address from a
|
||||
// Tor RESOLVE/RESOLVE_PTR reply as metadata. It implements net.Conn (passthrough)
|
||||
// and metadata.Metadatable so the handler can extract the resolved address.
|
||||
type resolveConn struct {
|
||||
net.Conn
|
||||
resolvedAddr *gosocks5.Addr
|
||||
}
|
||||
|
||||
func (c *resolveConn) Metadata() md.Metadata {
|
||||
return xmd.NewMetadata(map[string]any{
|
||||
"resolvedAddr": c.resolvedAddr,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/gosocks5"
|
||||
)
|
||||
|
||||
type dummyConn struct{ net.Conn }
|
||||
|
||||
func TestResolveConn_Metadata(t *testing.T) {
|
||||
addr := &gosocks5.Addr{Type: gosocks5.AddrIPv4, Host: "93.184.216.34", Port: 0}
|
||||
rc := &resolveConn{Conn: dummyConn{}, resolvedAddr: addr}
|
||||
|
||||
md := rc.Metadata()
|
||||
if md == nil {
|
||||
t.Fatal("Metadata() returned nil")
|
||||
}
|
||||
|
||||
v := md.Get("resolvedAddr")
|
||||
if v == nil {
|
||||
t.Fatal("resolvedAddr key not present in metadata")
|
||||
}
|
||||
|
||||
got, ok := v.(*gosocks5.Addr)
|
||||
if !ok {
|
||||
t.Fatalf("resolvedAddr is %T, want *gosocks5.Addr", v)
|
||||
}
|
||||
if got.Host != "93.184.216.34" {
|
||||
t.Errorf("Host = %q, want %q", got.Host, "93.184.216.34")
|
||||
}
|
||||
if got.Type != gosocks5.AddrIPv4 {
|
||||
t.Errorf("Type = %d, want %d", got.Type, gosocks5.AddrIPv4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveConn_Metadata_NilAddr(t *testing.T) {
|
||||
rc := &resolveConn{Conn: dummyConn{}, resolvedAddr: nil}
|
||||
|
||||
md := rc.Metadata()
|
||||
v := md.Get("resolvedAddr")
|
||||
addr, ok := v.(*gosocks5.Addr)
|
||||
if !ok {
|
||||
t.Fatalf("resolvedAddr has type %T, want *gosocks5.Addr", v)
|
||||
}
|
||||
if addr != nil {
|
||||
t.Errorf("resolvedAddr = %v, want nil", addr)
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,22 @@ func PeerCertFromContext(ctx context.Context) *PeerCert {
|
||||
return v
|
||||
}
|
||||
|
||||
type socks5CmdKey struct{}
|
||||
|
||||
// ContextWithSocks5Cmd returns a copy of ctx that carries a SOCKS5 command
|
||||
// override. Connectors use this to send a non-CONNECT command (e.g. Tor
|
||||
// RESOLVE/RESOLVE_PTR) to the upstream proxy.
|
||||
func ContextWithSocks5Cmd(ctx context.Context, cmd uint8) context.Context {
|
||||
return context.WithValue(ctx, socks5CmdKey{}, cmd)
|
||||
}
|
||||
|
||||
// Socks5CmdFromContext returns the SOCKS5 command override stored in ctx.
|
||||
// The bool is false if no override is present.
|
||||
func Socks5CmdFromContext(ctx context.Context) (uint8, bool) {
|
||||
v, ok := ctx.Value(socks5CmdKey{}).(uint8)
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// labelsKey saves the static service labels.
|
||||
type labelsKey struct{}
|
||||
|
||||
|
||||
@@ -261,6 +261,33 @@ func (c *testContextConn) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func TestContextWithSocks5Cmd(t *testing.T) {
|
||||
ctx := ContextWithSocks5Cmd(context.Background(), 0xF0)
|
||||
|
||||
cmd, ok := Socks5CmdFromContext(ctx)
|
||||
if !ok {
|
||||
t.Fatal("Socks5CmdFromContext() ok = false, want true")
|
||||
}
|
||||
if cmd != 0xF0 {
|
||||
t.Errorf("Socks5CmdFromContext() = 0x%02X, want 0xF0", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocks5CmdFromContext_Empty(t *testing.T) {
|
||||
_, ok := Socks5CmdFromContext(context.Background())
|
||||
if ok {
|
||||
t.Error("Socks5CmdFromContext(empty) ok = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocks5CmdFromContext_WrongType(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), socks5CmdKey{}, "not-a-uint8")
|
||||
_, ok := Socks5CmdFromContext(ctx)
|
||||
if ok {
|
||||
t.Error("Socks5CmdFromContext(wrong type) ok = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextInterface(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = ContextWithSid(ctx, Sid("iface-test"))
|
||||
|
||||
@@ -208,6 +208,10 @@ func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
||||
case socks.CmdUDPTun:
|
||||
ro.Network = "udp"
|
||||
return h.handleUDPTun(ctx, conn, networkAddr("udp", req.Addr), address, ro, log)
|
||||
case socks.CmdResolve:
|
||||
return h.handleResolve(ctx, conn, address, log)
|
||||
case socks.CmdResolvePTR:
|
||||
return h.handleResolvePTR(ctx, conn, address, log)
|
||||
default:
|
||||
err = ErrUnknownCmd
|
||||
log.Error(err)
|
||||
|
||||
@@ -31,6 +31,7 @@ type metadata struct {
|
||||
udpBindMax int
|
||||
compatibilityMode bool
|
||||
hash string
|
||||
enableTor bool
|
||||
muxCfg *mux.Config
|
||||
|
||||
observerPeriod time.Duration
|
||||
@@ -67,6 +68,7 @@ func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
|
||||
h.md.compatibilityMode = mdutil.GetBool(md, "comp")
|
||||
h.md.hash = mdutil.GetString(md, "hash")
|
||||
h.md.enableTor = mdutil.GetBool(md, "tor", "enableTor", "socks5.tor")
|
||||
|
||||
h.md.muxCfg = &mux.Config{
|
||||
Version: mdutil.GetInt(md, "mux.version"),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
md "github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/gosocks5"
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
"github.com/go-gost/x/internal/util/socks"
|
||||
)
|
||||
|
||||
// handleResolve handles a Tor SOCKS5 RESOLVE command (0xF0).
|
||||
// It resolves a hostname to an IP address via the upstream Tor SOCKS5 proxy.
|
||||
func (h *socks5Handler) handleResolve(ctx context.Context, conn net.Conn, address string, log logger.Logger) error {
|
||||
if !h.md.enableTor {
|
||||
return h.rejectCmd(conn, log)
|
||||
}
|
||||
|
||||
ctx = xctx.ContextWithSocks5Cmd(ctx, socks.CmdResolve)
|
||||
return h.doResolve(ctx, conn, address, log)
|
||||
}
|
||||
|
||||
// handleResolvePTR handles a Tor SOCKS5 RESOLVE_PTR command (0xF1).
|
||||
// It performs a reverse DNS lookup (IP to hostname) via the upstream Tor SOCKS5 proxy.
|
||||
func (h *socks5Handler) handleResolvePTR(ctx context.Context, conn net.Conn, address string, log logger.Logger) error {
|
||||
if !h.md.enableTor {
|
||||
return h.rejectCmd(conn, log)
|
||||
}
|
||||
|
||||
ctx = xctx.ContextWithSocks5Cmd(ctx, socks.CmdResolvePTR)
|
||||
return h.doResolve(ctx, conn, address, log)
|
||||
}
|
||||
|
||||
// rejectCmd writes a CmdUnsupported reply and returns ErrUnknownCmd.
|
||||
func (h *socks5Handler) rejectCmd(conn net.Conn, log logger.Logger) error {
|
||||
log.Error(ErrUnknownCmd)
|
||||
resp := gosocks5.NewReply(gosocks5.CmdUnsupported, nil)
|
||||
log.Trace(resp)
|
||||
resp.Write(conn)
|
||||
return ErrUnknownCmd
|
||||
}
|
||||
|
||||
func (h *socks5Handler) doResolve(ctx context.Context, conn net.Conn, address string, log logger.Logger) error {
|
||||
cc, err := h.options.Router.Dial(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
writeSocksReply(conn, gosocks5.Failure, nil)
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Extract resolved address from the chain connection's metadata.
|
||||
var resolvedAddr *gosocks5.Addr
|
||||
if mdConn, ok := cc.(md.Metadatable); ok {
|
||||
if v := mdConn.Metadata().Get("resolvedAddr"); v != nil {
|
||||
resolvedAddr, _ = v.(*gosocks5.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
if resolvedAddr == nil {
|
||||
err = errors.New("socks5: no resolved address in reply")
|
||||
log.Error(err)
|
||||
writeSocksReply(conn, gosocks5.Failure, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("resolved %s -> %s", address, resolvedAddr)
|
||||
resp := gosocks5.NewReply(gosocks5.Succeeded, resolvedAddr)
|
||||
log.Trace(resp)
|
||||
resp.Write(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeSocksReply(conn net.Conn, rep uint8, addr *gosocks5.Addr) error {
|
||||
resp := gosocks5.NewReply(rep, addr)
|
||||
return resp.Write(conn)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/gosocks5"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
)
|
||||
|
||||
func testLogger() logger.Logger {
|
||||
return xlogger.Nop()
|
||||
}
|
||||
|
||||
func TestWriteSocksReply(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
addr := &gosocks5.Addr{Type: gosocks5.AddrIPv4, Host: "127.0.0.1", Port: 9050}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- writeSocksReply(server, gosocks5.Succeeded, addr)
|
||||
}()
|
||||
|
||||
reply, err := gosocks5.ReadReply(client)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadReply: %v", err)
|
||||
}
|
||||
if reply.Rep != gosocks5.Succeeded {
|
||||
t.Errorf("Rep = %d, want %d", reply.Rep, gosocks5.Succeeded)
|
||||
}
|
||||
if reply.Addr == nil || reply.Addr.Host != "127.0.0.1" {
|
||||
t.Errorf("Addr = %v, want 127.0.0.1", reply.Addr)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Errorf("writeSocksReply error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSocksReply_CmdUnsupported(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- writeSocksReply(server, gosocks5.CmdUnsupported, nil)
|
||||
}()
|
||||
|
||||
reply, err := gosocks5.ReadReply(client)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadReply: %v", err)
|
||||
}
|
||||
if reply.Rep != gosocks5.CmdUnsupported {
|
||||
t.Errorf("Rep = %d, want %d", reply.Rep, gosocks5.CmdUnsupported)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Errorf("writeSocksReply error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleResolve_TorDisabled(t *testing.T) {
|
||||
h := &socks5Handler{
|
||||
md: metadata{enableTor: false},
|
||||
}
|
||||
h.options.Logger = testLogger()
|
||||
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- h.handleResolve(context.Background(), server, "example.com:0", testLogger())
|
||||
}()
|
||||
|
||||
reply, err := gosocks5.ReadReply(client)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadReply: %v", err)
|
||||
}
|
||||
if reply.Rep != gosocks5.CmdUnsupported {
|
||||
t.Errorf("Rep = %d, want CmdUnsupported (%d)", reply.Rep, gosocks5.CmdUnsupported)
|
||||
}
|
||||
|
||||
err = <-done
|
||||
if err == nil {
|
||||
t.Error("expected error when tor is disabled, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleResolvePTR_TorDisabled(t *testing.T) {
|
||||
h := &socks5Handler{
|
||||
md: metadata{enableTor: false},
|
||||
}
|
||||
h.options.Logger = testLogger()
|
||||
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- h.handleResolvePTR(context.Background(), server, "192.168.1.1:0", testLogger())
|
||||
}()
|
||||
|
||||
reply, err := gosocks5.ReadReply(client)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadReply: %v", err)
|
||||
}
|
||||
if reply.Rep != gosocks5.CmdUnsupported {
|
||||
t.Errorf("Rep = %d, want CmdUnsupported (%d)", reply.Rep, gosocks5.CmdUnsupported)
|
||||
}
|
||||
|
||||
err = <-done
|
||||
if err == nil {
|
||||
t.Error("expected error when tor is disabled, got nil")
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,13 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
// CmdResolve is a Tor SOCKS5 extension command (0xF0) for resolving
|
||||
// a hostname to an IP address via Tor's DNS.
|
||||
CmdResolve uint8 = 0xF0
|
||||
// CmdResolvePTR is a Tor SOCKS5 extension command (0xF1) for reverse
|
||||
// DNS lookup (IP to hostname) via Tor's DNS.
|
||||
CmdResolvePTR uint8 = 0xF1
|
||||
|
||||
// CmdMuxBind is an extended SOCKS5 request CMD for
|
||||
// multiplexing transport with the binding server.
|
||||
CmdMuxBind uint8 = 0xF2
|
||||
|
||||
Reference in New Issue
Block a user