Skip to content

Commit ab9cd13

Browse files
committed
HAProxy protocol receiver trait and data types
1 parent 200cee4 commit ab9cd13

File tree

6 files changed

+302
-0
lines changed

6 files changed

+302
-0
lines changed

pingora-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ openssl_derived = ["any_tls"]
106106
any_tls = []
107107
sentry = ["dep:sentry"]
108108
connection_filter = []
109+
proxy_protocol = []

pingora-core/src/listeners/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ pub use crate::protocols::tls::ALPN;
9090
use crate::protocols::GetSocketDigest;
9191
pub use l4::{ServerAddress, TcpSocketOptions};
9292

93+
#[cfg(feature = "proxy_protocol")]
94+
use crate::protocols::proxy_protocol::ProxyProtocolReceiver;
95+
9396
/// The APIs to customize things like certificate during TLS server side handshake
9497
#[async_trait]
9598
pub trait TlsAccept {
@@ -110,6 +113,8 @@ struct TransportStackBuilder {
110113
tls: Option<TlsSettings>,
111114
#[cfg(feature = "connection_filter")]
112115
connection_filter: Option<Arc<dyn ConnectionFilter>>,
116+
#[cfg(feature = "proxy_protocol")]
117+
pp_receiver: Option<Arc<dyn ProxyProtocolReceiver>>,
113118
}
114119

115120
impl TransportStackBuilder {
@@ -135,6 +140,8 @@ impl TransportStackBuilder {
135140
Ok(TransportStack {
136141
l4,
137142
tls: self.tls.take().map(|tls| Arc::new(tls.build())),
143+
#[cfg(feature = "proxy_protocol")]
144+
pp_receiver: self.pp_receiver.take(),
138145
})
139146
}
140147
}
@@ -143,6 +150,8 @@ impl TransportStackBuilder {
143150
pub(crate) struct TransportStack {
144151
l4: ListenerEndpoint,
145152
tls: Option<Arc<Acceptor>>,
153+
#[cfg(feature = "proxy_protocol")]
154+
pp_receiver: Option<Arc<dyn ProxyProtocolReceiver>>,
146155
}
147156

148157
impl TransportStack {
@@ -155,6 +164,8 @@ impl TransportStack {
155164
Ok(UninitializedStream {
156165
l4: stream,
157166
tls: self.tls.clone(),
167+
#[cfg(feature = "proxy_protocol")]
168+
pp_receiver: self.pp_receiver.clone(),
158169
})
159170
}
160171

@@ -166,11 +177,26 @@ impl TransportStack {
166177
pub(crate) struct UninitializedStream {
167178
l4: L4Stream,
168179
tls: Option<Arc<Acceptor>>,
180+
#[cfg(feature = "proxy_protocol")]
181+
pp_receiver: Option<Arc<dyn ProxyProtocolReceiver>>,
169182
}
170183

171184
impl UninitializedStream {
172185
pub async fn handshake(mut self) -> Result<Stream> {
173186
self.l4.set_buffer();
187+
188+
#[cfg(feature = "proxy_protocol")]
189+
if let Some(receiver) = self.pp_receiver {
190+
let (header, unused) = receiver.accept(&mut self.l4).await?;
191+
192+
self.l4.get_socket_digest().map(|sd| {
193+
sd.proxy_protocol
194+
.set(Some(header))
195+
.expect("Newly created OnceCell must be empty");
196+
});
197+
self.l4.rewind(&unused);
198+
}
199+
174200
if let Some(tls) = self.tls {
175201
let tls_stream = tls.tls_handshake(self.l4).await?;
176202
Ok(Box::new(tls_stream))
@@ -288,6 +314,26 @@ impl Listeners {
288314
tls,
289315
#[cfg(feature = "connection_filter")]
290316
connection_filter: self.connection_filter.clone(),
317+
#[cfg(feature = "proxy_protocol")]
318+
pp_receiver: None,
319+
})
320+
}
321+
322+
#[cfg(feature = "proxy_protocol")]
323+
/// Add TCP endpoint to self with optional [`TcpSocketOptions`] and [`TlsSettings`], and a [`ProxyProtocolReceiver`].
324+
pub fn add_proxy_protocol_endpoint<R: ProxyProtocolReceiver + 'static>(
325+
&mut self,
326+
addr: &str,
327+
sock_opt: Option<TcpSocketOptions>,
328+
tls: Option<TlsSettings>,
329+
pp_receiver: R,
330+
) {
331+
self.stacks.push(TransportStackBuilder {
332+
l4: ServerAddress::Tcp(addr.into(), sock_opt),
333+
tls,
334+
#[cfg(feature = "connection_filter")]
335+
connection_filter: self.connection_filter.clone(),
336+
pp_receiver: Some(Arc::new(pp_receiver)),
291337
})
292338
}
293339

pingora-core/src/protocols/digest.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ use super::l4::socket::SocketAddr;
2424
use super::raw_connect::ProxyDigest;
2525
use super::tls::digest::SslDigest;
2626

27+
#[cfg(feature = "proxy_protocol")]
28+
use super::proxy_protocol::ProxyProtocolHeader;
29+
2730
/// The information can be extracted from a connection
2831
#[derive(Clone, Debug, Default)]
2932
pub struct Digest {
@@ -72,6 +75,9 @@ pub struct SocketDigest {
7275
pub local_addr: OnceCell<Option<SocketAddr>>,
7376
/// Original destination address
7477
pub original_dst: OnceCell<Option<SocketAddr>>,
78+
/// Proxy Protocol header
79+
#[cfg(feature = "proxy_protocol")]
80+
pub proxy_protocol: OnceCell<Option<ProxyProtocolHeader>>,
7581
}
7682

7783
impl SocketDigest {
@@ -82,6 +88,8 @@ impl SocketDigest {
8288
peer_addr: OnceCell::new(),
8389
local_addr: OnceCell::new(),
8490
original_dst: OnceCell::new(),
91+
#[cfg(feature = "proxy_protocol")]
92+
proxy_protocol: OnceCell::new(),
8593
}
8694
}
8795

@@ -92,6 +100,8 @@ impl SocketDigest {
92100
peer_addr: OnceCell::new(),
93101
local_addr: OnceCell::new(),
94102
original_dst: OnceCell::new(),
103+
#[cfg(feature = "proxy_protocol")]
104+
proxy_protocol: OnceCell::new(),
95105
}
96106
}
97107

@@ -204,6 +214,11 @@ impl SocketDigest {
204214
})
205215
.as_ref()
206216
}
217+
218+
#[cfg(feature = "proxy_protocol")]
219+
pub fn proxy_protocol(&self) -> Option<&ProxyProtocolHeader> {
220+
self.proxy_protocol.get_or_init(|| None).as_ref()
221+
}
207222
}
208223

209224
/// The interface to return timing information

pingora-core/src/protocols/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
mod digest;
1818
pub mod http;
1919
pub mod l4;
20+
#[cfg(feature = "proxy_protocol")]
21+
pub mod proxy_protocol;
2022
pub mod raw_connect;
2123
pub mod tls;
2224
#[cfg(windows)]
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright 2025 Cloudflare, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Proxy Protocol support for preserving client connection information
16+
//!
17+
//! This module provides the [`ProxyProtocolReceiver`] trait and related types for implementing
18+
//! [HAProxy's Proxy Protocol](https://www.haproxy.org/download/2.8/doc/proxy-protocol.txt).
19+
//! The protocol allows intermediaries (like load balancers) to pass original client connection
20+
//! information to backend servers.
21+
//!
22+
//! # Feature Flag
23+
//!
24+
//! This functionality requires the `proxy_protocol` feature to be enabled:
25+
//! ```toml
26+
//! [dependencies]
27+
//! pingora-core = { version = "0.6", features = ["proxy_protocol"] }
28+
//! ```
29+
//!
30+
//! # Protocol Versions
31+
//!
32+
//! Both Proxy Protocol v1 (human-readable text format) and v2 (binary format with TLV support)
33+
//! are supported through the [`ProxyProtocolHeader`] enum.
34+
//!
35+
//! # Example
36+
//!
37+
//! ```rust,no_run
38+
//! use async_trait::async_trait;
39+
//! use pingora_core::protocols::proxy_protocol::{
40+
//! ProxyProtocolReceiver, ProxyProtocolHeader, HeaderV2,
41+
//! Command, Transport, Addresses
42+
//! };
43+
//! use pingora_core::protocols::l4::stream::Stream;
44+
//! use pingora_error::Result;
45+
//!
46+
//! struct MyProxyProtocolParser;
47+
//!
48+
//! #[async_trait]
49+
//! impl ProxyProtocolReceiver for MyProxyProtocolParser {
50+
//! async fn accept(&self, stream: &mut Stream) -> Result<(ProxyProtocolHeader, Vec<u8>)> {
51+
//! // Parse the Proxy Protocol header from the stream
52+
//! // Return the parsed header and any remaining bytes
53+
//! todo!("Implement parsing logic")
54+
//! }
55+
//! }
56+
//! ```
57+
58+
use async_trait::async_trait;
59+
use std::borrow::Cow;
60+
use std::net::SocketAddr;
61+
62+
use super::l4::stream::Stream;
63+
use pingora_error::Result;
64+
65+
/// A trait for parsing Proxy Protocol headers from incoming connections.
66+
///
67+
/// Implementations of this trait handle reading and parsing Proxy Protocol headers
68+
/// (v1 or v2) from a stream. The trait is designed to be flexible, allowing different
69+
/// parsing strategies or third-party parser libraries to be used.
70+
///
71+
/// # Example
72+
///
73+
/// ```rust,no_run
74+
/// use async_trait::async_trait;
75+
/// use pingora_core::protocols::proxy_protocol::{
76+
/// ProxyProtocolReceiver, ProxyProtocolHeader, HeaderV1,
77+
/// Transport, Addresses
78+
/// };
79+
/// use pingora_core::protocols::l4::stream::Stream;
80+
/// use pingora_error::Result;
81+
/// use tokio::io::AsyncReadExt;
82+
///
83+
/// struct SimpleV1Parser;
84+
///
85+
/// #[async_trait]
86+
/// impl ProxyProtocolReceiver for SimpleV1Parser {
87+
/// async fn accept(&self, stream: &mut Stream) -> Result<(ProxyProtocolHeader, Vec<u8>)> {
88+
/// let mut buffer = Vec::new();
89+
/// // Read and parse v1 header
90+
/// stream.read_buf(&mut buffer).await?;
91+
/// // Parse logic here...
92+
/// todo!("Parse v1 header and return result")
93+
/// }
94+
/// }
95+
/// ```
96+
///
97+
/// # Performance Considerations
98+
///
99+
/// This method is called once per connection that uses Proxy Protocol. Implementations
100+
/// should efficiently read only the necessary bytes from the stream to parse the header,
101+
/// returning any excess bytes for subsequent processing.
102+
#[async_trait]
103+
pub trait ProxyProtocolReceiver: Send + Sync {
104+
/// Parses the Proxy Protocol header from an accepted connection stream.
105+
///
106+
/// This method is called after a TCP connection is accepted on a Proxy Protocol endpoint.
107+
/// Implementors should read from the stream to parse the header according to either
108+
/// v1 (text) or v2 (binary) format specifications.
109+
///
110+
/// # Arguments
111+
///
112+
/// * `stream` - A mutable reference to the accepted connection stream
113+
///
114+
/// # Returns
115+
///
116+
/// A tuple containing:
117+
/// * The parsed [`ProxyProtocolHeader`] (v1 or v2)
118+
/// * Any remaining bytes read from the stream after the header (to be processed by the application)
119+
///
120+
/// # Errors
121+
///
122+
/// Returns an error if:
123+
/// * The stream cannot be read
124+
/// * The header format is invalid
125+
/// * The connection is closed unexpectedly
126+
///
127+
/// # Example
128+
///
129+
/// ```rust,no_run
130+
/// async fn accept(&self, stream: &mut Stream) -> Result<(ProxyProtocolHeader, Vec<u8>)> {
131+
/// // Read bytes from stream
132+
/// let mut buffer = Vec::new();
133+
/// stream.read_buf(&mut buffer).await?;
134+
///
135+
/// // Parse header and determine remaining bytes
136+
/// let (header, remaining) = parse_proxy_header(&buffer)?;
137+
/// Ok((header, remaining))
138+
/// }
139+
/// ```
140+
async fn accept(&self, stream: &mut Stream) -> Result<(ProxyProtocolHeader, Vec<u8>)>;
141+
}
142+
143+
/// Parsed Proxy Protocol header containing connection information.
144+
///
145+
/// This enum represents either a v1 (text) or v2 (binary) Proxy Protocol header.
146+
/// The version is determined by the parser implementation.
147+
#[derive(Debug)]
148+
pub enum ProxyProtocolHeader {
149+
/// Proxy Protocol version 1 (human-readable text format)
150+
V1(HeaderV1),
151+
/// Proxy Protocol version 2 (binary format with TLV extension support)
152+
V2(HeaderV2),
153+
}
154+
155+
/// Proxy Protocol version 1 header information.
156+
///
157+
/// Version 1 uses a human-readable text format. It contains basic transport
158+
/// and address information but does not support the command field or TLV extensions.
159+
#[derive(Debug)]
160+
pub struct HeaderV1 {
161+
/// The transport protocol used for the proxied connection
162+
pub transport: Transport,
163+
/// Source and destination addresses, if available.
164+
/// `None` indicates an unknown or local connection.
165+
pub addresses: Option<Addresses>,
166+
}
167+
168+
/// Proxy Protocol version 2 header information.
169+
///
170+
/// Version 2 uses a binary format and supports additional features including
171+
/// the command field (LOCAL vs PROXY) and optional TLV (Type-Length-Value) extensions
172+
/// for passing custom metadata.
173+
#[derive(Debug)]
174+
pub struct HeaderV2 {
175+
/// Indicates whether this is a proxied connection or a local health check
176+
pub command: Command,
177+
/// The transport protocol used for the proxied connection
178+
pub transport: Transport,
179+
/// Source and destination addresses, if available.
180+
/// `None` for LOCAL command or unknown connections.
181+
pub addresses: Option<Addresses>,
182+
/// Optional TLV (Type-Length-Value) data for protocol extensions.
183+
/// May contain additional metadata such as SSL information, unique IDs, etc.
184+
pub tlvs: Option<Cow<'static, [u8]>>,
185+
}
186+
187+
/// Transport protocol family for the proxied connection.
188+
///
189+
/// Indicates the network protocol (IPv4 or IPv6 over TCP) or unknown/unspecified transport.
190+
#[derive(Debug)]
191+
pub enum Transport {
192+
/// TCP over IPv4
193+
Tcp4,
194+
/// TCP over IPv6
195+
Tcp6,
196+
/// Unknown or unspecified transport protocol
197+
Unknown,
198+
}
199+
200+
/// Source and destination socket addresses for a proxied connection.
201+
///
202+
/// Contains the original client address and the destination address
203+
/// as seen by the proxy/load balancer.
204+
#[derive(Debug)]
205+
pub struct Addresses {
206+
/// The original source address (client)
207+
pub source: SocketAddr,
208+
/// The destination address as seen by the proxy
209+
pub destination: SocketAddr,
210+
}
211+
212+
/// Proxy Protocol v2 command type.
213+
///
214+
/// Distinguishes between actual proxied connections and local connections
215+
/// (typically used for health checks).
216+
#[derive(Debug)]
217+
pub enum Command {
218+
/// LOCAL command: indicates a health check or non-proxied connection.
219+
/// Receivers should not use any address information from LOCAL connections.
220+
Local,
221+
/// PROXY command: indicates a proxied connection with valid address information.
222+
Proxy,
223+
}

0 commit comments

Comments
 (0)