README
¶
proxy-port
A simple, fast TCP/UDP port forwarder written in Go.
It accepts connections on a local port and relays them to a service running on
another machine — a remote API, a Redis instance, a database — so they
appear as if they were running on localhost. Your machine never becomes a
routing gateway; it just shuttles bytes between two sockets.
[ your app ] ──▶ localhost:6379 ──[ proxy-port ]──▶ 192.168.1.10:6379 [ remote redis ]
Why
- Single static binary, no runtime. Run from CLI flags or a remembered YAML config.
- Fast. TCP relaying uses Go's
io.Copyover*net.TCPConn, which on Linux transfers bytes in-kernel via thesplice(2)syscall — zero userspace copies.TCP_NODELAYis enabled so small request/response payloads (Redis, HTTP) are not delayed by Nagle's algorithm. - Built for load.
SO_REUSEPORTopens N sockets per rule so the kernel spreads work across cores — TCP accepts for TCP rules, the datagram receive loop for UDP rules; an optional per-rule connection cap sheds load to protect against FD exhaustion; TCP keepalive reaps dead peers. - Concurrent. One lightweight goroutine per connection; thousands of simultaneous connections are cheap.
- UDP too, with per-client NAT sessions and idle eviction (handy for DNS).
With
reuseport > 1each receive loop owns a private session map, so the hot path scales across cores with no shared locking. - Load balancing. Point a rule at several upstreams and pick a strategy:
weightedround-robin,least_conn, oriphash(client affinity). Selection is lock-free and allocation-free; a backend that fails to dial is parked for a cooldown and traffic fails over to the rest (passive health). - Hot reload. Edit the config and
kill -HUPto add, remove, or change rules without dropping in-flight connections.
Install
Prebuilt binaries for Linux / macOS / Windows on amd64 and arm64 are attached
to every release. Each
archive bundles the README and LICENSE, and checksums.txt covers them all.
# example: Linux amd64
curl -fsSLO https://github.com/nuumz/proxy-port/releases/latest/download/proxy-port_VERSION_linux_amd64.tar.gz
tar xzf proxy-port_VERSION_linux_amd64.tar.gz
sudo install -m 0755 proxy-port /usr/local/bin/proxy-port
Linux packages — .deb, .rpm and .apk (amd64 / arm64) are attached to
each release as well. Download the one matching your distro and architecture:
sudo dpkg -i proxy-port_VERSION_linux_amd64.deb # Debian/Ubuntu
sudo rpm -i proxy-port_VERSION_linux_amd64.rpm # RHEL/Fedora
sudo apk add --allow-untrusted proxy-port_VERSION_linux_amd64.apk # Alpine
Go install (needs a Go toolchain):
go install github.com/nuumz/proxy-port@latest
Homebrew and an APT repository are wired up in the release pipeline but not yet enabled — they need one-time credentials (a tap repo + token, and a GPG signing key + GitHub Pages). See docs/RELEASING.md for how to turn them on; until then use the binaries or Linux packages above.
Build from source
go build -o proxy-port .
# or
make build
Usage
proxy-port [-c config.yaml] [-L LISTEN=REMOTE ...] [-v]
proxy-port init [path] # write a starter config (the "remembered" config)
proxy-port version # print the build version and exit
Each -L is a forwarding rule, LISTEN=REMOTE. Repeat -L for multiple
forwards. The protocol defaults to TCP; prefix with tcp:// or udp:// to be
explicit. -L rules are appended on top of any config file.
Examples
Expose a remote Redis as a local port:
proxy-port -L :6379=192.168.1.10:6379
# now: redis-cli -p 6379 talks to the remote Redis
Forward a local port to a remote HTTP API, plus DNS over UDP, at once:
proxy-port -L 127.0.0.1:8080=10.0.0.5:80 -L udp://:53=8.8.8.8:53
Bind to all interfaces (so other hosts on your LAN can reach the remote too):
proxy-port -L 0.0.0.0:5432=db.internal:5432
Load-balance across several upstreams (comma-separated; #N sets a weight):
proxy-port -L :8080=10.0.0.1:80,10.0.0.2:80,10.0.0.3:80#2
Add -v to log every connection open/close.
Config file (remembered config)
Generate a commented starter config, edit it, then run from it:
proxy-port init # writes ~/.config/proxy-port/config.yaml
proxy-port -c ~/.config/proxy-port/config.yaml
When no -c is given, the config is searched for in order: ./proxy-port.yaml,
$XDG_CONFIG_HOME/proxy-port/config.yaml, ~/.config/proxy-port/config.yaml.
defaults: # applied to every rule unless the rule overrides it
tcp_nodelay: true # disable Nagle for low latency on small payloads
tcp_keepalive: 30s # detect dead peers; 0 disables
dial_timeout: 10s # give up establishing the upstream after this long
balance: weighted # multi-upstream balancing: weighted | least_conn | iphash
fail_cooldown: 10s # park a backend this long after a dial failure before retrying it
max_connections: 0 # per-rule concurrent connection cap; 0 = unlimited
read_buffer: 0 # socket SO_RCVBUF in bytes; 0 = OS default
write_buffer: 0 # socket SO_SNDBUF in bytes; 0 = OS default
reuseport: 1 # SO_REUSEPORT sockets per rule (>1 spreads TCP accepts / UDP receive across cores)
drain_timeout: 15s # max wait for in-flight connections on stop/reload
rules:
- name: redis
listen: ":6379"
remote: "192.168.1.10:6379" # single upstream
max_connections: 5000 # per-rule override
- name: api # load-balanced across three backends
listen: ":8080"
balance: least_conn # per-rule override of the default strategy
remotes:
- "10.0.0.1:80"
- "10.0.0.2:80"
- "10.0.0.3:80#2" # weight 2: takes twice the share (weighted strategy)
- name: dns
proto: udp
listen: ":53"
remote: "8.8.8.8:53"
log:
verbose: false
Durations accept Go syntax (30s, 1m, 500ms) or a bare number of seconds.
Hot reload
Edit the config and send SIGHUP:
kill -HUP $(pgrep proxy-port)
Rules are diffed by listen address: unchanged rules keep serving (their live connections are never touched), added rules start, removed/changed rules stop accepting and drain. A bad edit is logged and the proxy keeps running on the previous config.
Flags
| Flag | Description |
|---|---|
-c PATH |
Path to a YAML config file (overrides the search path). |
-L LISTEN=REMOTE |
Forwarding rule. Repeatable. Optional tcp:// (default) / udp:// prefix. Appended on top of the config. |
-v |
Verbose: log each connection open and close. |
-version |
Print the build version (also proxy-port version) and exit. |
Sub-commands: proxy-port init [path] writes a starter config;
proxy-port version prints the build version, commit and date.
Behaviour notes
- TCP uses half-close: when one side finishes sending, its write half is closed so the peer sees EOF, while the other direction keeps flowing until it too completes. This is correct for request/response protocols.
- UDP is connectionless, so each distinct client source address gets its own upstream socket (symmetric-NAT style). Sessions idle for 60s are reclaimed. Under load balancing, a UDP client sticks to the upstream chosen when its session opened (per-client affinity for the session's lifetime).
- Load balancing distributes new connections (TCP) or new sessions
(UDP) across a rule's upstreams.
weighted(default) is round-robin honouring per-upstream#weight;least_connsends to the backend with the fewest live connections;iphashpins each client IP to a stable upstream. A dial failure parks that backend forfail_cooldownand the request fails over to another; the backend is retried once the cooldown elapses. SIGINT/SIGTERMtriggers a graceful shutdown that stops accepting new connections and drains in-flight TCP connections (bounded bydrain_timeout; stragglers past the deadline are force-closed).SIGHUPreloads the config in place (see Hot reload).
Benchmark
make load runs a concurrent round-trip throughput benchmark through the
forwarder in front of an in-process echo server.
For tail-latency and GC behaviour there is a closed-loop profile, gated so it never runs in the normal suite:
PROXY_LAT=1 go test ./internal/forward/ -run TestTCPLatencyProfile -v
It reports p50/p90/p99/p99.9/max round-trip latency alongside GC cycles and pause totals.
Releasing
Pushing a semver tag on main publishes a GitHub Release with binaries and
Linux packages — see docs/RELEASING.md for the full
runbook, including how to enable the Homebrew and APT channels.
License
MIT — see LICENSE.
Documentation
¶
There is no documentation for this package.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
config
Package config loads and validates proxy-port's YAML configuration and resolves it into fully-populated forward.Rule values.
|
Package config loads and validates proxy-port's YAML configuration and resolves it into fully-populated forward.Rule values. |
|
forward
Package forward implements a lightweight, high-throughput TCP/UDP port forwarder.
|
Package forward implements a lightweight, high-throughput TCP/UDP port forwarder. |