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.
| 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 |
go get github.com/malivvan/socks5Minimum Go version: 1.23. The only runtime dependency is github.com/malivvan/zero
for structured logging; testify is test-only.
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"))
}server, _ := socks5.NewServer(&socks5.ServerConfig{
AuthMethods: []socks5.Authenticator{
socks5.UserPassAuthenticator{
Credentials: socks5.StaticCredentials{"admin": "secret"},
},
},
})
server.ListenAndServe("tcp", ":1080")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,
})| 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 |
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.
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")| 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) |
| 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 |
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.
// 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.
echoPC, _ := socks5.NewPacketConn("127.0.0.1:0")
// echoPC implements net.PacketConn — use ReadFrom/WriteToThe demo/packetconn example shows a complete in-process UDP echo
service routed through the SOCKS5 ASSOCIATE handler.
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.
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.
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.
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/basicmake 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 cacheSee docs/BENCH.md for detailed benchmark methodology and results,
including throughput, latency, and allocation comparisons.
See docs/CHANGELOG.md for the complete v1.0.0 release notes covering
all audit-cycle changes.
If you are pinned to a pre-v1 commit, two source-breaking changes during the audit cycle require small migrations:
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.ReadWriterforwarder (in-process pipes, custom transports): use the newLiteForwarder(src, dst io.ReadWriter) errorhelper.
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 toReplyHostUnreachablevia the newErrHostUnreachablesentinel.
VirtualListener→ListenerVirtualPacketConn→PacketConnNewVirtualListener→NewListenerNewVirtualPacketConn→NewPacketConnServerConfig.VirtualListeners→ServerConfig.Listeners
See docs/CHANGELOG.md for the complete v1.0.0 changelog.
| 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 |