Expand description
§pg-proto
pg-proto is an asynchronous Rust implementation of the PostgreSQL
frontend/backend wire protocol designed for proxies, poolers, gateways, drivers,
and protocol-aware test infrastructure.
Its distinguishing feature is a builder-only facade over PostgreSQL’s typed
connection state machine. Client::builder(), Server::builder(), and
Intermediary::builder() require explicit transport-security, authentication,
middleware, and routing policy before they establish operational connections.
The internal protocol typestates prevent illegal sequencing without exposing
the implementation graph as application API.
Most PostgreSQL protocol libraries decode messages but retain the session phase
in a runtime enum. pg-proto is useful when protocol correctness is part of the
architecture rather than merely an implementation detail: the legal next
operations are visible in function signatures, illegal compositions are rejected
at compile time, and proxy policy can still inspect, replace, or reject complete
typed messages.
§What it provides
- Direction-parameterised frontend and backend codecs. Ambiguous tags such as
SandEcannot be decoded in the wrong direction. - Typed pre-startup handling for
SSLRequest,GSSENCRequest,CancelRequest, andStartupMessage, including transport-changing rustls upgrades. - A plain/client-TLS/server-TLS network stream, configurable TCP socket options, and outbound connection retry with capped exponential backoff.
- Independent client-facing and upstream authentication sessions, including cleartext, MD5, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS with channel binding.
- Simple and extended query sessions, pipelining, error draining, function calls, COPY IN/OUT/BOTH, and physical replication framing.
- Lossless, reconstructable
Parse,Bind,Describe,Execute,RowDescription, andDataRowvalues for SQL and result rewriting. - A demultiplexer for asynchronous notices, notifications, and parameter status updates without polluting the causal session type.
- Positionally tagged notices and transaction/parameter evidence for pooling decisions.
- Connection-branded prepared statements and portals with name rewriting.
- Exact typestate erasure and checked re-entry at storage and pool boundaries.
- A protocol grammar macro which emits typestates, their duals, a runtime FSM for differential testing, and railroad diagrams embedded in rustdoc.
The crate owns protocol representation and ordering. Applications retain control of listeners, credentials, authorisation, SQL transformation, routing, pooling, cancellation storage, telemetry, and failure policy.
§Why use it?
PostgreSQL infrastructure tends to fail at phase boundaries rather than while
decoding an individual frame. A pooler may return a connection while it is still
in a transaction, a proxy may forward Query while a COPY exchange is active,
or an extended-query error path may forget to discard messages until Sync.
Typestate makes these transitions explicit and turns many such bugs into type
errors.
The phase index is orthogonal to connection cleanliness. A connection can be
protocol-ready but unsuitable for unconditional pool release because of an open
transaction, changed GUC, prepared statement, portal, LISTEN, or advisory lock.
Operational connections return explicit state evidence and preserve caller-owned
state until teardown.
§What can be built with it?
The bounded intermediary pipeline example shows ordered forwarding, local interception, and backpressure without proxy-owned message queues.
- A TLS-terminating PostgreSQL proxy which authenticates each side independently and inspects plaintext SQL and result rows.
- A transaction or session pooler whose release policy consumes explicit protocol and cleanliness evidence.
- A SQL firewall, audit gateway, query rewriter, or column-encryption proxy.
- A sharding/router layer which rewrites prepared-statement and portal names.
- A logical or physical replication relay with typed COPY-BOTH half-closes.
- A PostgreSQL-compatible server, mock backend, recorder, replay tool, or protocol conformance harness.
- A driver or administrative client which benefits from compile-time sequencing.
§Security choices come first
Every role builder requires explicit TLS and authentication policy. The short
examples below deliberately use plaintext (ClientTlsPolicy::Disabled and
ServerTlsPolicy::Disabled) and unverified trust authentication
(TrustClientAuthentication and TrustServerAuthentication) so that their
insecure posture is visible in code. They are suitable for a protected local
development network, not an Internet-facing production deployment.
For production, use ClientTlsPolicy::libpq with SslMode::VerifyFull and an
application-owned reloadable ClientTlsProvider; use ServerTlsPolicy::Required
with an application-owned ServerIdentityProvider. Supply application-defined
ClientAuthentication and ServerAuthenticationProvider implementations that
return typed identity evidence. pg-proto orchestrates the protocol but remains
policy-neutral: it does not store credentials, authorise identities, choose
authentication mechanisms, or provision certificates.
The default one-mebibyte frame limits are conservative. Raising a limit or
calling ProtocolLimits::without_frame_limit is an explicit resource-exhaustion
downgrade; production services should instead choose the smallest limit their
workload needs. Likewise, SslMode::Allow, Prefer, Require, and VerifyCa
provide less assurance than VerifyFull, and must be selected deliberately.
§Client: connect to PostgreSQL
Build one reusable upstream-facing component, then establish operational connections with caller-owned state and per-call startup parameters.
use pg_proto::{
Client, ClientTlsPolicy, ConnectTarget, StartupParameters,
TrustClientAuthentication,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.connector(|target| {
let address = target.name().to_owned();
async move { tokio::net::TcpStream::connect(address).await }
})
// Development-only: plaintext transport and no credential exchange.
.tls(ClientTlsPolicy::Disabled)
.authentication(TrustClientAuthentication)
.startup_parameters(StartupParameters::new("application"))
.build()?;
let connection = client
.connect(
ConnectTarget::new("127.0.0.1:5432"),
StartupParameters::default().database("postgres"),
Vec::<String>::new(),
)
.await?;
let (_transport, state, _middleware, context) = connection.into_parts();
assert!(state.is_empty());
assert_eq!(context.target().name(), "127.0.0.1:5432");
Ok(())
}§Server: accept PostgreSQL clients
The server builder owns reusable client-facing policy. The application owns the listener, peer metadata, per-connection state, credentials, and authorisation.
use pg_proto::{Server, ServerAccept, ServerTlsPolicy, TrustServerAuthentication};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = Server::builder()
// Development-only: clients are neither encrypted nor authenticated.
.tls(ServerTlsPolicy::Disabled)
.authentication(TrustServerAuthentication)
.build()?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:6432").await?;
let (transport, peer) = listener.accept().await?;
match server.accept(transport, peer, Vec::<String>::new()).await? {
ServerAccept::Session(connection) => {
println!("accepted user {:?}", connection.startup().parameters.get(b"user".as_slice()));
let (_transport, _state, _middleware, _context) = connection.teardown();
}
ServerAccept::Cancellation(cancellation) => {
println!("cancel process {}", cancellation.request().process_id());
}
}
Ok(())
}§Intermediary: compose both roles
An intermediary takes complete server and client components. Startup routing, authenticated routing, cancellation storage, middleware, and failure disclosure remain explicit application policies.
use std::{convert::Infallible, future::Future, pin::Pin};
use pg_proto::{
CancellationPolicy, Client, ClientTlsPolicy, ConnectTarget, InitialServerContext,
Intermediary, Server, ServerTlsPolicy, StartupParameters, StartupRouteResolver,
TrustClientAuthentication, TrustServerAuthentication,
};
struct Route;
impl<Peer> StartupRouteResolver<Peer> for Route {
type Error = Infallible;
fn resolve<'a>(
&'a self,
_: StartupParameters,
_: InitialServerContext<'a, Peer>,
) -> Pin<Box<dyn Future<Output = Result<ConnectTarget, Self::Error>> + 'a>> {
Box::pin(async { Ok(ConnectTarget::new("127.0.0.1:5432")) })
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = Server::builder()
.tls(ServerTlsPolicy::Disabled) // Development-only plaintext/trust.
.authentication(TrustServerAuthentication)
.build()?;
let client = Client::builder()
.connector(|target| {
let address = target.name().to_owned();
async move { tokio::net::TcpStream::connect(address).await }
})
.tls(ClientTlsPolicy::Disabled) // Development-only plaintext/trust.
.authentication(TrustClientAuthentication)
.build()?;
let intermediary = Intermediary::builder()
.server(server)
.client(client)
.startup_resolver(Route)
.cancellation(CancellationPolicy::Reject)
.build()?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:6432").await?;
let (transport, peer) = listener.accept().await?;
let mut connection = Box::pin(intermediary.accept(transport, peer, ()))
.await?
.into_session();
while !matches!(
connection.forward_next().await?,
pg_proto::ForwardedMessage::Frontend(pg_proto::FrontendMessage::Terminate)
) {}
Ok(())
}§Stateful message middleware
A proxy can inspect or replace owned frontend and backend messages through
builder middleware. Each accepted connection gets a fresh handler with mutable
access to the application-supplied per-connection state. Repeated .middleware
calls compose stages in declaration order.
use pg_proto::{
Client, ClientConnectionContext, ClientInitialContext, ClientMiddleware, ClientTlsPolicy,
FrontendMessage, TrustClientAuthentication,
};
#[derive(Clone, Copy)]
struct CountQueries;
impl ClientMiddleware<usize, ClientConnectionContext> for CountQueries {
fn frontend(
&mut self,
_context: &ClientConnectionContext,
count: &mut usize,
message: FrontendMessage,
) -> FrontendMessage {
if matches!(message, FrontendMessage::Query(_)) {
*count += 1;
}
message
}
}
let _client = Client::builder()
.connector(|_| async { Ok::<_, std::io::Error>(()) })
.tls(ClientTlsPolicy::Disabled)
.authentication(TrustClientAuthentication)
.middleware(|_: &ClientInitialContext| CountQueries)
.build()?;For a complete networked example, see the TLS-terminating
SQL logging proxy. The companion
protocol logging proxy prints every
decoded message in both directions. More focused examples live in the
examples/ directory, including message rewriting and the neutral
proxy composition boundary.
§Rustdoc entry point
The crate overview documents the complete root facade: role builders, nested security configuration, middleware, operational connection types, and root message vocabulary.
Build the same documentation locally with:
cargo doc --workspace --no-deps --open§Supported PostgreSQL versions
PostgreSQL 14, 15, 16, 17, and 18 are supported. Each version runs the same live suite against its official Alpine image. PostgreSQL 14–17 negotiate a requested protocol 3.2 startup down to 3.0; PostgreSQL 18 reports protocol 3.2. Both behaviours are covered explicitly.
Run a selected version locally with a Docker-compatible runtime:
PG_PROTO_POSTGRES_VERSION=18 \
cargo test --lib internal_tests::postgres_container -- --ignoredSee SUPPORTED_VERSIONS.md for the tested protocol
matrix.
§Known limitations
- The API is pre-1.0 and may change as it is integrated into a production proxy.
- Kerberos V5, GSSAPI, SSPI, and GSS token exchanges are represented by the protocol API, but the crate does not ship platform credential-provider engines. GSS encryption negotiation is modelled; a production GSSENC transport adapter remains application work.
- Pool scheduling, routing, SQL parsing, policy, credential storage, certificate provisioning, and cancellation-key persistence are intentionally not included.
- Rust is affine rather than linear: callers can deliberately abandon an operational connection by dropping it.
- Unknown future PostgreSQL message tags are rejected by the typed codec until their direction and semantics are added.
- Formal multiparty verification is not provided. Client and server roles are dual generated APIs with differential runtime-FSM testing, not a machine-checked proof of a complete three-party proxy.
Security assumptions and downstream responsibilities are documented in
SECURITY.md. The audited proxy capability boundary is in
PROXY_COMPATIBILITY.md, and migration from a
runtime-enum implementation is covered by MIGRATION.md.
Contribution instructions and community expectations are in
CONTRIBUTING.md and
CODE_OF_CONDUCT.md.
§Verification
The ordinary suite includes unit, fixture, property-style differential, compile-fail, and documentation tests:
cargo test --workspaceLive PostgreSQL tests require a Docker-compatible runtime:
cargo test --lib internal_tests::postgres_container -- --ignored§Licence
Licensed under the MIT License.
Structs§
- Allow
Authenticated Route - Identity authenticated-route policy.
- Authenticated
Route Context - Borrowed facts passed to authenticated route policy.
- Backend
Projection Error - A backend message was not legal for any outstanding operation.
- Bind
- Structured
Bindmessage. - Bounded
Pipeline - Configuration for a bounded frontend operation pipeline.
- Cancel
Key - Backend cancellation credentials captured during startup.
- Cancellation
Request - A decoded out-of-band PostgreSQL cancellation request.
- Cancellation
Route - A destination and upstream key retained independently of startup routing.
- Client
- Reusable client-role component.
- Client
Builder - Ordinary generic builder for a reusable client-role component.
- Client
Connection - Operational client-role connection.
- Client
Connection Context - Immutable facts retained by a client-role connection.
- Client
Initial Context - Immutable facts available when a client middleware handler is created.
- Client
TlsConfig - Application-owned TLS material resolved afresh for a connection attempt.
- Close
- Structured
Closemessage. - Connect
Target - Application-defined destination supplied to the connector.
- Copy
Response - Format metadata which begins a COPY sub-protocol.
- DataRow
- One result row retaining raw text or binary column values.
- Describe
- Structured
Describemessage. - Diagnostic
Field - One tagged
PostgreSQLdiagnostic field. - Diagnostic
Response - Ordered diagnostic fields from an error or notice response.
- Disabled
Server Tls - Explicit plaintext-only server TLS policy.
- Execute
- Structured
Executemessage. - Field
Description - Metadata for one result column.
- Function
Call - Structured legacy
FunctionCallmessage. - Identity
Handler - Identity middleware handler.
- Identity
Intermediary Middleware - Identity forwarding-boundary middleware.
- Identity
Middleware - Default factory and handler: messages pass through unchanged.
- Identity
Server Handler - Identity handler used until contextual middleware is configured.
- Initial
Server Context - Immutable server-side facts available before authentication begins.
- Intermediary
- A reusable operational intermediary configuration.
- Intermediary
Builder - Progressive builder for
Intermediary. - Intermediary
Connection - One operational, independently authenticated intermediary session.
- Intermediary
Contexts - Both role contexts recovered during deliberate intermediary teardown.
- Middleware
Chain - Two factories/handlers composed in builder declaration order.
- Negotiate
Protocol Version - Backend response negotiating a requested protocol minor version and options.
- NoPipeline
- Pipeline policy which preserves the historical lock-step behaviour.
- NoServer
Identity - Error returned by the marker provider, which has no identity.
- NoServer
Identity Provider - Marker provider used by the disabled TLS policy.
- Optional
Server Tls - Optional server TLS termination backed by a reloadable identity provider.
- Parse
- Structured
Parsemessage. - Pipeline
Config Error - A zero operation-count limit is not a usable pipeline configuration.
- Protocol
Limit Error - Invalid protocol limit configuration.
- Protocol
Limits - Conservative protocol allocation limits.
- Protocol
Version PostgreSQLprotocol version, including supported 3.x minor versions.- Query
Error - Failure while executing an operational client-role action.
- Reject
Cancellation - Marker registry used by explicit cancellation rejection.
- Reloadable
Client Tls - A libpq-compatible policy backed by application-owned reloadable TLS material.
- Required
Server Tls - Required server TLS termination backed by a reloadable identity provider.
- RowDescription
- Backend row metadata retained in reconstructable form.
- Server
- Reusable client-facing PostgreSQL server component.
- Server
Authentication Request - Immutable inputs available to one authentication session.
- Server
Builder - Builder for a reusable
Server. - Server
Cancellation - A cancellation branch retaining all caller and handler ownership.
- Server
Connection - An operational server-role connection with all per-connection ownership.
- Server
Connection Context - Immutable facts known about a client-facing connection.
- Server
Identity - A reloadable server identity resolved for each TLS connection.
- Server
Protocol Limits - Conservative allocation limits applied to newly accepted transports.
- Server
TlsPolicy - Namespace for explicit server-side TLS policy values.
- SslStrategy
- Actions needed to apply an
SslModeacross connection attempts. - Startup
Message - A frontend startup message, retained as bytes for lossless proxy forwarding.
- Startup
Parameters - Structured startup fields and extension parameters.
- Trust
Client Authentication - Explicit client authentication policy which accepts only
AuthenticationOk. - Trust
Identity - Typed evidence for an explicitly trusted connection.
- Trust
Server Authentication - Explicit trust authentication, which accepts every protocol-compatible client.
Enums§
- Accept
Error - Failures while establishing a live server-role connection.
- Accepted
Server Transport - Transport recovered when a server connection is explicitly torn down.
- Authentication
- Backend authentication request or continuation message.
- Backend
Message - Messages sent by a
PostgreSQLbackend. - Build
Error - A deterministic client component configuration failure.
- Build
Server Error - Deterministic failures while constructing a reusable server component.
- Cancel
Error - Failure while sending a one-shot PostgreSQL cancellation packet.
- Cancellation
Policy - Required posture for out-of-band cancellation connections.
- Certificate
Verification - Peer-certificate checks required by an SSL mode.
- Client
Authentication Challenge - An authentication request offered by a PostgreSQL server.
- Client
Authentication Error - Failure while running an application authentication policy.
- Client
Authentication Response - An application authentication policy’s wire response.
- Client
TlsError - Failure while resolving or establishing client TLS.
- Client
TlsPolicy - Explicit libpq-compatible TLS policy and its reloadable configuration provider.
- Client
TlsStatus - Progressively discovered transport security for a client connection.
- Client
Transport - Transport selected by libpq-compatible negotiation.
- Connect
Error - Failure while establishing a client-role connection, distinct from
BuildError. - Connection
Changed - Evidence that an operation may have changed session-local state.
- Connection
Clean - Evidence that a connection has not performed a state-changing operation.
- Describe
Target - Namespace selected by
DescribeandClosemessages. - Establishment
Failure Policy - Disclosure-safe handling for failures after a downstream connection exists.
- Forward
Error - Operational forwarding or pipeline projection failure.
- Forwarded
Message - Direction selected by one cancellation-safe duplex forwarding step.
- Frontend
Message - Frontend messages whose contents a rewriting proxy must retain structurally.
- Frontend
Projection Error - Why a frontend message could not be accepted.
- Intermediary
Accept - Result of accepting either an ordinary session or an out-of-band request.
- Intermediary
Accept Error - Failure while establishing both independently authenticated roles.
- Intermediary
Build Error - Deterministic failure while assembling an intermediary component.
- Negotiated
Server Tls - TLS facts recorded after pre-startup negotiation.
- PreStartup
Message - The external choice occupying a new connection’s untagged first packet.
- Server
Accept - Result of accepting one caller-established transport.
- Server
Authentication Action - The next protocol action selected by application authentication policy.
- Server
Authentication Response - Owned client response supplied to application authentication policy.
- SslMode
- libpq-compatible TLS negotiation policy.
- Startup
Parameter Error - Invalid structured startup configuration.
- Startup
Resolution Error - Failure while decoding or resolving a startup route.
- Transaction
Status - Transaction state reported by
ReadyForQuery.
Traits§
- Authenticated
Route Policy - Optional policy that validates or refines a destination after authentication.
- Client
Authentication - Factory for asynchronous, fallible per-connection authentication sessions.
- Client
Authentication Session - Mutable authentication policy state owned by one connection attempt.
- Client
Middleware - Middleware for a PostgreSQL client role. Implement only the directions used.
- Client
TlsConfiguration - Internal shape shared by disabled and reloadable TLS policies.
- Client
TlsProvider - Application-owned source of reloadable client TLS material.
- Intermediary
Cancellation Registry - Application-owned concurrent cancellation mapping and key allocator.
- Intermediary
Middleware - Middleware at the forwarding boundary between the two role components.
- Intermediary
Middleware Factory - Creates fresh forwarding-boundary middleware for one established pair.
- Middleware
Factory - Creates one isolated handler synchronously for a new connection.
- Pipeline
Policy - Sealed configuration accepted by the intermediary builder.
- Server
Authentication - Per-connection asynchronous authentication policy.
- Server
Authentication Provider - Factory creating one isolated authentication policy per connection.
- Server
Identity Provider - Application-owned source of the current TLS identity.
- Server
Middleware - Middleware for a PostgreSQL server role. Implement only the directions used.
- Startup
Route Resolver - Required asynchronous startup routing policy.
Type Aliases§
- Client
Authentication Future - Boxed future used by application-defined authentication policies.
- Server
Accept Future - Non-
Sendfuture returned while accepting one server-role connection. - Server
Authentication Future - A non-
Sendfuture returned by application-defined server authentication.