Skip to content

Latest commit

 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

socks5 godoc test Coverage Status Release 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/malivvan/zero 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

About

a production-grade SOCKS5 server package for Go with full RFC 1928 compliance

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages