Portunus
Features

TCP Forwarding

The core forwarding primitive — bind a TCP listener on the edge client and forward every accepted connection to a configured upstream target.

TCP forwarding is the core primitive. Every other feature (UDP, port ranges, SNI routing, rate limiting) layers on top of it.

How it works

Each TCP rule runs a single accept loop on the client. For every accepted connection the client opens one upstream TcpStream::connect to the configured target and runs a bidirectional byte copy until either side closes.

There is no userspace buffering beyond the kernel's send/recv windows. The forwarder is a pure L4 byte passthrough — TLS sessions, HTTP requests, and arbitrary protocols pass through unmodified.

On Linux, connections without a bandwidth cap take the splice(2) fast path (zero-copy through the kernel). Rate-limited connections fall back to a userspace copy loop. The listener binds with SO_REUSEADDR, so a fast process restart (e.g. docker restart) rebinds immediately instead of failing with port_in_use while a prior socket sits in TIME_WAIT.

Push a rule

portunus-server push-rule <client_name> <listen_port> <target_host>:<target_port>

Example:

portunus-server push-rule edge-01 8080 example.com:80
# → rule_id=1

Within ~1 second (the ack budget) the rule transitions Pending → Active and the client starts accepting on port 8080.

Inspect

# Operator CLI
portunus-server list-rules --client edge-01
portunus-server rule-stats 1

# Operator HTTP API
curl -H "Authorization: Bearer $PORTUNUS_OPERATOR_TOKEN" \
     http://127.0.0.1:7080/v1/rules

# Prometheus
curl -s http://127.0.0.1:7081/metrics | grep '^portunus_rule'

Remove

portunus-server remove-rule <rule_id>

In-flight connections drain up to shutdown_drain_timeout_secs (default 30 s). The listener stops accepting immediately.

Failure modes

ConditionOutcome
Listen port already in use on clientRule state Failed(port_in_use); remove + retry on a free port
Target connect refused / timed outConnection-level error; rule stays Active, no metric scrubbing
Client disconnected when rule pushedHTTP 4xx with client_not_connected
Client reconnectsRules persist server-side and re-push to the same client

Metrics

Per-rule collectors (one row per live rule, labels {client, rule, owner}):

  • portunus_rule_bytes_in_total
  • portunus_rule_bytes_out_total
  • portunus_rule_active_connections

Byte counters update incrementally (every 64 KiB) even on the splice fast path, so long-lived connections (SSH, gRPC streams, WebSockets) report live throughput instead of staying frozen until the connection closes.

See Metrics for the full collector list.

See also

On this page