StreamHost¶
Raw TCP/UDP forwarding. gpm opens a proxy listener per listenPort (TCP, UDP,
or both) and forwards to the backend; listeners are reconciled on every reload
(ports added/removed, backend swapped, with no listener restart for unchanged
ports). UDP uses per-client sessions with an idle timeout.
A TCP stream can additionally be TLS-aware (tls): SNI-routed so several
hosts share one port, either passed through untouched or terminated at gpm. And
it can be gated at L4 (accessLists) on the client IP, evaluated before any
backend is dialled.
| Field | Type | Required | Notes |
|---|---|---|---|
listenPort |
int | yes | 1-65535. Publish this port from the container (compose ports:) so it is reachable, and avoid colliding with the proxy listener ports 80/443 or the admin port: a bind failure is logged and that one port is skipped, never fatal. |
protocol |
string | yes | tcp|udp|both. |
target |
StreamTarget | yes | The backend this port forwards to: {host, port}. Mirrors upstream's vocabulary minus the scheme: a raw stream carries an arbitrary protocol, so http/https means nothing here. |
tls |
StreamTLS | no | SNI routing and/or TLS termination. TCP only. |
accessLists |
[]string | no | L4 access lists evaluated on the client IP (below). |
name: postgres
listenPort: 5432
protocol: tcp
target:
host: db.internal
port: 5432
accessLists: [lan-only]
Removed fields¶
| Field | Status | Migration |
|---|---|---|
forwardHost |
Removed. Decoded only so the loader can reject it loudly. | Replace the pair with target: {host, port}. |
forwardPort |
Removed, same. | Same. |
A file still carrying either key is rejected at load with an error naming the new shape: it is never silently accepted with no backend. See Upgrading.
StreamTarget (target)¶
| Field | Type | Required | Notes |
|---|---|---|---|
host |
string | yes | Backend host. |
port |
int | yes | 1-65535. |
L4 access lists (accessLists)¶
A stream host may reference AccessList objects, exactly like a proxy host. Only
the IP/CIDR rules and the geo rules are evaluated: basic auth is an HTTP
challenge/response with nowhere to live in a raw stream, so referencing a list
that has (deprecated) basicAuth users is rejected at validation rather than
silently half-applied; moving those users to an auth middleware with
mode: basic removes the conflict, since that middleware is HTTP-only and is
never attached to a stream host. All referenced lists must allow, and the check runs before any
backend is dialled, so a denied client cannot make gpm open a socket to the
backend at all.
For UDP the list is evaluated once per session (the first packet from a source); a denied source creates no session and no upstream socket. Geo rules follow the same fail-closed rule as HTTP: with geo configured and no GeoIP database loaded, the port denies.
Behind an L4 balancer, enable settings.proxyProtocol, otherwise every
connection looks like it came from the balancer and the rules match the wrong
address.
The maxUDPSessions bound (4096 per listener) still caps spoofed-source UDP
memory independently of any access list.
StreamTLS (tls)¶
Makes a TCP stream port TLS-aware: several hosts can share one port, separated by the SNI in the ClientHello, and gpm can either forward the handshake untouched or terminate it.
| Field | Type | Required | Notes |
|---|---|---|---|
mode |
string | yes | passthrough | terminate. |
sniMatch |
[]string | see below | Server names this host claims. Exact (db.example.com) or a single-label wildcard (*.example.com). |
certificateRef |
string | terminate only | Names a Certificate. Required in terminate, forbidden in passthrough. |
passthroughpeeks the ClientHello (a bounded, stdlib-only parse of the record, handshake andserver_nameextension), routes on the SNI, and replays the peeked bytes to the backend. gpm never decrypts and never needs the key: the backend terminates, end to end.terminatecompletes the handshake at gpm withcertificateReffrom the normal certificate store (custom or ACME-issued) and forwards plaintext to the backend. The floor is TLS 1.2 with the same AEAD cipher suites the HTTPS listener uses, or TLS 1.3 whensettings.tls.minVersionraises it (a stream host has no per-host floor of its own). No ALPN is offered: what rides inside a stream is an arbitrary TCP protocol.
Port sharing. Two or more enabled stream hosts may share a TCP listenPort
only if every one of them sets sniMatch: that is the only thing that tells
their connections apart. Validation rejects a mixed or duplicate claim, so
routing can never fall back to "whichever host compiled last". A host alone on its
port may omit sniMatch and take every connection. On an SNI-routed port, a
connection whose server name no host claims (or that sends no SNI) is closed
rather than handed to an arbitrary backend, and a connection that is not TLS at
all is closed too.
UDP. tls requires protocol: tcp. A UDP datagram carries no ClientHello, so
udp and both are rejected at validation; two hosts can never share a UDP port
either.
# Two Postgres instances behind one public 5432, separated by SNI, never decrypted.
name: pg-blue
listenPort: 5432
protocol: tcp
target:
host: pg-blue.internal
port: 5432
accessLists: [lan-only]
tls:
mode: passthrough
sniMatch: [blue.db.example.com]
---
# TLS terminated at gpm, plaintext to a backend that speaks none.
name: mqtt
listenPort: 8883
protocol: tcp
target:
host: mosquitto.internal
port: 1883
tls:
mode: terminate
sniMatch: [mqtt.example.com]
certificateRef: wildcard