Skip to main content

Crate pg_proto

Crate pg_proto 

Source
Expand description

§pg-proto

CI docs.rs crates.io MSRV License: MIT

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 S and E cannot be decoded in the wrong direction.
  • Typed pre-startup handling for SSLRequest, GSSENCRequest, CancelRequest, and StartupMessage, 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, and DataRow values 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 -- --ignored

See SUPPORTED_VERSIONS.md for the tested protocol matrix.

§PostgreSQL wire protocol documentation

The primary reference for the wire protocol is PostgreSQL’s official Frontend/Backend Protocol documentation. Its key sections are:

Compatibility note: PostgreSQL 18 introduced protocol 3.2, but most clients still negotiate version 3.0 for compatibility. Read the documentation for the oldest PostgreSQL version you intend to support, and use an established driver’s source code as an executable reference.

§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 --workspace

Live PostgreSQL tests require a Docker-compatible runtime:

cargo test --lib internal_tests::postgres_container -- --ignored

§Licence

Licensed under the MIT License.

Structs§

AllowAuthenticatedRoute
Identity authenticated-route policy.
AuthenticatedRouteContext
Borrowed facts passed to authenticated route policy.
BackendProjectionError
A backend message was not legal for any outstanding operation.
Bind
Structured Bind message.
BoundedPipeline
Configuration for a bounded frontend operation pipeline.
CancelKey
Backend cancellation credentials captured during startup.
CancellationRequest
A decoded out-of-band PostgreSQL cancellation request.
CancellationRoute
A destination and upstream key retained independently of startup routing.
Client
Reusable client-role component.
ClientBuilder
Ordinary generic builder for a reusable client-role component.
ClientConnection
Operational client-role connection.
ClientConnectionContext
Immutable facts retained by a client-role connection.
ClientInitialContext
Immutable facts available when a client middleware handler is created.
ClientTlsConfig
Application-owned TLS material resolved afresh for a connection attempt.
Close
Structured Close message.
ConnectTarget
Application-defined destination supplied to the connector.
CopyResponse
Format metadata which begins a COPY sub-protocol.
DataRow
One result row retaining raw text or binary column values.
Describe
Structured Describe message.
DiagnosticField
One tagged PostgreSQL diagnostic field.
DiagnosticResponse
Ordered diagnostic fields from an error or notice response.
DisabledServerTls
Explicit plaintext-only server TLS policy.
Execute
Structured Execute message.
FieldDescription
Metadata for one result column.
FunctionCall
Structured legacy FunctionCall message.
IdentityHandler
Identity middleware handler.
IdentityIntermediaryMiddleware
Identity forwarding-boundary middleware.
IdentityMiddleware
Default factory and handler: messages pass through unchanged.
IdentityServerHandler
Identity handler used until contextual middleware is configured.
InitialServerContext
Immutable server-side facts available before authentication begins.
Intermediary
A reusable operational intermediary configuration.
IntermediaryBuilder
Progressive builder for Intermediary.
IntermediaryConnection
One operational, independently authenticated intermediary session.
IntermediaryContexts
Both role contexts recovered during deliberate intermediary teardown.
MiddlewareChain
Two factories/handlers composed in builder declaration order.
NegotiateProtocolVersion
Backend response negotiating a requested protocol minor version and options.
NoPipeline
Pipeline policy which preserves the historical lock-step behaviour.
NoServerIdentity
Error returned by the marker provider, which has no identity.
NoServerIdentityProvider
Marker provider used by the disabled TLS policy.
OptionalServerTls
Optional server TLS termination backed by a reloadable identity provider.
Parse
Structured Parse message.
PipelineConfigError
A zero operation-count limit is not a usable pipeline configuration.
ProtocolLimitError
Invalid protocol limit configuration.
ProtocolLimits
Conservative protocol allocation limits.
ProtocolVersion
PostgreSQL protocol version, including supported 3.x minor versions.
QueryError
Failure while executing an operational client-role action.
RejectCancellation
Marker registry used by explicit cancellation rejection.
ReloadableClientTls
A libpq-compatible policy backed by application-owned reloadable TLS material.
RequiredServerTls
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.
ServerAuthenticationRequest
Immutable inputs available to one authentication session.
ServerBuilder
Builder for a reusable Server.
ServerCancellation
A cancellation branch retaining all caller and handler ownership.
ServerConnection
An operational server-role connection with all per-connection ownership.
ServerConnectionContext
Immutable facts known about a client-facing connection.
ServerIdentity
A reloadable server identity resolved for each TLS connection.
ServerProtocolLimits
Conservative allocation limits applied to newly accepted transports.
ServerTlsPolicy
Namespace for explicit server-side TLS policy values.
SslStrategy
Actions needed to apply an SslMode across connection attempts.
StartupMessage
A frontend startup message, retained as bytes for lossless proxy forwarding.
StartupParameters
Structured startup fields and extension parameters.
TrustClientAuthentication
Explicit client authentication policy which accepts only AuthenticationOk.
TrustIdentity
Typed evidence for an explicitly trusted connection.
TrustServerAuthentication
Explicit trust authentication, which accepts every protocol-compatible client.

Enums§

AcceptError
Failures while establishing a live server-role connection.
AcceptedServerTransport
Transport recovered when a server connection is explicitly torn down.
Authentication
Backend authentication request or continuation message.
BackendMessage
Messages sent by a PostgreSQL backend.
BuildError
A deterministic client component configuration failure.
BuildServerError
Deterministic failures while constructing a reusable server component.
CancelError
Failure while sending a one-shot PostgreSQL cancellation packet.
CancellationPolicy
Required posture for out-of-band cancellation connections.
CertificateVerification
Peer-certificate checks required by an SSL mode.
ClientAuthenticationChallenge
An authentication request offered by a PostgreSQL server.
ClientAuthenticationError
Failure while running an application authentication policy.
ClientAuthenticationResponse
An application authentication policy’s wire response.
ClientTlsError
Failure while resolving or establishing client TLS.
ClientTlsPolicy
Explicit libpq-compatible TLS policy and its reloadable configuration provider.
ClientTlsStatus
Progressively discovered transport security for a client connection.
ClientTransport
Transport selected by libpq-compatible negotiation.
ConnectError
Failure while establishing a client-role connection, distinct from BuildError.
ConnectionChanged
Evidence that an operation may have changed session-local state.
ConnectionClean
Evidence that a connection has not performed a state-changing operation.
DescribeTarget
Namespace selected by Describe and Close messages.
EstablishmentFailurePolicy
Disclosure-safe handling for failures after a downstream connection exists.
ForwardError
Operational forwarding or pipeline projection failure.
ForwardedMessage
Direction selected by one cancellation-safe duplex forwarding step.
FrontendMessage
Frontend messages whose contents a rewriting proxy must retain structurally.
FrontendProjectionError
Why a frontend message could not be accepted.
IntermediaryAccept
Result of accepting either an ordinary session or an out-of-band request.
IntermediaryAcceptError
Failure while establishing both independently authenticated roles.
IntermediaryBuildError
Deterministic failure while assembling an intermediary component.
NegotiatedServerTls
TLS facts recorded after pre-startup negotiation.
PreStartupMessage
The external choice occupying a new connection’s untagged first packet.
ServerAccept
Result of accepting one caller-established transport.
ServerAuthenticationAction
The next protocol action selected by application authentication policy.
ServerAuthenticationResponse
Owned client response supplied to application authentication policy.
SslMode
libpq-compatible TLS negotiation policy.
StartupParameterError
Invalid structured startup configuration.
StartupResolutionError
Failure while decoding or resolving a startup route.
TransactionStatus
Transaction state reported by ReadyForQuery.

Traits§

AuthenticatedRoutePolicy
Optional policy that validates or refines a destination after authentication.
ClientAuthentication
Factory for asynchronous, fallible per-connection authentication sessions.
ClientAuthenticationSession
Mutable authentication policy state owned by one connection attempt.
ClientMiddleware
Middleware for a PostgreSQL client role. Implement only the directions used.
ClientTlsConfiguration
Internal shape shared by disabled and reloadable TLS policies.
ClientTlsProvider
Application-owned source of reloadable client TLS material.
IntermediaryCancellationRegistry
Application-owned concurrent cancellation mapping and key allocator.
IntermediaryMiddleware
Middleware at the forwarding boundary between the two role components.
IntermediaryMiddlewareFactory
Creates fresh forwarding-boundary middleware for one established pair.
MiddlewareFactory
Creates one isolated handler synchronously for a new connection.
PipelinePolicy
Sealed configuration accepted by the intermediary builder.
ServerAuthentication
Per-connection asynchronous authentication policy.
ServerAuthenticationProvider
Factory creating one isolated authentication policy per connection.
ServerIdentityProvider
Application-owned source of the current TLS identity.
ServerMiddleware
Middleware for a PostgreSQL server role. Implement only the directions used.
StartupRouteResolver
Required asynchronous startup routing policy.

Type Aliases§

ClientAuthenticationFuture
Boxed future used by application-defined authentication policies.
ServerAcceptFuture
Non-Send future returned while accepting one server-role connection.
ServerAuthenticationFuture
A non-Send future returned by application-defined server authentication.