A Proxy Protocol (v1 and v2) Implementation for Rust and Tokio
This crate implements the Proxy Protocol (versions 1 and 2) on top of Tokio.
It provides a drop-in TcpListener wrapper that reads and strips Proxy Protocol headers,
exposing the real client address to the application. Beyond the listener, the crate includes
a standalone parser, a header builder, and TLV support with vendor-specific extensions for AWS, Azure, and GCP.
This library was heavily inspired by Ranch (Erlang, Elixir) and go-proxyproto (Go).
Key Features
- Proxy Protocol v1 (text) and v2 (binary) parsing and building
ProxyProtocolListener: a TCP listener wrapper with configurable policiesaxum::serveintegration behind theaxumfeature viaaxum::serve::Listener- TLV extensions: ALPN, authority, TLS metadata, CRC32c, unique ID, NETNS
- Connection policies: trusted proxies (exact IPs and CIDRs), mixed mode, custom closures
- Header validation: reject connections based on parsed header content
- Bounded concurrency: configurable semaphore for in-flight PP handshakes
- Vendor-specific TLV support: AWS VPC endpoint ID, Azure Private Link ID, GCP PSC connection ID
Project Maturity
This library is young but the API is stabilizing.
Before 1.0, breaking API changes can happen.
Requirements
- Rust 1.88+ (edition 2024)
- Tokio runtime
Dependency
= "0.8"
With Axum Integration
= { = "0.8", = ["axum"] }
With Vendor TLV Support
With AWS NLB:
# AWS VPC endpoint ID (PP2_TYPE_AWS, 0xEA)
= { = "0.8", = ["aws"] }
With GCP Private Service Connect:
# GCP Private Service Connect connection ID (PP2_TYPE_GCE, 0xE0)
= { = "0.8", = ["gcp"] }
With Azure Private Link:
# Azure Private Endpoint LinkID (PP2_TYPE_AZURE, 0xEE)
= { = "0.8", = ["azure"] }
Usage
With axum::serve
ProxyProtocolListener implements axum::serve::Listener, so it plugs directly
into axum::serve:
use ;
use TcpListener;
use ;
let tcp = bind.await?;
let pp = new;
let app = new.route;
serve
.with_graceful_shutdown
.await?;
async
Connection errors are logged via tracing and retried internally;
your handlers only see successfully accepted connections.
Accept Connections Directly
use TcpListener;
use ProxyProtocolListener;
let listener = bind.await?;
let pp = new;
loop
Configuration
Using the builder:
use Duration;
use ;
let config = builder
.header_timeout
.max_header_size
.max_pending_handshakes
.version
.build?;
Or using struct literals with ..Default::default():
use Duration;
use ;
let config = ProxyProtocolConfig ;
TLS Termination
The PP header is always cleartext before any TLS handshake.
ProxiedStream implements AsyncRead + AsyncWrite, so a TLS acceptor wraps it directly:
let stream = pp.accept.await?;
// capture metadata before TLS consumes the stream
let info = stream.connect_info;
let tls_stream = tls_acceptor.accept.await?;
Parse a Header from a Byte Buffer
use parse;
let = parse?;
let leftover = &buf;
println!;
if let Some = info.source_inet
if let Some = info.destination_inet
Trusted Proxies
Only accept PP headers from known load balancers; reject everyone else:
use ;
let cidrs: = vec!;
let policy = from_ipnets;
let config = builder
.policy
.build?;
let pp = new;
Mixed Mode (Proxied and Direct Connections)
Accept PP from trusted proxies, pass direct connections through as-is:
use ;
let trusted = new;
let config = builder
.policy
.build?;
let pp = new;
Header Validation
Reject connections after parsing, e.g. to block spoofed source addresses:
use SocketAddr;
use ;
let config = builder
.validator
.build?;
Build and Send a Header
use HeaderBuilder;
let header = v2_proxy
.with_authority
.with_unique_id
.with_crc32c
.build;
stream.write_all.await?;
// or write directly without using an intermediate Vec:
// builder.write_to(&mut stream).await?;
// v2 LOCAL (health-check / proxy-to-self, no addresses)
let header = v2_local.build;
// v1 text header
let header = v1_proxy.build;
// => b"PROXY TCP4 203.0.113.42 10.0.0.1 54321 8080\r\n"
build panics if any single TLV value or the total v2 payload exceeds
65,535 bytes (the hard limit of the v2 format).
TLV Extensions
V2 headers carry TLV (Type-Length-Value) extensions, parsed into typed fields
with the raw bytes always preserved in tlvs.raw:
let = parse?;
if let Some = &info.tlvs.alpn
if let Some = &info.tlvs.authority
if let Some = &info.tlvs.unique_id
if let Some = &info.tlvs.netns
if let Some = info.tlvs.crc32c
if let Some = &info.tlvs.ssl
// all TLVs are also available as raw (type, value) pairs
for in &info.tlvs.raw
Public Cloud Vendor TLVs
Each requires its feature flag:
// feature = "aws"
if let Some = info.tlvs.aws
// feature = "azure"
if let Some = info.tlvs.azure
// feature = "gcp"
if let Some = info.tlvs.gcp
Copyright
(c) 2025-2026 Michael S. Klishin and Contributors.
License
This crate, proxy-protocol-rs, is dual-licensed under
the Apache Software License 2.0 and the MIT license.
SPDX-License-Identifier: Apache-2.0 OR MIT