HAProxy PROXY Protocol Fixes

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey 2026-02-20 16:42:40 +03:00
parent 2ea4c83d9d
commit e8454ea370
No known key found for this signature in database
5 changed files with 80 additions and 5 deletions

View File

@ -226,6 +226,7 @@ impl ProxyConfig {
ip: ipv4, ip: ipv4,
announce: None, announce: None,
announce_ip: None, announce_ip: None,
proxy_protocol: None,
}); });
} }
if let Some(ipv6_str) = &config.server.listen_addr_ipv6 { if let Some(ipv6_str) = &config.server.listen_addr_ipv6 {
@ -234,6 +235,7 @@ impl ProxyConfig {
ip: ipv6, ip: ipv6,
announce: None, announce: None,
announce_ip: None, announce_ip: None,
proxy_protocol: None,
}); });
} }
} }

View File

@ -513,6 +513,9 @@ pub struct ListenerConfig {
/// Migrated to `announce` automatically if `announce` is not set. /// Migrated to `announce` automatically if `announce` is not set.
#[serde(default)] #[serde(default)]
pub announce_ip: Option<IpAddr>, pub announce_ip: Option<IpAddr>,
/// Per-listener PROXY protocol override. When set, overrides global server.proxy_protocol.
#[serde(default)]
pub proxy_protocol: Option<bool>,
} }
// ============= ShowLink ============= // ============= ShowLink =============

View File

@ -699,6 +699,8 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
Ok(socket) => { Ok(socket) => {
let listener = TcpListener::from_std(socket.into())?; let listener = TcpListener::from_std(socket.into())?;
info!("Listening on {}", addr); info!("Listening on {}", addr);
let listener_proxy_protocol =
listener_conf.proxy_protocol.unwrap_or(config.server.proxy_protocol);
// Resolve the public host for link generation // Resolve the public host for link generation
let public_host = if let Some(ref announce) = listener_conf.announce { let public_host = if let Some(ref announce) = listener_conf.announce {
@ -724,7 +726,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
print_proxy_links(&public_host, link_port, &config); print_proxy_links(&public_host, link_port, &config);
} }
listeners.push(listener); listeners.push((listener, listener_proxy_protocol));
} }
Err(e) => { Err(e) => {
error!("Failed to bind to {}: {}", addr, e); error!("Failed to bind to {}: {}", addr, e);
@ -810,12 +812,13 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
let me_pool = me_pool.clone(); let me_pool = me_pool.clone();
let tls_cache = tls_cache.clone(); let tls_cache = tls_cache.clone();
let ip_tracker = ip_tracker.clone(); let ip_tracker = ip_tracker.clone();
let proxy_protocol_enabled = config.server.proxy_protocol;
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = crate::proxy::client::handle_client_stream( if let Err(e) = crate::proxy::client::handle_client_stream(
stream, fake_peer, config, stats, stream, fake_peer, config, stats,
upstream_manager, replay_checker, buffer_pool, rng, upstream_manager, replay_checker, buffer_pool, rng,
me_pool, tls_cache, ip_tracker, me_pool, tls_cache, ip_tracker, proxy_protocol_enabled,
).await { ).await {
debug!(error = %e, "Unix socket connection error"); debug!(error = %e, "Unix socket connection error");
} }
@ -855,7 +858,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
}); });
} }
for listener in listeners { for (listener, listener_proxy_protocol) in listeners {
let config = config.clone(); let config = config.clone();
let stats = stats.clone(); let stats = stats.clone();
let upstream_manager = upstream_manager.clone(); let upstream_manager = upstream_manager.clone();
@ -879,6 +882,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
let me_pool = me_pool.clone(); let me_pool = me_pool.clone();
let tls_cache = tls_cache.clone(); let tls_cache = tls_cache.clone();
let ip_tracker = ip_tracker.clone(); let ip_tracker = ip_tracker.clone();
let proxy_protocol_enabled = listener_proxy_protocol;
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = ClientHandler::new( if let Err(e) = ClientHandler::new(
@ -893,6 +897,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
me_pool, me_pool,
tls_cache, tls_cache,
ip_tracker, ip_tracker,
proxy_protocol_enabled,
) )
.run() .run()
.await .await

View File

@ -755,4 +755,65 @@ mod tests {
// Should return None (no match) but not panic // Should return None (no match) but not panic
assert!(result.is_none()); assert!(result.is_none());
} }
fn build_client_hello_with_exts(exts: Vec<(u16, Vec<u8>)>, host: &str) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&TLS_VERSION); // legacy version
body.extend_from_slice(&[0u8; 32]); // random
body.push(0); // session id len
body.extend_from_slice(&2u16.to_be_bytes()); // cipher suites len
body.extend_from_slice(&[0x13, 0x01]); // TLS_AES_128_GCM_SHA256
body.push(1); // compression len
body.push(0); // null compression
// Build SNI extension
let host_bytes = host.as_bytes();
let mut sni_ext = Vec::new();
sni_ext.extend_from_slice(&(host_bytes.len() as u16 + 3).to_be_bytes());
sni_ext.push(0);
sni_ext.extend_from_slice(&(host_bytes.len() as u16).to_be_bytes());
sni_ext.extend_from_slice(host_bytes);
let mut ext_blob = Vec::new();
for (typ, data) in exts {
ext_blob.extend_from_slice(&typ.to_be_bytes());
ext_blob.extend_from_slice(&(data.len() as u16).to_be_bytes());
ext_blob.extend_from_slice(&data);
}
// SNI last
ext_blob.extend_from_slice(&0x0000u16.to_be_bytes());
ext_blob.extend_from_slice(&(sni_ext.len() as u16).to_be_bytes());
ext_blob.extend_from_slice(&sni_ext);
body.extend_from_slice(&(ext_blob.len() as u16).to_be_bytes());
body.extend_from_slice(&ext_blob);
let mut handshake = Vec::new();
handshake.push(0x01); // ClientHello
let len_bytes = (body.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len_bytes[1..4]);
handshake.extend_from_slice(&body);
let mut record = Vec::new();
record.push(TLS_RECORD_HANDSHAKE);
record.extend_from_slice(&[0x03, 0x01]);
record.extend_from_slice(&(handshake.len() as u16).to_be_bytes());
record.extend_from_slice(&handshake);
record
}
#[test]
fn test_extract_sni_with_grease_extension() {
// GREASE type 0x0a0a with zero length before SNI
let ch = build_client_hello_with_exts(vec![(0x0a0a, Vec::new())], "example.com");
let sni = extract_sni_from_client_hello(&ch);
assert_eq!(sni.as_deref(), Some("example.com"));
}
#[test]
fn test_extract_sni_tolerates_empty_unknown_extension() {
let ch = build_client_hello_with_exts(vec![(0x1234, Vec::new())], "test.local");
let sni = extract_sni_from_client_hello(&ch);
assert_eq!(sni.as_deref(), Some("test.local"));
}
} }

View File

@ -51,6 +51,7 @@ pub async fn handle_client_stream<S>(
me_pool: Option<Arc<MePool>>, me_pool: Option<Arc<MePool>>,
tls_cache: Option<Arc<TlsFrontCache>>, tls_cache: Option<Arc<TlsFrontCache>>,
ip_tracker: Arc<UserIpTracker>, ip_tracker: Arc<UserIpTracker>,
proxy_protocol_enabled: bool,
) -> Result<()> ) -> Result<()>
where where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static, S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
@ -58,7 +59,7 @@ where
stats.increment_connects_all(); stats.increment_connects_all();
let mut real_peer = normalize_ip(peer); let mut real_peer = normalize_ip(peer);
if config.server.proxy_protocol { if proxy_protocol_enabled {
match parse_proxy_protocol(&mut stream, peer).await { match parse_proxy_protocol(&mut stream, peer).await {
Ok(info) => { Ok(info) => {
debug!( debug!(
@ -229,6 +230,7 @@ pub struct RunningClientHandler {
me_pool: Option<Arc<MePool>>, me_pool: Option<Arc<MePool>>,
tls_cache: Option<Arc<TlsFrontCache>>, tls_cache: Option<Arc<TlsFrontCache>>,
ip_tracker: Arc<UserIpTracker>, ip_tracker: Arc<UserIpTracker>,
proxy_protocol_enabled: bool,
} }
impl ClientHandler { impl ClientHandler {
@ -244,6 +246,7 @@ impl ClientHandler {
me_pool: Option<Arc<MePool>>, me_pool: Option<Arc<MePool>>,
tls_cache: Option<Arc<TlsFrontCache>>, tls_cache: Option<Arc<TlsFrontCache>>,
ip_tracker: Arc<UserIpTracker>, ip_tracker: Arc<UserIpTracker>,
proxy_protocol_enabled: bool,
) -> RunningClientHandler { ) -> RunningClientHandler {
RunningClientHandler { RunningClientHandler {
stream, stream,
@ -257,6 +260,7 @@ impl ClientHandler {
me_pool, me_pool,
tls_cache, tls_cache,
ip_tracker, ip_tracker,
proxy_protocol_enabled,
} }
} }
} }
@ -303,7 +307,7 @@ impl RunningClientHandler {
} }
async fn do_handshake(mut self) -> Result<HandshakeOutcome> { async fn do_handshake(mut self) -> Result<HandshakeOutcome> {
if self.config.server.proxy_protocol { if self.proxy_protocol_enabled {
match parse_proxy_protocol(&mut self.stream, self.peer).await { match parse_proxy_protocol(&mut self.stream, self.peer).await {
Ok(info) => { Ok(info) => {
debug!( debug!(