English | 简体中文
A lightweight, scalable SOCKS5 server written in Rust (RFC 1928 + RFC 1929), with a live terminal dashboard and a headless mode for containers. The protocol is hand-written; the dependency footprint is kept deliberately small.
- SOCKS5 commands —
CONNECTandUDP ASSOCIATE(RFC 1928).BINDis rejected with reply code0x07by design. - Authentication — No-Auth (
0x00) and Username/Password (0x02, RFC 1929). - Address types — IPv4, IPv6, and Domain (
ATYP0x01/0x04/0x03), with server-side DNS resolution for both CONNECT and UDP targets. - Full RFC error mapping — every reply code
0x00–0x08is produced where applicable (e.g. unknown command →0x07, unknown address type →0x08, connection limit →0x02, refused/unreachable/timeout mapped from the OS). - UDP relay — SOCKS5 encapsulation,
FRAG != 0dropped, source-IP filtering, a client-reachableBND.ADDR(never0.0.0.0), and idle reclaim. - TUI dashboard — real-time throughput with a trend chart, a sortable
active-connection table, success/error stats, and a scrollable log with
keyboard navigation (built on ratatui). A hidden
--mockflag streams synthetic data for previewing/testing the UI without real traffic. - Headless mode —
--no-tuistreams events to stdout, ideal for systemd / containers. The TUI is an optional cargo feature, so headless builds drop the ratatui/crossterm dependencies entirely. - Robustness — connect / TCP-idle / UDP-idle timeouts, optional
max_connectionslimit, half-open-aware relay, and graceful shutdown. - Configuration — TOML file with CLI overrides.
- Small & portable — pure Rust, no C dependencies; ships as fully static
musl binaries and a ~3.5 MB
scratch-based container image.
The installer picks binary or docker, generates credentials and a free port automatically, and starts the service.
# Binary install, auth enabled with auto-generated user/password, random port:
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/next-socks5/main/install.sh | sh
# With options (note the `-s --` to pass args through curl | sh):
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/next-socks5/main/install.sh \
| sh -s -- --port 1080Or clone and run locally. Each example is annotated below; in auth mode with no
--user / --pass, the installer generates a username and a 20-character
password and prints them at the end, together with a ready-to-use socks5://
URL and a curl test command.
# Show every flag and exit
./install.sh --help
# Simplest run: binary install, auth ON (auto-generated user/password), random free port
./install.sh
# Docker instead of a native binary (host networking, so UDP ASSOCIATE works)
./install.sh --method docker
# Open proxy (no auth) on a fixed port — only on a trusted network
./install.sh --method binary --no-auth --port 1080
# Explicit credentials on a fixed port
./install.sh --method docker --auth --user alice --pass secret --port 1080
# Bind to a single interface instead of 0.0.0.0 (here: localhost only)
./install.sh --no-auth --listen 127.0.0.1 --port 1080
# UDP relay behind NAT/Docker: pin the relay port range and advertise a public IP
./install.sh --port 1080 --udp-port-range 40000-40100 --udp-advertise 203.0.113.42
# Pin a specific release instead of `latest`
./install.sh --version v0.2.0 --port 1080
# Install the binary + config only — do NOT create or start a service
./install.sh --no-service --port 1080 # same as: NO_SERVICE=1 ./install.sh --port 1080
# Custom location: binary install dir (binary) / compose deploy dir (docker)
./install.sh --bin-dir /opt/bin --port 1080
./install.sh --method docker --dir ./ns5 --port 1080| Flag | Description | Default |
|---|---|---|
--method <binary|docker> |
Native binary (systemd/OpenRC) or Docker Compose | binary |
--auth / --no-auth |
Enable username/password auth, or run open | --auth |
--user / --pass |
Credentials for auth mode (random if omitted) | random |
--port <port> |
Listen port (random free port if omitted) | random |
--listen <addr> |
Bind address | 0.0.0.0 |
--udp-port-range <range> |
Bind UDP relay sockets inside an inclusive range (e.g. 40000-40100) |
OS ephemeral |
--udp-advertise <ip> |
Advertised BND IP for UDP behind NAT/Docker (a client-reachable address) | bound IP |
--version <tag> |
Release version, e.g. v0.1.0 |
latest |
--bin-dir <dir> |
Binary install directory (binary method) | /usr/local/bin |
--dir <dir> |
Docker deploy directory (docker method) | ./next-socks5-deploy |
--no-service |
Install binary + config only; don't set up/start a service | off |
Binary install targets Linux (musl x86_64 / aarch64) and sets up a systemd or OpenRC service. If neither init system is present, the binary and config are installed but not started (and won't auto-start on reboot) — start it manually or use
--method dockerfor a self-restarting container. The installer is POSIXsh(no bash required).
Fastest — let the installer generate docker-compose.yml + config.toml and
start the container for you (host networking; with --auth and no --user /
--pass, credentials are auto-generated and printed at the end):
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/next-socks5/main/install.sh \
| sh -s -- --method docker --auth --port 1080This writes both files into ./next-socks5-deploy/ (override with --dir) and
runs docker compose up -d. To wire it up manually instead:
# No-auth, host networking (UDP ASSOCIATE works), listening on 1080:
docker run -d --name next-socks5 --network host \
ghcr.io/zingerlittlebee/next-socks5:latest --listen 0.0.0.0:1080With a config file (for auth):
docker run -d --name next-socks5 --network host \
-v "$PWD/config.toml:/etc/next-socks5/config.toml:ro" \
ghcr.io/zingerlittlebee/next-socks5:latest --config /etc/next-socks5/config.tomlOr with Compose (docker-compose.yml):
services:
next-socks5:
image: ghcr.io/zingerlittlebee/next-socks5:latest
container_name: next-socks5
restart: unless-stopped
network_mode: host
volumes:
- ./config.toml:/etc/next-socks5/config.toml:ro
# Writable runtime dir for the admin/attach socket — the image runs as an
# unprivileged user that can't create /run/next-socks5 on its own. Without
# this, `docker exec ... next-socks5 attach` cannot connect.
tmpfs:
- /run/next-socks5
command: ["--config", "/etc/next-socks5/config.toml"]docker compose up -dImages are multi-arch (linux/amd64, linux/arm64) and tagged with both the
release version (e.g. 0.1.0) and latest. The container always runs headless.
Download a static musl build from the Releases page:
curl -fL -o next-socks5.tar.gz \
https://github.com/ZingerLittleBee/next-socks5/releases/latest/download/next-socks5-x86_64-unknown-linux-musl.tar.gz
tar xzf next-socks5.tar.gz
./next-socks5-x86_64-unknown-linux-musl/next-socks5 serve --no-tui --listen 0.0.0.0:1080(Replace x86_64 with aarch64 for ARM64.)
Requires a recent stable Rust toolchain.
git clone https://github.com/ZingerLittleBee/next-socks5
cd next-socks5
cargo build --release
./target/release/next-socks5 serve # TUI dashboard
./target/release/next-socks5 serve --no-tui # headless
# Headless-only build (drops the TUI deps):
cargo build --release --no-default-featuresOr install straight from git:
cargo install --git https://github.com/ZingerLittleBee/next-socks5Configuration is a TOML file (see config.example.toml);
CLI flags override file values.
listen = "0.0.0.0:1080"
[auth]
method = "password" # "none" | "password"
# One or more credentials — add a [[auth.users]] block per user.
[[auth.users]]
username = "alice"
password = "secret"
[[auth.users]]
username = "bob"
password = "hunter2"
[timeouts]
handshake_ms = 10000 # greeting+auth+request deadline (anti-slowloris)
connect_ms = 10000
tcp_idle_ms = 300000
udp_idle_ms = 60000
[limits]
max_connections = 2048 # optional: global concurrent cap (unbounded if unset)
max_per_ip = 64 # optional: per-source-IP concurrent cap (unbounded if unset)
[admin]
enabled = true # local attach endpoint (default on)
# socket = "/run/next-socks5/admin.sock" # override the socket pathMultiple users. With method = "password", add a [[auth.users]] block per
credential — a client is accepted if its username/password matches any entry
in the list (RFC 1929). This is the recommended way to serve several users from a
single port; you do not need a separate port per user. With method = "none" the
proxy is open and the users list is ignored. (The dashboard logs each auth
attempt as auth ok/failed for '<user>'; per-user traffic accounting is not yet
shown in the connections table.)
Connection limits. Both caps under [limits] are optional and unbounded by
default; the server enforces them at accept time, so half-open/handshaking
connections count too. They are not set automatically — opt in via the config:
max_connections— global cap on concurrent connections; a backstop against file-descriptor / task exhaustion. Size it to your host (the OSRLIMIT_NOFILEis the ultimate ceiling; each CONNECT relay uses ~2 fds).max_per_ip— concurrent connections from a single source IP. Stops one client from monopolizing the proxy or brute-forcing credentials at high concurrency. A generous value (e.g. 64–256) does not affect normal clients; lower it only if you do not expect many users behind a single NAT.
For an internet-facing deployment, set both. The proxy has no built-in auth rate-limiting, so also front the listen port with a host firewall / fail2ban when it is publicly exposed.
Secure defaults. Egress filtering is on by default: the proxy refuses to
relay to loopback, link-local (including the 169.254.169.254 cloud-metadata
address), and private/RFC1918 ranges (an SSRF / open-relay guard). If you genuinely
need to reach internal targets, relax it with an [egress] section — see
config.example.toml. The pre-relay handshake is bounded by
timeouts.handshake_ms (default 10s) to drop slowloris-style stalled clients.
CONNECT works over the single TCP listen port, but UDP ASSOCIATE uses a
separate UDP relay socket. By default each association binds an OS-assigned
ephemeral UDP port and the server advertises a BND.ADDR:BND.PORT that the client
must send its datagrams to (RFC 1928). Two [udp] options make this work
through firewalls and NAT:
[udp]
port_range = "40000-40100" # bind UDP relay sockets to this inclusive range
advertise = "203.0.113.42" # advertised BND IP (a client-reachable address)
install.sh --udp-port-range 40000-40100 --udp-advertise 203.0.113.42writes exactly this[udp]block for you.
port_range— bind each association's UDP socket inside a known range instead of a random ephemeral port, so a firewall/NAT only needs that range opened. Each association binds its own socket, so size the range ≥ your expected concurrent UDP clients;"40000-40000"is a single port and serializes UDP. When the range is exhausted, UDP ASSOCIATE returns a general failure.advertise— the IP put in the UDP ASSOCIATE reply. By default the server advertises the server-side IP the client's TCP connection arrived on (the control socket's local address); override it when that IP is not client-reachable (behind NAT, or Docker bridge networking). The advertised port is always the real bound port, so any NAT/forward must be port-preserving (1:1). An unreachable advertised address is the #1 cause of "TCP works but UDP doesn't". Accepts a bare IP orip:port(port ignored); a malformed value is rejected at startup rather than silently ignored.
Docker. The provided compose uses network_mode: host (Linux), which needs no
port mapping. For bridge networking, publish the TCP control port and the UDP
range with short syntax (Compose long syntax has no range support) and set
advertise to the host's public IP:
ports:
- "1080:1080/tcp"
- "40000-40100:40000-40100/udp"Keep the range small with the default userland proxy (Docker spawns one
docker-proxy process per published port); for large ranges set
userland-proxy=false or use host networking.
Firewall (range 40000-40100/udp + control 1080/tcp):
# ufw
ufw allow 1080/tcp && ufw allow 40000:40100/udp
# nftables
nft add rule inet filter input udp dport 40000-40100 accept
# iptables
iptables -A INPUT -p udp --dport 40000:40100 -j ACCEPTPort-remapping (PAT / symmetric) NAT cannot work with a multi-port range (the
advertised internal port is wrong after translation). Use a single fixed port
("40000-40000") with a 1:1 forward of that one port, or host on a directly
reachable public IP.
next-socks5 Print help (a bare invocation never starts a server)
next-socks5 serve [OPTIONS] Run the server (alias: run)
next-socks5 attach [OPTIONS] Attach to a running server's dashboard
Server options:
--config <path> Path to a TOML config file
--listen <addr> Override the listen address (e.g. 0.0.0.0:1080)
--no-tui Run headless (events to stdout) instead of the dashboard
--no-admin Disable the local admin/attach endpoint
--admin-socket <path> Override the admin socket path
-h, --help Print help
attach options:
--socket <path> Admin socket to connect to
(default /run/next-socks5/admin.sock)
# Test a no-auth proxy:
curl --socks5 127.0.0.1:1080 https://example.com
# Test a password-authenticated proxy:
curl --socks5 alice:secret@127.0.0.1:1080 https://example.comThe terminal dashboard is on by default — just run the server without
--no-tui:
next-socks5 serve --listen 127.0.0.1:1080It shows live throughput (with a 30s trend chart), success/error stats, a sortable Active connections table, and a scrolling Log. Keys:
| Key | Action |
|---|---|
Tab |
Move scroll focus between the connections table and the log (focused panel is highlighted) |
s |
Cycle the connection sort key: ID → UP↓ → DOWN↓ → AGE↓ (shown in the table title) |
↑ / ↓ or k / j |
Scroll the focused panel one line |
PgUp / PgDn |
Scroll the focused panel one screen |
q / Ctrl-C |
Quit |
To exercise the dashboard without sending any real traffic, add --mock. It
drives the same metrics and event bus the proxy uses with a stream of synthetic
connections, throughput, and errors — handy for trying the sorting/scrolling
keys or taking screenshots. The fake activity stops as soon as you quit.
# Local preview: open the dashboard and continuously generate mock data.
cargo run --release -- serve --listen 127.0.0.1:1080 --mock
# Or with an installed binary:
next-socks5 serve --listen 127.0.0.1:1080 --mock--mock is a demo/testing aid only; never enable it on a real proxy.
A service installed via systemd / OpenRC / Docker runs headless (no UI of
its own), but it still serves the live dashboard over a local Unix socket
(default /run/next-socks5/admin.sock). To watch a server that is already
running, attach to it from the same machine — there is nothing to restart and
no flag to enable; the endpoint is on by default.
# 1. SSH into the host where the service runs (as root for the default socket):
ssh root@your-server
# 2. Attach — default socket /run/next-socks5/admin.sock:
next-socks5 attach
# Docker: run attach inside the container instead:
docker exec -it next-socks5 next-socks5 attachIf the service uses a non-default socket path (e.g. a manual install on a custom
path), point --socket at it:
next-socks5 attach --socket /tmp/ns5.sockThe endpoint is local-only (no network exposure, no auth) and read-only — attach
clients observe but cannot control the server. Press q to detach; if the
server stops, the dashboard exits with connection lost.
The default socket lives under
/run/next-socks5(mode0710, owned by the service user).rootcan always attach; a non-root user can only attach to a socket it owns (e.g. a manual install under/tmpor$XDG_RUNTIME_DIR).Docker: the container runs as an unprivileged user (uid
65534) and needs a writable/run/next-socks5for the admin socket. The installer's generated Compose (and the example above) provide it viatmpfs; a baredocker runneeds--tmpfs /run/next-socks5. Without it the server logsadmin endpoint disabled: Permission deniedandattachcannot connect.
Disable the endpoint with --no-admin or [admin] enabled = false.
For a manual install (--no-service), the process runs as your user and the
default /run path is usually not writable. Start the server with a writable
socket and attach to the same path:
next-socks5 serve --no-tui --admin-socket /tmp/ns5.sock
next-socks5 attach --socket /tmp/ns5.sockOn a single 4-core cloud VM (loopback), next-socks5 relays at ~2 GB/s with
~1.6 ms of added per-request latency and ~6k new connections/s, and
profiling shows the proxy is kernel/network-bound with no lock contention — i.e.
the proxy itself is not the bottleneck. See
docs/PERFORMANCE.md for the methodology, the reproducible
harness (tests/scripts/), and full numbers.
See LICENSE.
