Architecture¶
The TCP proxy sits between a client and the upstream host the client is trying to reach. It inspects only the opening bytes of each connection, asks the simulator whether the host is allowed for this client, and then either tunnels the connection through or refuses it.
Role in Platform Filter¶
The proxy is the enforcement point for HTTP and HTTPS traffic, but it only sees a connection when the DNS layer steers the client to it. The filter engine in the DNS server answers selected lookups with the proxy's IP instead of the real one, so the client opens its connection to the proxy rather than to the host directly. The proxy does not make policy itself. The simulator owns the policy, and the proxy asks it for a decision on every connection. Allowed traffic is forwarded to the same upstream host the client originally asked for, and the proxy is transparent to that upstream because TLS is never terminated.
Connection flow¶
A single connection moves through a fixed sequence of stages. The happy path:
flowchart LR
C[Client] -- initial bytes --> P[pdns-pf-proxy-tcp]
P -- simulate --> S[Simulator]
S -- allow / block --> P
P -- resolve + dial --> U[(Upstream host)]
P -- tunnel both ways --> U
- inspect — read the first HTTP request headers or the TLS ClientHello to
determine the target host. Bounded by
tcp.initial_read_timeout. - decision — send the host and client IP to the simulator and wait for an
allow or block answer. Bounded by
decision.timeout. - dns — resolve the host. Bounded by
tcp.dial_timeout. - dial — open a TCP connection to the upstream host. The proxy tries
multiple resolved addresses before giving up. Bounded by
tcp.dial_timeout. - proxy — copy bytes in both directions until either side closes. The buffered initial bytes are replayed to the upstream first, so the upstream sees an unmodified stream.
- closing — the connection is torn down and a summary is logged.
These stage names appear in the per-connection logs and in the periodic activity stats, so an operator can see where time was spent or where a connection is stuck. See Operations → Observability.
When a connection is blocked, the behaviour depends on the protocol: an HTTP listener returns an HTML error page with a proper status code, while an HTTPS listener simply closes the connection because TLS was never terminated and a block page could not be presented.
Decision backends¶
The proxy consults one decision backend, chosen with the decision.backend
setting. There is no fallback between backends and no support for more than one
at a time.
nats(default) — sends decisions over the simulator's NATS service. This is the standard production transport. It requires a NATS URL; the proxy refuses to start without one.http— calls the simulator's REST API directly at/api/v1/simulate. Useful when migrating from the OpenResty proxy path or when NATS is not available. It requires a simulator HTTP URL.none— disables remote filtering. Every connection is allowed and tunnelled straight through. Use this when you want a plain proxy with no policy.
See Configuration → decision for the full set of backend options.
Fail-open and fail-closed¶
When the decision backend errors or times out, the per-listener fail_open
setting decides what happens:
- Fail-closed (
fail_open: false, the default) — the connection is refused. A backend outage stops traffic. - Fail-open (
fail_open: true) — the connection is allowed through without a decision. A backend outage degrades to an unfiltered proxy rather than blocking everyone.
Each listener sets this independently, so a single instance can fail open on one listener and fail closed on another.
PROXY protocol¶
HTTP and HTTPS listeners can require an HAProxy PROXY protocol v1 header before the first HTTP request or TLS ClientHello. The header carries the real client source IP, which the proxy then uses for the decision and for logging. PROXY protocol listeners use a separate bind address from the plain listeners so an upstream load balancer can speak PROXY protocol while direct connections still work on the regular ports. PROXY protocol is not supported on SOCKS5 listeners.
Core concepts¶
These terms are used across the configuration and operations pages.
- Listener. One bind address with a protocol (
http,https, orsocks5) and its own routing options. An instance can run any number of listeners, including several of the same protocol. - Protocol. Selects how the opening bytes are inspected and which upstream port is used by default (80 for HTTP, 443 for HTTPS).
- Upstream / remote port. The TCP port the proxy connects to on the resolved
host. Defaults follow the protocol and can be overridden per listener with
remote_port. - vhost and profile. Optional static values a listener attaches to every decision request, letting the simulator apply a specific policy context for that listener's traffic.
- Decision / simulator. The remote policy lookup and the service that answers it. The decision is made once per connection.
- Request ID. A short identifier assigned to each connection. It appears in every log line for that connection so one session can be followed end to end.
- Pause. A runtime command that closes a subscriber's live sessions and optionally rejects new ones for a short window. See Pause control.