proxy-port

command module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 10 Imported by: 0

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.Copy over *net.TCPConn, which on Linux transfers bytes in-kernel via the splice(2) syscall — zero userspace copies. TCP_NODELAY is enabled so small request/response payloads (Redis, HTTP) are not delayed by Nagle's algorithm.
  • Built for load. SO_REUSEPORT opens 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 > 1 each 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: weighted round-robin, least_conn, or iphash (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 -HUP to 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_conn sends to the backend with the fewest live connections; iphash pins each client IP to a stable upstream. A dial failure parks that backend for fail_cooldown and the request fails over to another; the backend is retried once the cooldown elapses.
  • SIGINT / SIGTERM triggers a graceful shutdown that stops accepting new connections and drains in-flight TCP connections (bounded by drain_timeout; stragglers past the deadline are force-closed).
  • SIGHUP reloads 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

The Go Gopher

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL