socks5

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 19 Imported by: 0

README

socks5

GoDoc test Go Report Card License

A production-grade RFC 1928 SOCKS5 server and client package for Go, with RFC 1929 username/password authentication. Supports CONNECT, BIND, and UDP ASSOCIATE with in-process virtual networking, pluggable middleware, and comprehensive observability.

GSSAPI (RFC 1961, method 0x01) and SOCKS4/4a are intentionally out of scope. The server emits a diagnostic log on receipt of a SOCKS4 version byte.


Features

Category Details
RFC 1928 CONNECT, BIND, UDP ASSOCIATE, fragment reassembly with non-contiguous reset, strict RSV validation (TCP + UDP), SOCKS4/4a graceful reject with diagnostic
RFC 1929 Username/password authentication with pluggable CredentialStore; constant-time password compare; per-IP backoff via MaxConnectionsPerIP
Extensibility Middleware chains, custom Handler per command, AddressRewriter, pluggable Forwarder / LiteForwarder, Observer, Metrics, RoutinePool
Virtual Networking Listener (net.Listener) and PacketConn (net.PacketConn) backed by net.Pipe — route CONNECT and UDP ASSOCIATE to in-process services without real sockets
Performance Ring-buffer proxy with TCP half-close and pooled slabs, atomic SPSC ring buffer, mutex-free idleConn, single-allocation wire builders (appendAddrSpec, Datagram.AppendTo), bucketed BytesPool with zero-on-Get, happy-eyeballs NameResolver, LRU-bounded UDP rate buckets
Security HandshakeTimeout, AssociateSetupTimeout, LDH hostname validation, FQDN control-char rejection, CONNECT to 0.0.0.0 / :: refused, StrictBindOrigin, StrictAssociateBinding, per-IP / per-source / per-port RuleSet helpers, structured Identity field on AuthContext, ErrServerAlreadyServing guard on double-Serve
Observability Graceful Shutdown(ctx) with one-shot Observer.OnShutdown(), connection tracking, structured Observer events (OnConnect / OnBind / OnAssociate / OnError / OnShutdown), pluggable Metrics (counters for connections, commands, auth failures, bytes proxied) with MeteredForwarder helper
PROXY Protocol PROXY protocol v1 (human-readable) and v2 (binary) header parsing for both TCP and UDP — extract the original client address when behind a proxy-aware load balancer
Operability zerolog logger, IdleTimeout with per-I/O reset, KeepAlivePeriod, MaxConnections semaphore, MaxConnectionsPerIP, RateLimit token bucket, RoutinePool for concurrency control

Installation

go get github.com/malivvan/socks5

Minimum Go version: 1.23. The only runtime dependency is github.com/rs/zerolog for structured logging; testify is test-only.


Quick Start

package main

import (
    "log"
    "github.com/malivvan/socks5"
)

func main() {
    server, err := socks5.NewServer(nil) // nil uses defaults
    if err != nil {
        log.Fatal(err)
    }
    log.Fatal(server.ListenAndServe("tcp", ":1080"))
}
With Authentication
server, _ := socks5.NewServer(&socks5.ServerConfig{
    AuthMethods: []socks5.Authenticator{
        socks5.UserPassAuthenticator{
            Credentials: socks5.StaticCredentials{"admin": "secret"},
        },
    },
})
server.ListenAndServe("tcp", ":1080")
Production Server Configuration

Configure the server directly via ServerConfig struct fields:

server, _ := socks5.NewServer(&socks5.ServerConfig{
    AuthMethods: []socks5.Authenticator{
        socks5.UserPassAuthenticator{
            Credentials: socks5.StaticCredentials{"admin": "secret"},
        },
    },
    HandshakeTimeout:    10 * time.Second,
    IdleTimeout:         5 * time.Minute,
    MaxConnections:      500,
    MaxConnectionsPerIP: 10,
    KeepAlivePeriod:     30 * time.Second,
    StrictRSV:           true,
    StrictBindOrigin:    true,
    RuleSet:             socks5.PermitDest("10.0.0.0/8", "192.168.0.0/16"),
    Metrics:             myMetrics,
    Observer:            myObserver,
})

ServerConfig

Fields
Field Type / Description
AuthMethods []Authenticator — authentication methods to offer (NoAuth, UserPass, or custom)
NameResolver func(ctx, name) (context.Context, []net.IP, error) — custom DNS resolver; multi-record results drive happy-eyeballs dialing
RuleSet RuleSet — access control rules (PermitAll, PermitDest, PermitSource, PermitPorts, …)
AddressRewriter func(ctx, *Request) (context.Context, *AddrSpec) — transparent destination rewriting
Observer Observer — lifecycle event hooks (OnConnect, OnBind, OnAssociate, OnError, OnShutdown)
Metrics Metrics — pluggable counters (IncConnectionsAccepted, IncCommand, …)
Forwarder func(src, dst net.Conn) error — custom data forwarder (see also LiteForwarder for io.ReadWriter pairs)
RoutinePool RoutinePool — goroutine pool for concurrency control
ConnectHandle / BindHandle / AssociateHandle Handler — custom per-command handlers
ConnectMiddleware / BindMiddleware / AssociateMiddleware MiddlewareChain — per-command middleware chains
IdleTimeout time.Duration — idle connection timeout (resets on every I/O)
HandshakeTimeout time.Duration — auth/request parsing deadline
AssociateSetupTimeout time.Duration — slow-loris defence: max time to wait for first datagram after ASSOCIATE
MaxConnections int — global concurrent connection limit (semaphore acquired before Accept)
MaxConnectionsPerIP int — per-source-IP connection cap
MaxDatagramSize int — max accepted UDP datagram payload
RateLimit RateLimit — per-client UDP token-bucket rate (datagrams/second)
FragmentTimeout / FragmentPoolSize time.Duration / int — UDP fragment reassembly timer and pool size
StrictRSV bool — reject non-zero RSV bytes in TCP requests
LenientUDPRSV bool — allow (log) non-zero RSV bytes in UDP relay datagrams
StrictAssociateBinding bool — pin both client IP and port on late-binding UDP ASSOCIATE
StrictBindOrigin bool — refuse BIND with unspecified DST.ADDR (closes wildcard-peer takeover)
ReadBufferSize / WriteBufferSize int — TCP socket buffer tuning
KeepAlivePeriod time.Duration — TCP keep-alive period on accepted connections
BytesPool BytesPool — buffer pool (default: bucketed sync.Pool with zero-on-Get)
Listeners map[string]*Listener — in-process virtual TCP listeners for routing CONNECT to in-memory services
BindIP / BindPort net.IP / int — BIND/ASSOCIATE listen address
ProxyListen / ProxyListenPacket / ProxyListenBind Custom listener factories for BIND/ASSOCIATE
Context context.Context — base context for the server
Logger *zerolog.Logger — structured logger for errors and operational messages
RuleSet Helpers

Built-in access-control rulesets compose with AND semantics — wrap them in your own closure to combine multiple checks.

Helper Effect
PermitAll / PermitNone Allow / deny everything
PermitCommand(connect, bind, assoc bool) Allow per command
PermitDest(cidrs ...string) Allow only resolved destination IPs inside the given CIDRs
PermitSource(cidrs ...string) Allow only client source IPs inside the given CIDRs
PermitPorts(ports ...int) Allow only the listed destination ports

Bare IPs are treated as /32 (IPv4) or /128 (IPv6). Invalid CIDR strings panic at construction time so misconfiguration fails fast at startup.


Client

The package includes a full SOCKS5 client for dialing through proxies.

import "github.com/malivvan/socks5"

// Simple dial through a proxy.
client := socks5.NewClient(&socks5.ClientConfig{ProxyAddr: "127.0.0.1:1080"})
conn, _ := client.Dial("tcp", "example.com:80")
// conn is a *socks5.SocksConn with BoundAddr() metadata.

// BIND — listen for an incoming connection through the proxy.
bound, _ := client.Bind(context.Background(), "0.0.0.0:0")
// bound is a *socks5.BoundConn with ListenAddr() (phase-1) and PeerAddr() (phase-2).
// Share bound.ListenAddr() with the remote peer; they connect to the proxy.
// bound implements net.Conn for the forwarded connection.
go func() {
    defer bound.Close()
    io.Copy(bound, os.Stdin)
}()

// ASSOCIATE — UDP relay through the proxy.
relay, _ := client.Associate(context.Background(), "0.0.0.0:0")
// relay is a *socks5.UDPRelay with RelayAddr() — the proxy's UDP relay port.
// Send SOCKS5-encapsulated datagrams to relay.RelayAddr() via a local PacketConn.
// Close relay (or its control connection) to terminate the association.
defer relay.Close()

// With username/password authentication.
client = socks5.NewClient(&socks5.ClientConfig{
    ProxyAddr: "127.0.0.1:1080",
    Username:  "admin",
    Password:  "secret",
})
conn, _ = client.Dial("tcp", "example.com:80")
Client Functions
Function Description
NewClient(cfg *ClientConfig) Create a client
Client.Dial(network, addr) CONNECT through the proxy
Client.DialContext(ctx, network, addr) CONNECT with context
Client.Bind(ctx, addr) BIND through the proxy (returns *BoundConn)
Client.Associate(ctx, addr) UDP ASSOCIATE (returns *UDPRelay)
ClientConfig Fields
Field Description
ProxyAddr Proxy host:port
Username / Password RFC 1929 credentials
Dialer Custom *net.Dialer for proxy connections
DialFunc Custom dial function (TLS, SSH tunnel, …)
HandshakeTimeout Handshake deadline
Auth Custom Authenticator

Virtual Networking

The package provides Listener and PacketConn — in-process implementations of net.Listener and net.PacketConn. They use memory pipes (net.Pipe) and buffered channels instead of real network sockets, enabling in-process service routing through the SOCKS5 proxy.

TCP Listener
// Create a virtual TCP listener (no real socket).
vl, _ := socks5.NewListener("127.0.0.1:0")
go http.Serve(vl, mux) // any net.Listener consumer works

server, _ := socks5.NewServer(&socks5.ServerConfig{
    Listeners: map[string]*socks5.Listener{
        vl.Addr().String(): vl,
    },
})

When the proxy receives a CONNECT request to a virtual listener's address, it routes the connection through the listener instead of making an outbound TCP dial. The demo/webserver example demonstrates this pattern — the internal HTTP server runs entirely in-process without a single net.Listen() call.

UDP PacketConn
echoPC, _ := socks5.NewPacketConn("127.0.0.1:0")
// echoPC implements net.PacketConn — use ReadFrom/WriteTo

The demo/packetconn example shows a complete in-process UDP echo service routed through the SOCKS5 ASSOCIATE handler.


Observer & Metrics

Observer

The Observer interface provides lifecycle hooks for observability, tracing, and logging:

type Observer interface {
    OnConnect(ctx context.Context, req *Request, target net.Conn)
    OnBind(ctx context.Context, req *Request, listener net.Listener)
    OnAssociate(ctx context.Context, req *Request, conn net.PacketConn)
    OnError(ctx context.Context, req *Request, err error)
    OnShutdown()
}

Use NoOpObserver as a base when you only need a subset of hooks.

Metrics

The Metrics interface provides hot-path counters:

type Metrics interface {
    IncConnectionsAccepted(ctx context.Context)
    IncConnectionsRejected(ctx context.Context, reason string)
    IncCommand(ctx context.Context, command uint8)
    IncAuthFailure(ctx context.Context, method uint8)
    AddBytesProxied(ctx context.Context, direction string, n int64)
}

Wire per-byte accounting with MeteredForwarder(myMetrics):

server, _ := socks5.NewServer(&socks5.ServerConfig{
    Forwarder: socks5.MeteredForwarder(myMetrics),
})

See docs/METRICS.md for detailed integration guidance.


PROXY Protocol

The package supports PROXY protocol v1 (human-readable) and v2 (binary) headers for both TCP stream and UDP datagram transports. When a proxy-aware load balancer prepends the PROXY header, the server can extract the original client address rather than the load balancer's IP.

// PROXY header parsing happens automatically when the server receives
// a PROXY-protocol prefixed connection. The parsed address is available
// through the PROXY header parsing functions in proxyproto.go.

See proxyproto.go for the full API.


Examples

See the demo/ directory for 13 runnable examples:

Demo Description
basic Minimal no-auth server
userpass Username/password authentication
observer Lifecycle event logging
shutdown Graceful shutdown with signal handling
middleware Logging middleware on CONNECT requests
rewriter Transparent address rewriting
acl Command-level access control (CONNECT only)
customdial Custom outbound dialer with timeouts
resolver Custom DNS resolver (Cloudflare 1.1.1.1)
production Hardened production server with all safety options
packetconn Virtual UDP PacketConn for in-process echo service
webserver In-process HTTP service routed via Listener
chained clientA → proxyB → proxyA → target via ClientConfig.DialFunc

Run any demo with:

go run ./demo/basic

Development

make test          # run tests (60s timeout, no cache)
make test-race     # run tests with race detector (120s timeout)
make bench         # run benchmarks (3s benchtime, 300s timeout)
make bench-compare BASE=v1.0.0 HEAD=main  # compare benchmarks with benchstat
make soak          # -race soak test (~60 s, requires build tag soak)
make cover         # generate coverage report
make cover-html    # generate HTML coverage report
make fmt           # gofmt -s -w .
make vet           # go vet
make lint          # golangci-lint (install with `make install`)
make install       # install dev tools (golint, gotestsum, golangci-lint)
make clean         # remove coverage output + test cache
Benchmark Results

See docs/BENCH.md for detailed benchmark methodology and results, including throughput, latency, and allocation comparisons.

Changelog

See docs/CHANGELOG.md for the complete v1.0.0 release notes covering all audit-cycle changes.


Upgrading to v1.0.0

If you are pinned to a pre-v1 commit, two source-breaking changes during the audit cycle require small migrations:

Forwarder signature

ServerConfig.Forwarder is now func(src, dst net.Conn) error (previously func(src, dst io.ReadWriter) error). The earlier shape internally type-asserted to net.Conn; making it explicit removes a hidden interface requirement.

  • If your forwarder already worked with net.Conn: no change needed — drop the type assertion.
  • If you need an io.ReadWriter forwarder (in-process pipes, custom transports): use the new LiteForwarder(src, dst io.ReadWriter) error helper.
NameResolver return type (C-10)

ServerConfig.NameResolver now returns (context.Context, []net.IP, error) (previously (context.Context, net.IP, error)). The CONNECT handler uses the full slice for happy-eyeballs dialing across multi-record FQDN lookups.

  • Single-address resolvers: wrap your result in a one-element slice: return ctx, []net.IP{ip}, nil.
  • "No addresses found": return nil, nil (no error); the server short-circuits to ReplyHostUnreachable via the new ErrHostUnreachable sentinel.
Type renames (v1.0.0)
  • VirtualListenerListener
  • VirtualPacketConnPacketConn
  • NewVirtualListenerNewListener
  • NewVirtualPacketConnNewPacketConn
  • ServerConfig.VirtualListenersServerConfig.Listeners

See docs/CHANGELOG.md for the complete v1.0.0 changelog.


License

MIT


Additional Resources

Resource Description
docs/CHANGELOG.md Full v1.0.0 changelog with audit-cycle details
docs/BENCH.md Benchmark methodology and results
docs/METRICS.md Metrics design and integration guide
AGENTS.md Contributor guidelines, versioning policy, and architecture decisions
GitHub Issues Bug reports and feature requests

Documentation

Overview

Package socks5 implements a production-grade SOCKS5 server and client per RFC 1928, with RFC 1929 username/password authentication. It supports the CONNECT, BIND, and UDP ASSOCIATE commands and ships with pluggable Authenticator, RuleSet, NameResolver, Forwarder and Observer interfaces plus middleware chains for per-command extensibility.

GSSAPI authentication (RFC 1961, method 0x01) is intentionally out of scope; see AGENTS.md for the package's design principles. (Doc-02)

Index

Constants

View Source
const (
	CommandConnect   = 1
	CommandBind      = 2
	CommandAssociate = 3
)

Command codes as defined in RFC 1928, section 4.

View Source
const (
	AddressIPv4       = 1
	AddressDomainName = 3
	AddressIPv6       = 4
)

Address type constants as defined in RFC 1928, section 5.

View Source
const (
	ReplySucceeded            = 0
	ReplyGeneralFailure       = 1
	ReplyConnectionNotAllowed = 2
	ReplyNetworkUnreachable   = 3
	ReplyHostUnreachable      = 4
	ReplyConnectionRefused    = 5
	ReplyTTLExpired           = 6
	ReplyCommandNotSupported  = 7
	ReplyAddrTypeNotSupported = 8
)

SOCKS reply codes as defined in RFC 1928, section 6.

View Source
const (
	AuthMethodNoAcceptable = 0xFF
	AuthMethodNoAuth       = 0
	AuthMethodUserPass     = 2
)

Authentication method constants.

Variables

View Source
var (
	ErrUnrecognizedAddrType = errors.New("socks5: unrecognized address type")
	ErrNoSupportedAuth      = errors.New("socks5: no supported authentication mechanism")
	ErrUserAuthFailed       = errors.New("socks5: user authentication failed")
	ErrInvalidVersion       = errors.New("socks5: invalid SOCKS version")
	ErrInvalidRequest       = errors.New("socks5: invalid request")
	ErrCommandNotSupported  = errors.New("socks5: command not supported")
	ErrConnectionNotAllowed = errors.New("socks5: connection not allowed")
	ErrInvalidHostname      = errors.New("socks5: invalid hostname")
	ErrHostUnreachable      = errors.New("socks5: host unreachable")
	ErrShuttingDown         = errors.New("socks5: server is shutting down")
	ErrReservedNotZero      = errors.New("socks5: reserved byte is non-zero")
	ErrServerAlreadyServing = errors.New("socks5: Server.Serve already called")
)

SOCKS errors.

View Source
var ErrListenerNotFound = errors.New("socks5: virtual listener not found")

ErrListenerNotFound is returned when a virtual listener is not found for the requested address.

View Source
var ErrProxyProtoMalformed = errors.New("socks5: malformed PROXY protocol header")

ErrProxyProtoMalformed is returned when the PROXY header bytes cannot be parsed.

Functions

func DNSResolver

func DNSResolver(ctx context.Context, name string) (context.Context, []net.IP, error)

DNSResolver is the default NameResolver. It uses the Go runtime's default resolver (which respects /etc/resolv.conf and CGO settings) and is context-aware: deadlines and cancellation on ctx propagate to the underlying lookup. It returns the full list of A and AAAA addresses for name in resolver order; callers (typically the CONNECT handler) iterate through the returned slice to perform happy-eyeballs dialing. (C-10)

func DefaultForwarder

func DefaultForwarder(src, dst net.Conn) error

DefaultForwarder is the default data forwarder. It copies data bidirectionally between src and dst using ProxyStream, which supports TCP half-close.

Prior versions accepted io.ReadWriter and type-asserted to net.Conn internally. The signature now matches reality. For pure io.ReadWriter use cases (in-process pipes, custom transports) call LiteForwarder directly.

func LiteForwarder

func LiteForwarder(src, dst io.ReadWriter) error

LiteForwarder is a simple bidirectional copy without half-close, for io.ReadWriter pairs that do not provide net.Conn (e.g., io.Pipe halves). It returns the first non-nil error from either direction.

func MeteredForwarder

func MeteredForwarder(m Metrics) func(src, dst net.Conn) error

MeteredForwarder returns a Forwarder that wraps DefaultForwarder (ProxyStream-based, with TCP half-close) and reports bytes proxied in each direction to m.AddBytesProxied.

Conventions:

  • "outbound" counts bytes flowing from the client → target (the SOCKS5 client → the proxied destination).
  • "inbound" counts bytes flowing from the target → client (the proxied destination → the SOCKS5 client).

When m is nil, NoOpMetrics is used so the wrapper degrades to the default behaviour at negligible cost. The returned function has the same signature and semantics as DefaultForwarder; in particular it honours the CloseWriter contract documented on ServerConfig.Forwarder (C-11) by forwarding CloseWrite through the shim.

Use it as ServerConfig.Forwarder = MeteredForwarder(myMetrics) to opt into per-byte accounting without re-implementing the half-close logic.

func NewLogger

func NewLogger(l zerolog.Logger) *zerolog.Logger

NewLogger returns a pointer to the given zerolog.Logger.

func NewStdLogger

func NewStdLogger() *zerolog.Logger

NewStdLogger returns a pointer to a zerolog.Logger writing to stderr with a consistent time format. This is the canonical logger factory for demos and external consumers.

func PermitAll

func PermitAll(ctx context.Context, req *Request) (context.Context, bool)

PermitAll allows all commands.

func PermitCommand

func PermitCommand(connect, bind, assoc bool) func(ctx context.Context, req *Request) (context.Context, bool)

PermitCommand returns a rule function that allows or denies specific SOCKS5 commands based on the given boolean flags.

func PermitDest

func PermitDest(cidrs ...string) func(ctx context.Context, req *Request) (context.Context, bool)

PermitDest returns a ruleset that allows the request only when the resolved destination IP (req.realDestAddr.IP) belongs to one of the given CIDR ranges. Bare IPs are treated as /32 or /128. CIDRs that fail to parse cause a panic at construction time so misconfiguration is caught at startup. Requests whose realDestAddr has no IP (e.g. an FQDN destination whose NameResolver returned no addresses) are denied. (C-13)

func PermitNone

func PermitNone(ctx context.Context, req *Request) (context.Context, bool)

PermitNone denies all commands.

func PermitPorts

func PermitPorts(ports ...int) func(ctx context.Context, req *Request) (context.Context, bool)

PermitPorts returns a ruleset that allows the request only when the destination port (req.DestAddr.Port) appears in the given list. An empty list denies all requests. (C-13)

func PermitSource

func PermitSource(cidrs ...string) func(ctx context.Context, req *Request) (context.Context, bool)

PermitSource returns a ruleset that allows the request only when the client source IP (req.RemoteAddr) belongs to one of the given CIDR ranges. Non-IP remote addresses (e.g. in-process pipes) are denied. CIDR parse errors panic at construction. (C-13)

func ProxyStream

func ProxyStream(a, b net.Conn) error

ProxyStream bidirectionally copies data between a and b using ring buffers to decouple reads and writes. It supports TCP half-close: when one direction reaches EOF, it sends a FIN to signal the peer while keeping the other direction open.

B1 fixes applied:

  • Cross-cancellation: when one direction errors, both ring buffers are closed immediately to prevent deadlocks.
  • Read wraparound: explicit min(avail, end-of-slice) bounds.
  • Pool: 32 KiB read buffers, 64 KiB ring buffers (Perf-04 also pools the ring buffer slabs).
  • Error propagation: write errors close both directions.

func ReadMethods

func ReadMethods(r io.Reader) ([]byte, error)

ReadMethods reads the list of authentication methods from the client's initial handshake. It reads 1 byte of count n, then n bytes of method IDs.

Optimized: uses a stack-allocated [8]byte array for the common case (most clients send 1-3 methods), falling back to heap allocation only for counts > 8.

Clean-03: the [8]byte stack path is justified by the BenchmarkReadMethods_Small / _Heap pair in bench_test.go — at NMETHODS<=8 (covering 99%+ of real clients) the stack path avoids a small-slice heap allocation per connection handshake. Profile before changing this path.

func SendReply

func SendReply(w io.Writer, rep uint8, bindAddr net.Addr) error

SendReply sends a SOCKS5 reply to w with the given reply code and bind address. The bindAddr is converted from net.Addr to *AddrSpec internally. If bindAddr is nil, a zero address (0.0.0.0:0) is used.

Types

type AddrSpec

type AddrSpec struct {
	FQDN string // fully-qualified domain name (used when ATYP is DOMAINNAME)
	IP   net.IP // IP address (used when ATYP is IPv4 or IPv6)
	Port int    // port number
}

AddrSpec represents a SOCKS address as described in RFC 1928, section 5. It can hold an IPv4, IPv6, or domain name address along with a port.

func ParseAddrSpec

func ParseAddrSpec(addr string) (*AddrSpec, error)

ParseAddrSpec parses a "host:port" string into an *AddrSpec. The host may be an IPv4 address, an IPv6 address (in brackets), or a domain name. This is the inverse of AddrSpec.Address().

func (*AddrSpec) Address

func (a *AddrSpec) Address() string

Address returns the string form suitable for use with net.Dial. (Byte-for-byte identical to String, kept as a separate name only for readability at call sites.)

func (*AddrSpec) String

func (a *AddrSpec) String() string

String returns the human-readable form of the address.

Optimized: uses net.JoinHostPort + strconv.Itoa instead of fmt.Sprintf to avoid the overhead of the format parser on the hot path.

type AuthContext

type AuthContext struct {
	// Method is the authentication method that was used.
	Method uint8
	// Identity is the verified principal — for RFC 1929 username/password
	// this is the authenticated username. It MUST NOT contain credentials.
	// Empty when no identity is associated with the method (e.g. NoAuth).
	// (Sec-06)
	Identity string
	// Payload carries method-specific data. Implementations MUST NEVER
	// place passwords or other secrets here; loggers may write this map
	// at debug level. Prefer Identity for the verified principal name.
	// For backward compatibility, UserPassAuthenticator continues to set
	// Payload["username"] alongside Identity. (Sec-06)
	Payload map[string]string
}

AuthContext holds the result of the authentication handshake.

type Authenticator

type Authenticator interface {
	// Authenticate performs the method-specific sub-negotiation. It is
	// invoked *after* Server.authenticate has written the
	// method-selection reply (R-03).
	Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error)
	// GetCode returns the method identifier byte for this authenticator.
	GetCode() uint8
}

Authenticator handles SOCKS5 per-method authentication sub-negotiation (e.g., RFC 1929 username/password). Method negotiation (RFC 1928 §3) is performed by Server.authenticate, which writes the {VER, METHOD} method-selection reply itself; Authenticator implementations must not write that reply. (R-03)

On error, the caller closes the underlying connection. Implementations should not attempt to drain or recover the remaining sub-negotiation bytes; the connection-close is the synchronisation point. (R-05)

type BoundConn

type BoundConn struct {
	net.Conn
	// contains filtered or unexported fields
}

BoundConn is the connection returned by Client.Bind. It wraps the underlying TCP connection to the proxy with the addresses from both BIND reply phases.

func (*BoundConn) ListenAddr

func (b *BoundConn) ListenAddr() *AddrSpec

ListenAddr returns the proxy's listen address (phase-1 reply).

func (*BoundConn) PeerAddr

func (b *BoundConn) PeerAddr() *AddrSpec

PeerAddr returns the peer address (phase-2 reply).

type BytesPool

type BytesPool interface {
	// Get returns a byte slice of at least n bytes.
	Get(n int) []byte
	// Put returns a byte slice to the pool.
	Put(buf []byte)
}

BytesPool is a pool of byte slices used for UDP relay and other buffered operations. Implementations should reuse byte slices to reduce allocations.

type Client

type Client struct {
	*ClientConfig
}

Client dials network addresses through a SOCKS5 proxy. It embeds *ClientConfig anonymously so every config field is accessible as a Client field via Go's field promotion (e.g. c.ProxyAddr, c.Username).

Prior versions duplicated every ClientConfig field on Client and copied them in NewClient — the duplication has been removed and Client now holds a *ClientConfig directly.

func NewClient

func NewClient(cfg *ClientConfig) *Client

NewClient returns a Client configured with the given ClientConfig. If cfg is nil, a default configuration is used (no auth, no timeout, default dialer).

func (*Client) Associate

func (c *Client) Associate(ctx context.Context, targetAddr string) (*UDPRelay, error)

Associate performs a SOCKS5 UDP ASSOCIATE operation through the proxy.

The target address indicates the address and port the client expects to use to send UDP datagrams. Use "0.0.0.0:0" if unknown.

The returned *UDPRelay provides RelayAddr() — the proxy's UDP relay address where SOCKS5-encapsulated datagrams should be sent. The caller is responsible for creating a local net.PacketConn and exchanging datagrams with the relay. Closing the UDPRelay (or its control connection) terminates the association.

func (*Client) Bind

func (c *Client) Bind(ctx context.Context, targetAddr string) (*BoundConn, error)

Bind performs a SOCKS5 BIND operation through the proxy.

RFC 1928 §6 specifies a two-phase reply:

  • Phase 1: the proxy creates a listening socket and replies with the listen address (BND.ADDR/BND.PORT).
  • Phase 2: when an incoming connection arrives, the proxy sends a second reply with the peer's address.

The target address should indicate the expected peer for the incoming connection. The returned *BoundConn provides ListenAddr() (phase 1) and PeerAddr() (phase 2), and wraps the forwarded connection.

func (*Client) Dial

func (c *Client) Dial(network, targetAddr string) (net.Conn, error)

Dial connects to targetAddr through the SOCKS5 proxy.

func (*Client) DialContext

func (c *Client) DialContext(ctx context.Context, network, targetAddr string) (net.Conn, error)

DialContext connects to targetAddr through the SOCKS5 proxy, using ctx for the initial dial and handshake. The returned net.Conn is a *SocksConn which provides access to the bound address via BoundAddr().

type ClientConfig

type ClientConfig struct {
	// ProxyAddr is the "host:port" of the SOCKS5 proxy.
	ProxyAddr string

	// Username and Password for RFC 1929 authentication. When Username
	// is non-empty the client offers AuthMethodUserPass.
	Username string
	Password string

	// Auth is an optional custom authenticator. For username/password
	// auth prefer the Username/Password fields instead. Auth is used
	// as a fallback for custom authentication methods only.
	Auth Authenticator

	// Dialer is the dialer used to connect to the proxy. If nil and
	// DialFunc is also nil, a default net.Dialer is used.
	Dialer *net.Dialer

	// DialFunc is a custom dial function for connecting to the proxy.
	// When set it takes precedence over Dialer. This allows custom
	// transport layers (TLS, Unix sockets, SSH tunnels, etc.).
	DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)

	// HandshakeTimeout is the maximum time for the SOCKS5 handshake
	// (method negotiation + auth + request/reply). Zero means no
	// separate handshake timeout; Dialer.Timeout or context deadlines
	// still apply to the TCP connect and handshake respectively.
	HandshakeTimeout time.Duration

	// ResolveOnProxy enables SOCKS5h hostname-forwarding mode.
	// When true, the client always sends the target hostname as an FQDN
	// in the SOCKS5 request, leaving DNS resolution to the proxy. When
	// false (default), the client resolves IP literals locally but still
	// sends hostnames as FQDNs.
	ResolveOnProxy bool
}

ClientConfig holds the configuration for a SOCKS5 client.

type CloseReader

type CloseReader interface {
	CloseRead() error
}

CloseReader is implemented by connections that support half-close (shutting down the read side).

type CloseWriter

type CloseWriter interface {
	CloseWrite() error
}

CloseWriter is implemented by connections that support half-close (TCP FIN on the write side).

type CredentialStore

type CredentialStore interface {
	// Valid returns true if the given username/password pair is valid.
	Valid(user, password string) bool
}

CredentialStore is used to authenticate SOCKS5 username/password pairs as defined in RFC 1929.

type Datagram

type Datagram struct {
	// Frag is the SOCKS5 UDP fragment field (RFC 1928 §7). Bit 0x80
	// marks the end-of-sequence fragment; the low 7 bits encode the
	// position (1..127). FRAG=0x00 means a standalone (non-fragmented)
	// datagram. (Clean-06)
	Frag    uint8
	DstAddr *AddrSpec
	Data    []byte
}

Datagram represents a SOCKS5 UDP relay datagram (RFC 1928 §7).

+----+------+------+----------+----------+----------+
|RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
+----+------+------+----------+----------+----------+
| 2  |  1   |  1   | Variable |    2     | Variable |
+----+------+------+----------+----------+----------+

func NewDatagram

func NewDatagram(frag uint8, dst *AddrSpec, data []byte) *Datagram

NewDatagram creates a new Datagram with the given parameters.

func ParseDatagram

func ParseDatagram(payload []byte) (*Datagram, error)

ParseDatagram parses raw UDP payload into a Datagram with strict RFC 1928 §7 RSV validation (non-zero RSV bytes are rejected).

The returned Data is a copy of the payload's data portion and is safe to retain after the input buffer is reused.

For lenient parsing (e.g., to accept legacy clients with non-zero RSV bytes), see ServerConfig.LenientUDPRSV. (R-09)

func (*Datagram) AppendTo

func (d *Datagram) AppendTo(buf []byte) []byte

AppendTo appends the wire-format datagram (RSV+FRAG+addr+data) to buf and returns the extended slice. It allocates only when buf has insufficient capacity, which makes it suitable for hot-path reuse with a pre-grown scratch slice. (Perf-10)

func (*Datagram) Bytes

func (d *Datagram) Bytes() []byte

Bytes serializes the Datagram into wire format.

Optimized: pre-computes the exact size and writes directly into a single allocation, avoiding bytes.Buffer. Implemented as a thin wrapper around AppendTo. (Perf-10)

func (*Datagram) Header

func (d *Datagram) Header() []byte

Header returns only the header portion (without data) in wire format.

Optimized: same approach as Bytes but without the data copy.

type Handler

type Handler func(ctx context.Context, writer io.Writer, request *Request) error

Handler is a function that handles a SOCKS5 request. It receives the context, an io.Writer (the client connection), and the request.

type Listener

type Listener struct {
	// contains filtered or unexported fields
}

Listener implements net.Listener using in-memory pipe connections. It allows an HTTP server (or any net.Listener consumer) to accept connections without a real TCP socket. When paired with a SOCKS5 proxy via ServerConfig.Listeners, CONNECT requests to the listener's address are routed through the virtual listener instead of making an outbound TCP dial.

Example:

vl := socks5.NewListener("127.0.0.1:0")
go http.Serve(vl, mux)

server, _ := socks5.NewServer(&socks5.ServerConfig{
    Listeners: map[string]*socks5.Listener{
        vl.Addr().String(): vl,
    },
})

func NewListener

func NewListener(addr string) (*Listener, error)

NewListener creates a Listener on the given address. The addr string must be "host:port". If port is "0", a random high port (>= 49152) is assigned so the address is unique per listener.

func (*Listener) Accept

func (vl *Listener) Accept() (net.Conn, error)

Accept waits for and returns the next connection to the listener. The returned connection is one end of a net.Pipe; the other end is provided to the caller that dialed into this listener.

func (*Listener) Addr

func (vl *Listener) Addr() net.Addr

Addr returns the listener's network address.

func (*Listener) Close

func (vl *Listener) Close() error

Close closes the listener. Any blocked Accept operations will be unblocked and return net.ErrClosed.

func (*Listener) Dial

func (vl *Listener) Dial() (net.Conn, error)

Dial creates a new in-memory connection to this virtual listener. It returns one end of a net.Pipe; the other end is queued for the next Accept call. If the listener has been closed, net.ErrClosed is returned.

type MethodReply

type MethodReply struct {
	Version uint8
	Method  uint8
}

MethodReply represents the server's method selection response.

+----+--------+
|VER | METHOD |
+----+--------+
| 1  |   1    |
+----+--------+

func NewMethodReply

func NewMethodReply(ver, method uint8) *MethodReply

NewMethodReply creates a MethodReply.

func ParseMethodReply

func ParseMethodReply(r io.Reader) (*MethodReply, error)

ParseMethodReply reads a method reply from r.

func (*MethodReply) Bytes

func (m *MethodReply) Bytes() []byte

Bytes serializes the MethodReply to wire format.

type MethodRequest

type MethodRequest struct {
	Version uint8
	Methods []byte
}

MethodRequest represents the client's initial method selection message.

+----+----------+----------+
|VER | NMETHODS | METHODS  |
+----+----------+----------+
| 1  |    1     | 1 to 255 |
+----+----------+----------+

func NewMethodRequest

func NewMethodRequest(ver uint8, methods []byte) *MethodRequest

NewMethodRequest creates a MethodRequest.

func ParseMethodRequest

func ParseMethodRequest(r io.Reader) (*MethodRequest, error)

ParseMethodRequest reads a method request from r.

func (*MethodRequest) Bytes

func (m *MethodRequest) Bytes() []byte

Bytes serializes the MethodRequest to wire format.

type Metrics

type Metrics interface {
	// IncConnectionsAccepted is called for every accepted TCP connection,
	// before any per-IP limit or semaphore check.
	IncConnectionsAccepted(ctx context.Context)

	// IncConnectionsRejected is called when a connection is rejected by a
	// per-IP limit, an auth failure, or a ruleset denial. The reason
	// string is a stable short identifier such as "per_ip_limit",
	// "auth_failed", or "rule_denied".
	IncConnectionsRejected(ctx context.Context, reason string)

	// IncCommand is called once per accepted CONNECT/BIND/ASSOCIATE,
	// after the request has passed rule checks. The command argument
	// is one of CommandConnect, CommandBind, CommandAssociate.
	IncCommand(ctx context.Context, command uint8)

	// IncAuthFailure is called every time an Authenticator returns an
	// error. The method byte identifies the authenticator (e.g.,
	// AuthMethodUserPass).
	IncAuthFailure(ctx context.Context, method uint8)

	// AddBytesProxied may be called by custom Forwarders to report the
	// number of bytes transferred in a given direction ("inbound" or
	// "outbound"). The built-in DefaultForwarder does not currently
	// emit this event; it is exposed for users who wrap their own
	// Forwarder and want per-byte accounting.
	AddBytesProxied(ctx context.Context, direction string, n int64)
}

Metrics is a counter/gauge interface that the SOCKS5 server uses to emit operational metrics. It is intentionally narrow: each method is a hot-path callback and should not block. Implementations must be safe for concurrent use. (C-07)

Pass a Metrics into ServerConfig.Metrics to wire it into the server. When nil, NoOpMetrics is used by default.

type Middleware

type Middleware func(next Handler) Handler

Middleware wraps a Handler to add cross-cutting behavior such as logging, rate limiting, or metrics collection.

type MiddlewareChain

type MiddlewareChain []Middleware

MiddlewareChain is an ordered list of middleware that can be applied to a handler. Middleware is executed in order: the first middleware in the chain is the outermost wrapper.

func (MiddlewareChain) Then

func (c MiddlewareChain) Then(h Handler) Handler

Then applies the middleware chain to the given handler, returning a new handler with all middleware applied.

type NoAuthAuthenticator

type NoAuthAuthenticator struct{}

NoAuthAuthenticator implements the NO AUTHENTICATION REQUIRED method (method 0x00).

func (NoAuthAuthenticator) Authenticate

func (a NoAuthAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error)

Authenticate is a no-op for NoAuthAuthenticator: the method-selection reply is written by Server.authenticate (R-03), and NoAuth has no sub-negotiation. Returns an empty AuthContext.

func (NoAuthAuthenticator) GetCode

func (a NoAuthAuthenticator) GetCode() uint8

GetCode returns 0x00.

type NoOpMetrics

type NoOpMetrics struct{}

NoOpMetrics is a Metrics implementation that drops every event. It is the default value of ServerConfig.Metrics when none is configured.

func (NoOpMetrics) AddBytesProxied

func (NoOpMetrics) AddBytesProxied(_ context.Context, _ string, _ int64)

AddBytesProxied implements Metrics.

func (NoOpMetrics) IncAuthFailure

func (NoOpMetrics) IncAuthFailure(_ context.Context, _ uint8)

IncAuthFailure implements Metrics.

func (NoOpMetrics) IncCommand

func (NoOpMetrics) IncCommand(_ context.Context, _ uint8)

IncCommand implements Metrics.

func (NoOpMetrics) IncConnectionsAccepted

func (NoOpMetrics) IncConnectionsAccepted(_ context.Context)

IncConnectionsAccepted implements Metrics.

func (NoOpMetrics) IncConnectionsRejected

func (NoOpMetrics) IncConnectionsRejected(_ context.Context, _ string)

IncConnectionsRejected implements Metrics.

type NoOpObserver

type NoOpObserver struct{}

NoOpObserver is an Observer that does nothing. Use it as a base when you only need to implement a subset of the hooks.

func (NoOpObserver) OnAssociate

func (NoOpObserver) OnAssociate(_ context.Context, _ *Request, _ net.PacketConn)

OnAssociate implements Observer.

func (NoOpObserver) OnBind

func (NoOpObserver) OnBind(_ context.Context, _ *Request, _ net.Listener)

OnBind implements Observer.

func (NoOpObserver) OnConnect

func (NoOpObserver) OnConnect(_ context.Context, _ *Request, _ net.Conn)

OnConnect implements Observer.

func (NoOpObserver) OnError

func (NoOpObserver) OnError(_ context.Context, _ *Request, _ error)

OnError implements Observer.

func (NoOpObserver) OnShutdown

func (NoOpObserver) OnShutdown()

OnShutdown implements Observer.

type Observer

type Observer interface {
	// OnConnect is called after a CONNECT request is accepted and the
	// target connection is established.
	OnConnect(ctx context.Context, req *Request, target net.Conn)

	// OnBind is called after a BIND listener is successfully created.
	OnBind(ctx context.Context, req *Request, listener net.Listener)

	// OnAssociate is called after a UDP ASSOCIATE is set up.
	OnAssociate(ctx context.Context, req *Request, conn net.PacketConn)

	// OnError is called when an error occurs during request handling.
	OnError(ctx context.Context, req *Request, err error)

	// OnShutdown is called when the server has finished shutting down.
	OnShutdown()
}

Observer provides lifecycle hooks into the SOCKS5 server. All methods are called synchronously from the server's goroutines, so implementations should not block.

Each method receives the context which may carry tracing or deadline information. Implementations can use these hooks for metrics, logging, or distributed tracing (e.g., OpenTelemetry span creation).

type PacketConn

type PacketConn struct {
	// contains filtered or unexported fields
}

PacketConn implements net.PacketConn using in-memory buffered channels. It can be used in conjunction with Listener to create a fully in-process network stack for the SOCKS5 proxy.

func NewPacketConn

func NewPacketConn(addr string) (*PacketConn, error)

NewPacketConn creates a PacketConn bound to the given address. The addr string must be "host:port". If port is "0", a random high port is assigned.

func (*PacketConn) Close

func (vpc *PacketConn) Close() error

Close closes the packet connection.

func (*PacketConn) LocalAddr

func (vpc *PacketConn) LocalAddr() net.Addr

LocalAddr returns the local network address.

func (*PacketConn) ReadFrom

func (vpc *PacketConn) ReadFrom(p []byte) (int, net.Addr, error)

ReadFrom reads a packet from the connection. It blocks until a packet is written via WriteTo.

func (*PacketConn) SetDeadline

func (vpc *PacketConn) SetDeadline(t time.Time) error

SetDeadline is a no-op for virtual packet connections.

func (*PacketConn) SetReadDeadline

func (vpc *PacketConn) SetReadDeadline(t time.Time) error

SetReadDeadline is a no-op for virtual packet connections.

func (*PacketConn) SetWriteDeadline

func (vpc *PacketConn) SetWriteDeadline(t time.Time) error

SetWriteDeadline is a no-op for virtual packet connections.

func (*PacketConn) WriteTo

func (vpc *PacketConn) WriteTo(p []byte, addr net.Addr) (int, error)

WriteTo writes a packet to the connection. The packet is buffered and will be returned by the next ReadFrom call on the peer. If the write buffer is full (backpressure), the oldest packet is dropped.

type ProtoReply

type ProtoReply struct {
	Version  uint8
	Reply    uint8
	Reserved uint8
	BndAddr  *AddrSpec
}

ProtoReply represents a SOCKS5 protocol-level reply message.

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+

func NewProtoReply

func NewProtoReply(reply uint8, bnd *AddrSpec) *ProtoReply

NewProtoReply creates a ProtoReply.

func ParseProtoReply

func ParseProtoReply(r io.Reader) (*ProtoReply, error)

ParseProtoReply reads a SOCKS5 reply from r.

func (*ProtoReply) Bytes

func (p *ProtoReply) Bytes() []byte

Bytes serializes the ProtoReply to wire format. (Clean-09: single-allocation builder using appendAddrSpec; no bytes.Buffer.)

type ProtoRequest

type ProtoRequest struct {
	Version  uint8
	Command  uint8
	Reserved uint8
	DstAddr  *AddrSpec
}

ProtoRequest represents a SOCKS5 protocol-level request message. Named ProtoRequest to avoid collision with the existing Request struct (which also carries auth context and connection state).

+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+

func NewProtoRequest

func NewProtoRequest(cmd uint8, dst *AddrSpec) *ProtoRequest

NewProtoRequest creates a ProtoRequest.

func ParseProtoRequest

func ParseProtoRequest(r io.Reader) (*ProtoRequest, error)

ParseProtoRequest reads a SOCKS5 request from r.

func (*ProtoRequest) Bytes

func (p *ProtoRequest) Bytes() []byte

Bytes serializes the ProtoRequest to wire format. (Clean-09: single-allocation builder using appendAddrSpec, matching the addr.go pattern; no bytes.Buffer.)

Returns nil when DstAddr.FQDN exceeds 255 bytes (RFC 1928 §4). Callers should use Validate() before calling Bytes() to catch this condition proactively.

func (*ProtoRequest) Validate

func (p *ProtoRequest) Validate() error

Validate checks that the ProtoRequest is well-formed per RFC 1928. Currently checks that the FQDN (if set) is ≤ 255 bytes.

type ReplyError

type ReplyError struct {
	Code    uint8
	Message string
}

ReplyError represents a SOCKS5 reply error with the reply code.

func (*ReplyError) Error

func (e *ReplyError) Error() string

type Request

type Request struct {
	Version     uint8
	Command     uint8
	Reserved    uint8
	AuthContext *AuthContext
	RemoteAddr  net.Addr
	DestAddr    *AddrSpec
	// contains filtered or unexported fields
}

Request represents a parsed SOCKS5 request from the client.

func NewRequest

func NewRequest(bufConn io.Reader) (*Request, error)

NewRequest reads and parses a SOCKS5 request from bufConn. It reads the VER + CMD + RSV header and the destination address. Command validation is NOT performed here — it is deferred to handleRequest so that proper SOCKS5 error replies can be sent.

func (*Request) String

func (r *Request) String() string

String returns a human-readable representation of the request.

type RoutinePool

type RoutinePool interface {
	// Submit submits a function for execution. It may block if the
	// pool is at capacity. Returns an error if the task cannot be
	// submitted (e.g., pool is shut down).
	Submit(f func()) error
}

RoutinePool is a goroutine pool interface. Implementations can limit the number of concurrent goroutines used for connection handling.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server implements a SOCKS5 server.

func NewServer

func NewServer(cfg *ServerConfig) (*Server, error)

NewServer creates a new SOCKS5 server with the given configuration. If cfg is nil, defaults are used. applyDefaults fills in any zero-valued fields.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(network, addr string) error

ListenAndServe creates a listener and starts serving SOCKS5 requests. It blocks until the server is shut down or an error occurs.

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

Serve starts serving SOCKS5 requests on the given listener. It blocks until the listener is closed or the server is shut down.

func (*Server) ServeConn

func (s *Server) ServeConn(conn net.Conn) error

ServeConn performs a full SOCKS5 handshake on an established connection.

RFC 1928 compliance notes:

  • [E4] Unknown commands reach handleRequest which sends ReplyCommandNotSupported.
  • [E5] Unknown ATYP in the request causes ReplyAddrTypeNotSupported to be sent.
  • [E7] Non-zero RSV byte is handled per StrictRSV config: rejected or logged.

func (*Server) ServeConnNoAuth

func (s *Server) ServeConnNoAuth(conn net.Conn) error

ServeConnNoAuth skips the version check and authentication, reading the SOCKS request directly from the connection. This is useful when the connection has already been authenticated (e.g., by TLS client certs).

RFC 1928 compliance notes: same as ServeConn (E5, E7).

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the server.

Steps:

  1. Cancel the server context so accept loops and worker goroutines observe the shutdown signal (cancel() is the single source of truth for "shutting down").
  2. Close the listener exactly once (sync.Once) so Accept() returns.
  3. Wait for active handlers to drain or for ctx to expire and then force-close any remaining tracked connections.
  4. Invoke Observer.OnShutdown() exactly once after the wait completes, regardless of how Shutdown returned (R-02).

type ServerConfig

type ServerConfig struct {
	// AuthMethods is the list of supported authentication methods, tried
	// in order. If nil, NoAuthAuthenticator is used.
	AuthMethods []Authenticator

	// NameResolver resolves domain names to IP addresses. If nil,
	// DNSResolver is used. The returned slice should be in resolver
	// order; the CONNECT handler tries each address in turn
	// (happy-eyeballs) until one succeeds. BIND and ASSOCIATE use only
	// the first address. (C-10)
	NameResolver func(ctx context.Context, name string) (context.Context, []net.IP, error)

	// RuleSet determines whether to allow or deny requests. If nil,
	// PermitAll is used.
	RuleSet func(ctx context.Context, req *Request) (context.Context, bool)

	// AddressRewriter optionally rewrites the destination address before
	// connecting. If nil, no rewriting is performed.
	AddressRewriter func(ctx context.Context, request *Request) (context.Context, *AddrSpec)

	// Logger is used to log errors and informational messages. If nil,
	// a default zerolog.Logger writing to stderr is used.
	Logger *zerolog.Logger

	// Dial is the function used to establish outbound TCP connections for
	// CONNECT requests. If nil, net.Dialer is used.
	Dial func(ctx context.Context, network, addr string) (net.Conn, error)

	// BindIP is the IP address to bind to for BIND and ASSOCIATE replies.
	// If nil, 0.0.0.0 is used.
	BindIP net.IP

	// BindPort is the port to bind to for BIND and ASSOCIATE replies.
	// If zero, a random port is chosen.
	BindPort int

	// ProxyListen creates a TCP listener with custom control (e.g., for
	// transparent proxying or reuseport). If nil, net.Listen is used.
	ProxyListen func(ctx context.Context, network, addr string) (net.Listener, error)

	// ProxyListenPacket creates a UDP packet connection. If nil,
	// net.ListenPacket is used.
	ProxyListenPacket func(ctx context.Context, network, addr string) (net.PacketConn, error)

	// ProxyOutgoingListenPacket creates a UDP packet connection for
	// outgoing relay traffic. If nil, ProxyListenPacket (or net.ListenPacket)
	// is used.
	ProxyOutgoingListenPacket func(ctx context.Context, network, addr string) (net.PacketConn, error)

	// PacketForwardAddress determines the source address for forwarded
	// UDP packets. If nil, the default implementation returns the local
	// address of the packet connection.
	PacketForwardAddress func(ctx context.Context, destinationAddr string, packet net.PacketConn, conn net.Conn) (net.IP, int, error)

	// ProxyListenBind creates a TCP listener specifically for BIND
	// requests. If nil, ProxyListen (or net.Listen) is used.
	ProxyListenBind func(ctx context.Context, network, addr string) (net.Listener, error)

	// ListenBindReuseTimeout is the duration for which a BIND listener
	// port can be reused after the previous binding is released.
	ListenBindReuseTimeout time.Duration

	// ListenBindAcceptTimeout is the maximum time to wait for an incoming
	// connection on a BIND listener.
	ListenBindAcceptTimeout time.Duration

	// Forwarder copies data between client and target connections. If
	// nil, DefaultForwarder is used.
	//
	// The signature was changed from func(src, dst io.ReadWriter)
	// error to func(src, dst net.Conn) error. The previous signature
	// type-asserted to net.Conn internally; making net.Conn explicit
	// removes a hidden interface requirement. For pure io.ReadWriter
	// use cases (e.g., in-process pipes) call LiteForwarder directly.
	//
	// C-11: custom Forwarders MUST perform a TCP half-close (CloseWrite)
	// on EOF in each direction so that BIND-style two-way streams finish
	// cleanly; otherwise long-lived peers may stall waiting for FIN. The
	// built-in DefaultForwarder (backed by ProxyStream) already honours
	// this contract via the CloseWriter interface.
	//
	// MeteredForwarder(metrics) is the canonical wrapper for
	// reporting per-byte traffic to a Metrics implementation without
	// re-implementing the half-close logic.
	Forwarder func(src, dst net.Conn) error

	// Observer receives lifecycle events. If nil, NoOpObserver is used.
	Observer Observer

	// Metrics receives operational counter events such as accepted
	// connections, rejected connections, command counts and auth
	// failures. If nil, NoOpMetrics is used. (C-07)
	Metrics Metrics

	// IdleTimeout is the maximum time a connection can remain idle before
	// being closed. Zero means no idle timeout.
	IdleTimeout time.Duration

	// BytesPool is used to allocate byte slices. If nil, a default pool
	// is used.
	BytesPool BytesPool

	// Context is the base context for the server. If nil, context.Background
	// is used.
	Context context.Context

	// FragmentTimeout is the maximum time to wait for a complete sequence
	// of UDP fragments before discarding. Default is 5 seconds.
	// (RFC 1928 §7 requires a reassembly timer.)
	FragmentTimeout time.Duration

	// FragmentPoolSize is the maximum byte size for which the fragment
	// reassembler uses a sync.Pool to reuse allocations. Fragments larger
	// than this are heap-allocated. Default 1500 (typical MTU). (P3-2)
	FragmentPoolSize int

	// MaxDatagramSize limits the maximum acceptable UDP datagram payload
	// size. Datagrams exceeding this limit are silently dropped.
	// Zero means no limit (up to 65535 bytes).
	MaxDatagramSize int

	// RateLimit limits per-client UDP datagram throughput (tokens per
	// second). Zero means no rate limiting.
	RateLimit float64

	// StrictRSV controls whether a non-zero RSV byte in the SOCKS5
	// request header causes the connection to be rejected. When false
	// (default), a non-zero RSV is logged but the request proceeds.
	// When true, the server replies with ReplyGeneralFailure.
	StrictRSV bool
	// LenientUDPRSV, when true, allows non-zero RSV bytes in incoming
	// UDP relay datagrams (logged but accepted). Default false (strict
	// rejection per RFC 1928 §7). This is the UDP counterpart of the
	// TCP-side StrictRSV escape hatch. (R-09)
	//
	// Note: the field is inverted vs. StrictRSV so that the zero value
	// preserves the current strict / RFC-compliant behaviour.
	LenientUDPRSV bool
	// StrictAssociateBinding, when true, pins both IP *and* port from
	// the first datagram on late-binding UDP ASSOCIATE sessions
	// (DST.ADDR=0.0.0.0:0). When false (default), only the IP is
	// pinned, matching prior behaviour. (R-04)
	StrictAssociateBinding bool

	// StrictBindOrigin, when true, refuses BIND requests that leave
	// the expected peer address unspecified (DST.ADDR=0.0.0.0:0).
	// RFC 1928 §6 lets the client leave the peer unspecified and
	// resolve it on the first accepted connection, but that lets
	// any peer take over the bind slot. With StrictBindOrigin=true,
	// the server replies with ReplyConnectionNotAllowed instead of
	// opening a wildcard listener, forcing the client to pre-commit
	// to a specific peer. (Sec-05)
	StrictBindOrigin bool

	// RoutinePool is an optional goroutine pool for limiting concurrency.
	// If nil, a new goroutine is spawned per connection.
	RoutinePool RoutinePool

	// ConnectHandle, BindHandle, AssociateHandle are optional custom
	// handlers that replace the default implementation for each command.
	ConnectHandle   Handler
	BindHandle      Handler
	AssociateHandle Handler

	// Listeners maps listener addresses to in-process virtual
	// listeners. When a CONNECT request targets a virtual listener,
	// the server routes the connection through the listener instead
	// of making an outbound TCP dial. The map key must match the
	// address returned by the listener's Addr().String().
	Listeners map[string]*Listener

	// ConnectMiddleware, BindMiddleware, AssociateMiddleware are optional
	// middleware chains applied to each command handler.
	ConnectMiddleware   MiddlewareChain
	BindMiddleware      MiddlewareChain
	AssociateMiddleware MiddlewareChain

	// MaxConnections limits concurrent connections. Zero means no limit.
	// When the limit is reached, new connections block until a slot is
	// available. (D2)
	MaxConnections int

	// MaxConnectionsPerIP limits the number of concurrent connections from
	// a single source IP. Zero means no per-IP limit (only the global
	// MaxConnections, if set, applies). When the limit is reached, new
	// connections from that IP are accepted at the TCP layer but
	// immediately closed by the server. (C-04)
	MaxConnectionsPerIP int

	// HandshakeTimeout is the maximum time for the SOCKS5 handshake
	// (version check + auth + request). If zero, no handshake timeout
	// is applied (IdleTimeout still covers the whole connection). (D6)
	HandshakeTimeout time.Duration

	// AssociateSetupTimeout is the maximum time the server waits for the
	// first datagram from the expected UDP client after a successful
	// ASSOCIATE reply. If no datagram arrives in time, the association is
	// torn down. Zero disables the timeout. (Sec-02 — slow-loris defence
	// on UDP ASSOCIATE setup.)
	AssociateSetupTimeout time.Duration

	// ReadBufferSize sets the kernel read buffer (SO_RCVBUF) on accepted
	// connections. Zero means use OS default. (D7)
	ReadBufferSize int

	// WriteBufferSize sets the kernel write buffer (SO_SNDBUF) on accepted
	// connections. Zero means use OS default. (D7)
	WriteBufferSize int

	// KeepAlivePeriod controls TCP keep-alive on accepted connections.
	// When > 0, SetKeepAlive(true) is called and the keep-alive period
	// is set to this value on every accepted *net.TCPConn. Zero leaves
	// the keep-alive setting at the OS default (typically off on most
	// kernels for accepted sockets). Useful for long-lived CONNECT
	// tunnels behind NAT where dead-peer detection would otherwise
	// take minutes or hours. (C-09)
	KeepAlivePeriod time.Duration

	// TLSConfig optionally wraps the accept-side listener in
	// tls.NewListener before the SOCKS5 version byte is read. The TLS
	// handshake completes first, then the SOCKS5 protocol runs on the
	// inner cleartext stream. This is the symmetric counterpart to
	// ClientConfig.DialFunc: clients that need to speak SOCKS5-over-TLS
	// already wrap their dialer; the server side now has an equivalent
	// one-liner.
	//
	// When non-nil, both Server.Serve and Server.ListenAndServe wrap
	// the listener via tls.NewListener(ln, TLSConfig). The TLS
	// handshake is lazy (triggered by the first Read/Write on the
	// *tls.Conn) and thus respects the connection deadline set by
	// HandshakeTimeout before the SOCKS5 negotiation begins.
	// A separate TLSHandshakeTimeout is therefore unnecessary in the
	// common case; if the OS TCP timeout is insufficient, set
	// HandshakeTimeout to bound both the TLS and SOCKS5 phases. (Sec-15)
	TLSConfig *tls.Config

	// AcceptProxyProtocol, when true, makes the server read a HAProxy
	// PROXY protocol v1 (text) or v2 (binary) header from each accepted
	// connection BEFORE the SOCKS5 version byte. The source address
	// from the header replaces the underlying TCP RemoteAddr() for the
	// rest of the connection's lifetime, which means MaxConnectionsPerIP
	// (C-04) and PermitSource (C-13) see the real client behind an L4
	// load balancer. When the header is malformed, the connection is
	// closed before any SOCKS5 negotiation occurs.
	//
	// Use this only when every accepted connection comes from a trusted
	// proxy that always emits the header (e.g. an HAProxy / AWS NLB
	// frontend). Enabling it on a public listener allows attackers to
	// spoof their source IP.
	AcceptProxyProtocol bool
}

ServerConfig holds the configuration for a SOCKS5 server. After constructing one, it can be passed directly to NewServer. Zero-value fields are filled with sensible defaults by applyDefaults.

type SocksConn

type SocksConn struct {
	net.Conn
	// contains filtered or unexported fields
}

SocksConn wraps a net.Conn with SOCKS5 metadata from the proxy reply.

func (*SocksConn) BoundAddr

func (c *SocksConn) BoundAddr() *AddrSpec

BoundAddr returns the BND.ADDR/BND.PORT from the SOCKS5 reply. For CONNECT this is the proxy's outbound address (informational). For BIND and ASSOCIATE it is the listen/relay address (essential).

type StaticCredentials

type StaticCredentials map[string]string

StaticCredentials is a map-based credential store backed by an in-memory username -> password map. Password comparison uses crypto/subtle.ConstantTimeCompare to avoid revealing the password length or the location of the first mismatching byte through timing side-channels (Sec-04). When the username is unknown, a dummy compare against a fixed-length scratch buffer is still performed so that the presence or absence of a user cannot be inferred from response time.

func (StaticCredentials) Valid

func (s StaticCredentials) Valid(user, password string) bool

Valid implements CredentialStore using a constant-time comparison.

type UDPRelay

type UDPRelay struct {
	// contains filtered or unexported fields
}

UDPRelay is returned by Client.Associate. It holds the proxy's TCP control connection (which must stay open) and the UDP relay address where datagrams should be sent.

func (*UDPRelay) Close

func (u *UDPRelay) Close() error

Close terminates the UDP association by closing the control connection.

func (*UDPRelay) RelayAddr

func (u *UDPRelay) RelayAddr() *AddrSpec

RelayAddr returns the UDP relay address from the ASSOCIATE reply.

type UserPassAuthenticator

type UserPassAuthenticator struct {
	Credentials CredentialStore
}

UserPassAuthenticator implements the USERNAME/PASSWORD authentication method (method 0x02) as defined in RFC 1929. It validates credentials against the provided CredentialStore.

func (UserPassAuthenticator) Authenticate

func (a UserPassAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error)

Authenticate performs the RFC 1929 sub-negotiation: the client sends a username/password, the server validates it, and writes a status byte.

The {VER, METHOD} method-selection reply is written by Server.authenticate before this method is called (R-03). On error the caller closes the connection (R-05).

func (UserPassAuthenticator) GetCode

func (a UserPassAuthenticator) GetCode() uint8

GetCode returns 0x02.

type UserPassReply

type UserPassReply struct {
	Version uint8
	Status  uint8
}

UserPassReply represents an RFC 1929 username/password reply.

+----+--------+
|VER | STATUS |
+----+--------+
| 1  |   1    |
+----+--------+

func NewUserPassReply

func NewUserPassReply(ver, status uint8) *UserPassReply

NewUserPassReply creates a UserPassReply.

func ParseUserPassReply

func ParseUserPassReply(r io.Reader) (*UserPassReply, error)

ParseUserPassReply reads an RFC 1929 auth reply from r.

func (*UserPassReply) Bytes

func (u *UserPassReply) Bytes() []byte

Bytes serializes the UserPassReply to wire format.

type UserPassRequest

type UserPassRequest struct {
	Version  uint8
	Username string
	Password string
}

UserPassRequest represents an RFC 1929 username/password request.

+----+------+----------+------+----------+
|VER | ULEN |  UNAME   | PLEN |  PASSWD  |
+----+------+----------+------+----------+
| 1  |  1   | 1 to 255 |  1   | 1 to 255 |
+----+------+----------+------+----------+

func NewUserPassRequest

func NewUserPassRequest(ver uint8, user, pass string) *UserPassRequest

NewUserPassRequest creates a UserPassRequest.

func ParseUserPassRequest

func ParseUserPassRequest(r io.Reader) (*UserPassRequest, error)

ParseUserPassRequest reads an RFC 1929 auth request from r.

func (*UserPassRequest) Bytes

func (u *UserPassRequest) Bytes() []byte

Bytes serializes the UserPassRequest to wire format.

Directories

Path Synopsis
demo
acl command
basic command
chained command
customdial command
middleware command
observer command
packetconn command
production command
resolver command
rewriter command
shutdown command
userpass command
webserver command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL