Portunus

Configuration

TOML config schema, CLI flags, signals, and example configs for portunus-standalone.

Config schema

# [global] section — all fields optional
[global]
label              = "my-forwarder"   # human label; informational only
log_level          = "warn"           # tracing EnvFilter directive (default: "warn")
log_format         = "json"           # "json" (default) or "pretty"
shutdown_drain_secs = 30              # graceful-drain budget on SIGTERM (default: 30)

# [defaults] section — values applied to rules that omit the field
[defaults]
protocol          = "tcp"    # optional; rules can still override
udp_max_flows     = 1024     # per-rule UDP flow table cap (default: 1024)
udp_flow_idle_secs = 60      # idle-flow expiry in seconds (default: 60)
prefer_ipv6       = false    # address-family preference (default: false)

# [[rule]] — one or more required
[[rule]]
name          = "ssh-tunnel"          # unique, used in log output
protocol      = "tcp"                 # "tcp" or "udp"
listen_port   = 2222                  # single port  }  mutually exclusive
# listen_ports = "8000-8009"          # port range   }
target        = "10.0.0.5:22"        # single target }  mutually exclusive
# targets = [...]                     # multi-target  }
prefer_ipv6   = false                 # per-rule override (optional)
udp_max_flows = 1024                  # per-rule override for UDP (optional)
udp_flow_idle_secs = 60              # per-rule override for UDP (optional)

CLI flags

FlagDefaultDescription
--config <PATH>./portunus.tomlPath to the TOML config file
--checkfalseValidate config and exit (0 = valid, 2 = invalid)
--log-level <LEVEL>from configOverride log level
--log-format <FMT>from configOverride log format (json or pretty)
--no-statsfalseDisable the [stats] UDS server (daemon only)
--stats-socket <PATH>from configOverride [stats].socket_path (daemon only)

The stats subcommand has its own flags — see Live stats dashboard.

When --config is omitted the binary loads ./portunus.toml from the working directory. Exit 2 if that file does not resolve.

Signals

SignalBehaviour
SIGTERM / SIGINTGraceful shutdown — active connections drain for up to shutdown_drain_secs, then the process exits 0.
SIGHUPNo-op in the current release (config reload is not yet supported).

The shipped systemd unit deliberately omits ExecReload, so systemctl reload portunus-standalone reports "Job type reload is not applicable" rather than killing the service. Edit the TOML and run systemctl restart portunus-standalone.

Example configs

Minimal

The smallest config that the binary will accept: one rule, no [global] or [defaults] blocks (every optional field falls back to its default).

[[rule]]
name        = "ssh"
protocol    = "tcp"
listen_port = 2222
target      = "10.0.0.5:22"

Full

Every field the schema recognises, plus one rule for each common case (single TCP, TCP range, multi-target failover with PROXY protocol, DNS target, single UDP, UDP range). Copy and trim as needed.

# ─────────── Global (all fields optional) ───────────
[global]
label               = "edge-1"     # informational label printed in logs
log_level           = "warn"       # EnvFilter; e.g. "portunus=debug,info"
log_format          = "json"       # "json" (default) or "pretty"
shutdown_drain_secs = 30           # graceful-drain budget on SIGTERM

# ─────────── Defaults (inherited by rules that omit the field) ───────────
[defaults]
protocol           = "tcp"         # default protocol when a rule omits it
udp_max_flows      = 1024          # per-rule UDP flow table cap
udp_flow_idle_secs = 60            # idle UDP flow expiry, seconds
prefer_ipv6        = false         # DNS address-family preference

# ─────────── Rules ───────────

# 1) Single-port TCP
[[rule]]
name        = "ssh-jump"
protocol    = "tcp"
listen_port = 2222
target      = "10.0.0.5:22"

# 2) TCP port range (listen and target widths must match)
[[rule]]
name         = "web-range"
protocol     = "tcp"
listen_ports = "8000-8009"
target       = "10.0.0.10:8000-8009"

# 3) Multi-target failover + PROXY protocol v2
[[rule]]
name        = "ha-https"
protocol    = "tcp"
listen_port = 8443
targets = [
  { host = "primary.internal",   port = 443, priority = 0,  proxy_protocol = "v2" },
  { host = "secondary.internal", port = 443, priority = 10, proxy_protocol = "v2" },
]

# 4) DNS target (resolved on each connect; `prefer_ipv6` overrides defaults)
[[rule]]
name        = "api"
protocol    = "tcp"
listen_port = 9000
target      = "api.internal.example.com:443"
prefer_ipv6 = false

# 5) Single-port UDP with bumped flow cap + idle timeout
[[rule]]
name               = "game-udp"
protocol           = "udp"
listen_port        = 27015
target             = "10.0.0.20:27015"
udp_max_flows      = 4096
udp_flow_idle_secs = 120

# 6) UDP port range
[[rule]]
name         = "voice-udp"
protocol     = "udp"
listen_ports = "30000-30019"
target       = "10.0.0.30:30000-30019"

Validate any config without starting the service:

# binary install
portunus-standalone --check --config /etc/portunus/standalone.toml

# docker
docker run --rm \
  -v "$PWD/portunus.toml:/etc/portunus/standalone.toml:ro" \
  ghcr.io/zingerlittlebee/portunus-standalone:latest \
  --config /etc/portunus/standalone.toml --check

Prints ok and exits 0 on success, exits 2 on error.

Observability

portunus-standalone logs to stderr in JSON format by default (switch to log_format = "pretty" for human-readable output during development). Every rule emits structured events on startup, and a periodic reporter logs per-rule byte + connection counts every 60 seconds.

There is no embedded HTTP server, Prometheus endpoint, or gRPC stream — those surfaces belong to portunus-server. Use log aggregation (e.g. journald, docker logs, a sidecar log shipper) to collect the JSON output.

Differences from portunus-client

Featureportunus-clientportunus-standalone
Control planegRPC stream to portunus-serverNone — TOML only
Dynamic rule updatesYes (server push)No — restart required
RBAC / multi-userYesNo
Rate limiting / QoSYesNo
Prometheus metricsYes (loopback HTTP)No
Audit logVia serverStructured log to stderr
SNI routingYesNo
TCP splice fast path (Linux)YesYes (same forwarder code)
PROXY protocol outYesYes

On this page