Conduit logo

Rust · Postgres v3 wire protocol

See a query
on the wire.

Conduit is a from-scratch PostgreSQL driver you can read end to end. It speaks the real v3 wire protocol over a plain TCP socket, from the startup handshake to typed result rows, with no ORM, no libpq, no C bindings, and zero external dependencies. Even the crypto is hand-written: MD5, SHA-256, HMAC, PBKDF2, and base64 all live in the tree so SCRAM-SHA-256 needs nothing from outside. This page ports that codec to JavaScript, so every byte you see is the byte the Rust driver puts on the wire.

Open the visualizer View on GitHub
How to use this playground

Pick a session preset (a connect, an authenticated query, an error) with the segmented control, then press Step to walk the exchange one message at a time, or Reveal all to see the whole session. Each step shows the exact bytes on the wire and the decoded meaning, so you can trace a query from raw frames up to typed result rows.

every byte below is produced by the same codec the Rust driver runs, ported to JavaScript, live in your browser

Every byte, cross-checked

The hard part of a wire-protocol driver is proving it without standing up a server. Conduit does it two ways, both run by cargo test: golden byte vectors assert each message against the exact bytes it must produce, and an in-process mock server on a real TcpListener runs the actual driver over TCP through a full session.

# cargo test, no database needed
golden byte vectors   every FE/BE message asserted byte for byte; decode(encode(x)) == x
mock server over TCP   handshake, MD5 + SCRAM-SHA-256 (right pw ok, wrong pw fails,
                       bad server signature fails), typed rows including a NULL,
                       parameterized query round-trip, multi-statement result sets,
                       ErrorResponse -> Error::Db { code: 42601, .. }

In the extended protocol a parameter travels in the Bind message as text, so a value like robert'); DROP TABLE students;-- is always data and never SQL. And a server-declared frame above the 64 MiB cap is rejected before a single body byte is buffered, so a hostile server cannot drive the client to OOM. The mock encodes backend messages by hand while the driver decodes them, and the reverse for frontend messages, so the two halves cross-check each other.

How it differs

Conduit is not trying to replace the production drivers. It is the one you can read.

libpq

The C reference client that most language bindings wrap. Battle tested and complete, but a C dependency you call through rather than read, and the wire protocol stays hidden behind it.

tokio-postgres, rust-postgres

The standard Rust drivers: async, connection pooling, prepared-statement caching, the binary format, COPY, and a dependency tree to match. What you want in production.

Conduit

A synchronous, zero-dependency core in a few hundred lines of Rust, with the crypto for SCRAM written by hand and each protocol message its own readable type. Honest about its edges: no TLS, text format only, one query at a time. A driver you can hold in your head, not a production client.

The building blocks

Each of these is a small, separately testable piece of the crate. Together they carry a query from the socket to a typed row.

framing Tag and length

Every message is a one-byte type tag and a four-byte big-endian length that counts itself plus the body. Startup and SSLRequest are the only untagged exceptions.

safety Bounded reads

The reader length-checks every field and returns a protocol error rather than indexing past the buffer. A truncated string or a short body is an error, never a panic.

framing Need more

Decode returns "not yet a full message" so the connection layer knows to read more, which is what makes a message split across several TCP reads just work.

safety Length cap

A server-declared length above the cap (default 64 MiB) is rejected before a single body byte is buffered, so a hostile server cannot announce a huge frame and drive the client to OOM.

auth MD5

The legacy scheme, md5(hex(md5(password + user)) + salt), built on a hand-written RFC 1321 MD5 checked against the standard's own known-answer vectors.

auth SCRAM-SHA-256

The default since PostgreSQL 14. The full RFC 5802 and 7677 exchange over hand-written SHA-256, HMAC, PBKDF2, and base64, verifying the server signature before it trusts the connection.

queries Text-format params

In the extended protocol a value travels in the Bind message as a length-prefixed byte string, never interpolated into SQL, so a parameter is structurally data and never code.

types OID decoding

Columns are decoded by their type OID into i16, i32, i64, f32, f64, bool, or String, with NULL as Option and a clear error on a type mismatch.

Use it

Conduit is a Rust library with a small CLI. These are the real ways to drive it.

Library API

Config::from_url parses a postgres:// URL, Connection::connect runs the handshake, and conn.query binds parameters and returns typed rows.

Simple and extended

simple_query and simple_query_multi use the one-message Query protocol; query uses Parse, Bind, Describe, Execute, Sync with text-format parameters.

Typed rows

row.get::<i32>("id") reads a column by name or index, Option<T> reads a nullable column, and asking for the wrong type is an honest error.

CLI

conduit query runs one SQL statement against a server, and conduit demo replays a scripted session with no database at all.

Tested without a database

Golden byte vectors pin the codec and an in-process mock server drives the real driver over TCP; an optional smoke test runs against real Postgres when CONDUIT_PG_URL is set.

# golden vectors plus the mock integration, no database
cargo test

# a scripted session, no database needed
cargo run -- demo

# run a query against a real server
cargo run -- query postgres://u:p@host/db "SELECT 1 AS one"

# opt-in real-Postgres smoke test (CI stays green without it)
CONDUIT_PG_URL=postgres://user:pass@localhost:5432/db cargo test