Skip to content

Repository files navigation

proxy-port

CI Release Go Reference Go License: MIT

A single-binary, high-throughput TCP/UDP port forwarder with load balancing.

It accepts connections on a local port and relays them to a service on another machine — a remote API, a Redis instance, a database — so it looks like it runs on localhost. Your host never becomes a routing gateway; it only shuttles bytes between two sockets.

[ your app ] ──▶ localhost:6379 ──[ proxy-port ]──▶ 192.168.1.10:6379 [ remote redis ]
proxy-port -L :6379=192.168.1.10:6379
# redis-cli -p 6379   now talks to the remote Redis

Features

Single static binary No runtime, no dependencies. Two Go modules total.
Fast TCP path splice(2) on Linux moves bytes in-kernel — zero userspace copies. TCP_NODELAY keeps small request/response payloads unbuffered.
UDP with NAT sessions Per-client upstream sockets, idle eviction after 60s. Practical for DNS and similar.
Scales across cores SO_REUSEPORT opens N sockets per rule so the kernel spreads TCP accepts and UDP receives across CPUs; each UDP loop owns a private session map (no shared locking).
Load balancing weighted round-robin, least_conn, or iphash affinity across multiple upstreams. Selection is lock-free and allocation-free.
Passive health + failover A backend that fails to dial is parked for a cooldown; traffic fails over to the rest and the backend is retried automatically.
Hot reload SIGHUP applies config changes without dropping in-flight connections. A bad edit is logged and the previous config keeps serving.
Bounded shutdown SIGINT/SIGTERM stops accepting, drains in-flight connections up to drain_timeout, then force-closes stragglers.
Load shedding Optional per-rule connection cap closes excess connections immediately instead of exhausting file descriptors.

Install

Homebrew (macOS and Linux):

brew install nuumz/tap/proxy-port

APT (Debian / Ubuntu) — trust the signing key once, then install and upgrade like any other package:

curl -fsSL https://nuumz.github.io/proxy-port/public.key | sudo gpg --dearmor -o /usr/share/keyrings/proxy-port.gpg
echo "deb [signed-by=/usr/share/keyrings/proxy-port.gpg] https://nuumz.github.io/proxy-port stable main" | sudo tee /etc/apt/sources.list.d/proxy-port.list
sudo apt-get update
sudo apt-get install proxy-port

Other Linux packages.rpm and .apk (amd64 / arm64) ship with every release, as does a standalone .deb if you would rather not add the repository:

sudo rpm  -i  proxy-port_VERSION_linux_amd64.rpm                   # RHEL/Fedora
sudo apk add --allow-untrusted proxy-port_VERSION_linux_amd64.apk  # Alpine
sudo dpkg -i  proxy-port_VERSION_linux_amd64.deb                   # Debian/Ubuntu, one-off

Prebuilt binaries — Linux / macOS / Windows on amd64 and arm64, attached to every release with a checksums.txt:

# example: Linux amd64 (replace VERSION with the release you want)
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

Go install — needs a Go 1.25+ toolchain:

go install github.com/nuumz/proxy-port@latest

From source:

git clone https://github.com/nuumz/proxy-port.git && cd proxy-port
make build          # CGO_ENABLED=0, stripped static binary

Quick start

# One forward, from the command line
proxy-port -L :6379=192.168.1.10:6379

# Several at once; UDP needs an explicit prefix
proxy-port -L 127.0.0.1:8080=10.0.0.5:80 -L udp://:53=8.8.8.8:53

# Load-balance across upstreams (#N sets a relative weight)
proxy-port -L :8080=10.0.0.1:80,10.0.0.2:80,10.0.0.3:80#2

# Or keep a config file and reload it in place
proxy-port init                 # writes ~/.config/proxy-port/config.yaml
proxy-port -c ~/.config/proxy-port/config.yaml
kill -HUP $(pgrep proxy-port)   # apply edits without dropping connections

A minimal config:

rules:
  - name: redis
    listen: ":6379"
    remote: "192.168.1.10:6379"

  - name: api                   # balanced across three backends
    listen: ":8080"
    balance: least_conn
    remotes:
      - "10.0.0.1:80"
      - "10.0.0.2:80"
      - "10.0.0.3:80#2"         # weight 2 under the weighted strategy

  - name: dns
    proto: udp
    listen: ":53"
    remote: "8.8.8.8:53"

Every tunable — timeouts, buffers, reuseport, connection caps, balancing — has a documented default and can be overridden globally or per rule. See the configuration reference.

Documentation

Document What's in it
Configuration reference Every CLI flag and YAML key, defaults, precedence, hot-reload semantics
Architecture Internals, concurrency model, invariants, and how to extend it (new balancing strategy, new protocol)
Contributing Build, test, benchmark, and PR conventions
Releasing Cutting a release; enabling the Homebrew and APT channels
Stable hostname over VPN Guide for reaching a proxy host whose VPN IP changes (Thai)

Embedding the engine

The forwarding engine is a self-contained package, but it currently lives under internal/, so Go will not let you import it from another module yet. To build on it today, fork or vendor the repository; if you would rather import it directly, open an issue — moving the package to a public path is a small change and a welcome request.

Once vendored, the whole engine is three calls:

import "github.com/nuumz/proxy-port/internal/forward"

rule, err := forward.ParseRule(":6379=10.0.0.5:6379")
if err != nil {
    return err
}

sup := forward.NewSupervisor(false)              // verbose logging off
go sup.Run(ctx, []forward.Rule{rule})            // blocks until ctx is done and the drain finishes
err = sup.Reload(updatedRules)                   // swap rules without dropping live connections
sup.Stop()                                       // stop everything and drain

forward.Rule is a plain struct, so rules can also be built programmatically instead of parsed from a spec string — including multi-upstream rules with weights and a balancing strategy. See Architecture for the types and lifecycle, and Configuration for what each field means.

Performance notes

Measured on loopback with an in-process echo upstream (see Contributing → benchmarks):

  • TCP relay: 0 allocations per round-trip — the data path is splice(2), so payload bytes never enter userspace.
  • UDP relay: 2 allocations per round-trip, with recycled 64 KiB buffers so memory tracks concurrent sessions rather than cumulative traffic.
  • Upstream selection: 0 allocations, lock-free — ~18 ns (iphash) to ~50 ns (weighted) per new connection under full parallelism, and it runs once per connection, not per packet.
  • GC is not on the critical path: the heap stays flat, so GOGC=off leaves p50/p99/p99.9 unchanged within noise. Tail spikes come from OS scheduling.

Numbers depend on hardware and kernel tuning — reproduce them locally rather than treating them as guarantees.

Contributing

Issues and pull requests are welcome. Please read CONTRIBUTING.md first — it covers the test/benchmark commands and the invariants (non-disruptive reload, bounded shutdown, non-blocking load shedding) that changes need to preserve.

License

MIT © nuumz

About

Single-binary TCP/UDP port forwarder with load balancing, passive health failover, and hot reload. Written in Go.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages