Portunus
Operations

Upgrade Guide

Wire-format invariants, capability gates, and the v0.7 → v0.8 fresh-start convention.

Wire-format changes are additive only — newer fields use proto3 default-stripping so cross-version pairs degrade gracefully. The only breaking step in the project's history is v0.7 → v0.8 (the JSON → SQLite cutover, documented below); v0.7 never reached production.

Since v1.0 no upgrade requires a manual SQLite schema migration — the server runs embedded migrations forward automatically at startup.

Compatibility matrix

CombinationBehaviour
Newer server, older clientNew rule fields are stripped on send; the server enforces capability gates and refuses pushes the client cannot honour.
Older server, newer clientThe client transparently omits new fields under proto3 default-stripping. Behaviour identical to running the older client.
Same version both sidesFull feature surface available.

Capability gates

The server refuses ahead of time to push a feature that the client on the other end does not understand. The rule never activates anywhere — operators see an HTTP 422 with a specific code:

CodeTriggered by
unsupported_protocolUDP rule pushed to a v0.3-or-earlier client
multi_target_unsupported_by_clienttargets[] pushed to a v0.6-or-earlier client
sni_unsupported_by_clientsni_pattern pushed to a v0.8-or-earlier client
rate_limit_unsupported_by_clientrate_limit pushed to a v0.10-or-earlier client

When mixing versions across server and client, upgrade the server first. The server is the gate, so a newer server with older clients is safe; an older server with a newer client cannot light up new features but otherwise operates normally.

v0.7 → v0.8 (clean-slate)

This is the only "breaking" upgrade in the project's history — and only because v0.7 never reached production.

In v0.8 every JSON file (tokens.json, identity.json, rules.json) plus the in-memory audit ring became one SQLite database at <data-dir>/state.db.

There is no upgrade path from a v0.7 deployment that wrote JSON files. The expectation is a fresh --data-dir:

sudo systemctl stop portunus-server
sudo rm /var/lib/portunus/tokens.json /var/lib/portunus/identity.json
portunus-server reset --confirm
sudo systemctl start portunus-server

After the server starts, complete first-run Web onboarding with the setup token printed in the server logs. Existing client bundles (TLS-pinned) survive — only the operator-side identity and the rule definitions need re-creating.

v0.4 → v0.5 (RBAC)

The operator API gained Authorization: Bearer <token> requirement. Existing CLI scripts need either:

export PORTUNUS_OPERATOR_TOKEN=<token>

…or the bootstrap shortcut in server.toml:

operator_token = "<43-char URL-safe-base64 token>"

The data plane (gRPC client tokens, TCP/UDP forwarding) is byte-identical — no client-side changes required.

v1.0 → v1.1 (local Web passwords)

The Web UI moved from browser-entered bearer tokens to local user ID/password login with server-side portunus_session cookies. Existing API bearer tokens continue to work for CLI and automation. Existing users are not assigned passwords automatically; a superadmin should create or reset local passwords from the Web UI, or use local reset-password while the server is stopped.

v1.x data-plane releases

All v1.x upgrades are drop-in: no manual SQLite migration, no wire break, no operator-surface removal. Upgrade the binary and restart. Highlights operators may notice:

  • v1.3 (Linux splice(2) fast path) — TCP throughput roughly doubles on Linux for rules with no bandwidth caps. Operator-invisible; see Troubleshooting → Disabling the Linux fast path.
  • v1.4 (traffic quotas) — adds SQLite migration V008 (traffic_quotas + history tables) applied automatically. No backfill, no default rows; unconfigured pairs forward exactly as before. See the Traffic Quotas runbook.
  • v1.5 — UDP per-rule flow cap correction. udp_max_flows_per_rule is now enforced once across the whole rule instead of per listen port. A range UDP rule's effective capacity drops by a factor of range_size. If you relied on the old inflated capacity, raise udp_max_flows_per_rule proportionally or split the range; the field is capped at 65535, so if cap × range_size previously exceeded that you must split the rule. Single-port UDP rules are unchanged.
  • v1.6 — standalone stats TUI (portunus-standalone stats); no control-plane change.
  • v1.6.1 — SO_REUSEADDR on TCP listeners. Fixes spurious port_in_use after docker restart / fast process recycles while accept()-ed child sockets linger in TIME_WAIT. Splice byte counters (bytes_in / bytes_out) now update incrementally on long-lived flows instead of only at connection close.
  • v1.7 — forwarder hardening. UDP head-of-line-blocking fix, accept-loop backoff, time-boxed PROXY-protocol prelude and IP-target dials, bounded DNS cache, splice + byte counters on the multi-target failover path, and a fix for the per-rule rate limiter leaking on rule removal. All internal; no operator action.

Downgrade caveats

  • v0.5 → v0.4: identity.json becomes inert; operator_token is silently ignored; auth-layer is absent so every operator request is unauthenticated again. RBAC state is lost (re-bootstrap required on a future v0.5+ upgrade). Forwarding rules and tokens.json survive.
  • v0.8 → v0.7: not supported. v0.7 wrote JSON files; v0.8 wrote SQLite. There is no automated downgrade path.

Checking the schema version

The query returns the single highest applied migration number (refinery stores one integer per migration in schema_migrations):

sqlite3 /var/lib/portunus/state.db \
    'SELECT MAX(version) FROM schema_migrations;'
VersionFirst shipped inMigration(s)
1v0.8initial schema
2v0.9TLS SNI routing
3v0.10PROXY protocol (per-target)
4v0.10rule runtime columns
5v0.11rate limiting
6v1.1local password auth
7v1.3.1per-client entry address
8v1.4traffic quotas + history
9v1.4.2client enrollments
10v1.4.3runtime advertised endpoint

v1.5 – v1.7 add no SQLite migrations; the head stays at 10.

A newer binary auto-applies any missing migrations on startup. A binary older than the on-disk schema refuses to start with exit 78 (startup.schema_version_too_new).

On this page