Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfa958cf3d | |||
| db2e586da2 |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-19 - 25.12.0 - feat(proxy-protocol)
|
||||
add PROXY protocol v2 support to the Rust passthrough listener and streamline TypeScript proxy protocol exports
|
||||
|
||||
- detect and parse PROXY protocol v2 headers in the Rust TCP listener, including TCP and UDP address families
|
||||
- add Rust v2 header generation, incomplete-header handling, and broader parser test coverage
|
||||
- remove deprecated TypeScript proxy protocol parser exports and tests, leaving shared type definitions only
|
||||
|
||||
## 2026-03-17 - 25.11.24 - fix(rustproxy-http)
|
||||
improve async static file serving, websocket handshake buffering, and shared metric metadata handling
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "25.11.24",
|
||||
"version": "25.12.0",
|
||||
"private": false,
|
||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -9,9 +9,11 @@ pub enum ProxyProtocolError {
|
||||
UnsupportedVersion,
|
||||
#[error("Parse error: {0}")]
|
||||
Parse(String),
|
||||
#[error("Incomplete header: need {0} bytes, got {1}")]
|
||||
Incomplete(usize, usize),
|
||||
}
|
||||
|
||||
/// Parsed PROXY protocol v1 header.
|
||||
/// Parsed PROXY protocol header (v1 or v2).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyProtocolHeader {
|
||||
pub source_addr: SocketAddr,
|
||||
@@ -24,14 +26,29 @@ pub struct ProxyProtocolHeader {
|
||||
pub enum ProxyProtocol {
|
||||
Tcp4,
|
||||
Tcp6,
|
||||
Udp4,
|
||||
Udp6,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Transport type for PROXY v2 header generation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProxyV2Transport {
|
||||
Stream, // TCP
|
||||
Datagram, // UDP
|
||||
}
|
||||
|
||||
/// PROXY protocol v2 signature (12 bytes).
|
||||
const PROXY_V2_SIGNATURE: [u8; 12] = [
|
||||
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
|
||||
];
|
||||
|
||||
// ===== v1 (text format) =====
|
||||
|
||||
/// Parse a PROXY protocol v1 header from data.
|
||||
///
|
||||
/// Format: `PROXY TCP4 <src_ip> <dst_ip> <src_port> <dst_port>\r\n`
|
||||
pub fn parse_v1(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtocolError> {
|
||||
// Find the end of the header line
|
||||
let line_end = data
|
||||
.windows(2)
|
||||
.position(|w| w == b"\r\n")
|
||||
@@ -56,10 +73,10 @@ pub fn parse_v1(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtoc
|
||||
_ => return Err(ProxyProtocolError::UnsupportedVersion),
|
||||
};
|
||||
|
||||
let src_ip: std::net::IpAddr = parts[2]
|
||||
let src_ip: IpAddr = parts[2]
|
||||
.parse()
|
||||
.map_err(|_| ProxyProtocolError::Parse("Invalid source IP".to_string()))?;
|
||||
let dst_ip: std::net::IpAddr = parts[3]
|
||||
let dst_ip: IpAddr = parts[3]
|
||||
.parse()
|
||||
.map_err(|_| ProxyProtocolError::Parse("Invalid destination IP".to_string()))?;
|
||||
let src_port: u16 = parts[4]
|
||||
@@ -75,7 +92,6 @@ pub fn parse_v1(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtoc
|
||||
protocol,
|
||||
};
|
||||
|
||||
// Consumed bytes = line + \r\n
|
||||
Ok((header, line_end + 2))
|
||||
}
|
||||
|
||||
@@ -97,10 +113,219 @@ pub fn is_proxy_protocol_v1(data: &[u8]) -> bool {
|
||||
data.starts_with(b"PROXY ")
|
||||
}
|
||||
|
||||
// ===== v2 (binary format) =====
|
||||
|
||||
/// Check if data starts with a PROXY protocol v2 header.
|
||||
pub fn is_proxy_protocol_v2(data: &[u8]) -> bool {
|
||||
data.len() >= 12 && data[..12] == PROXY_V2_SIGNATURE
|
||||
}
|
||||
|
||||
/// Parse a PROXY protocol v2 binary header.
|
||||
///
|
||||
/// Binary format:
|
||||
/// - [0..12] signature (12 bytes)
|
||||
/// - [12] version (high nibble) + command (low nibble)
|
||||
/// - [13] address family (high nibble) + transport (low nibble)
|
||||
/// - [14..16] address block length (big-endian u16)
|
||||
/// - [16..] address block (variable, depends on family)
|
||||
pub fn parse_v2(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtocolError> {
|
||||
if data.len() < 16 {
|
||||
return Err(ProxyProtocolError::Incomplete(16, data.len()));
|
||||
}
|
||||
|
||||
// Validate signature
|
||||
if data[..12] != PROXY_V2_SIGNATURE {
|
||||
return Err(ProxyProtocolError::InvalidHeader);
|
||||
}
|
||||
|
||||
// Version (high nibble of byte 12) must be 0x2
|
||||
let version = (data[12] >> 4) & 0x0F;
|
||||
if version != 2 {
|
||||
return Err(ProxyProtocolError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
// Command (low nibble of byte 12)
|
||||
let command = data[12] & 0x0F;
|
||||
// 0x0 = LOCAL, 0x1 = PROXY
|
||||
if command > 1 {
|
||||
return Err(ProxyProtocolError::Parse(format!("Unknown command: {}", command)));
|
||||
}
|
||||
|
||||
// Address family (high nibble) + transport (low nibble) of byte 13
|
||||
let family = (data[13] >> 4) & 0x0F;
|
||||
let transport = data[13] & 0x0F;
|
||||
|
||||
// Address block length
|
||||
let addr_len = u16::from_be_bytes([data[14], data[15]]) as usize;
|
||||
let total_len = 16 + addr_len;
|
||||
|
||||
if data.len() < total_len {
|
||||
return Err(ProxyProtocolError::Incomplete(total_len, data.len()));
|
||||
}
|
||||
|
||||
// LOCAL command: no real addresses, return unspecified
|
||||
if command == 0 {
|
||||
return Ok((
|
||||
ProxyProtocolHeader {
|
||||
source_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
dest_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
protocol: ProxyProtocol::Unknown,
|
||||
},
|
||||
total_len,
|
||||
));
|
||||
}
|
||||
|
||||
// PROXY command: parse addresses based on family + transport
|
||||
let addr_block = &data[16..16 + addr_len];
|
||||
|
||||
match (family, transport) {
|
||||
// AF_INET (0x1) + STREAM (0x1) = TCP4
|
||||
(0x1, 0x1) => {
|
||||
if addr_len < 12 {
|
||||
return Err(ProxyProtocolError::Parse("IPv4 address block too short".to_string()));
|
||||
}
|
||||
let src_ip = Ipv4Addr::new(addr_block[0], addr_block[1], addr_block[2], addr_block[3]);
|
||||
let dst_ip = Ipv4Addr::new(addr_block[4], addr_block[5], addr_block[6], addr_block[7]);
|
||||
let src_port = u16::from_be_bytes([addr_block[8], addr_block[9]]);
|
||||
let dst_port = u16::from_be_bytes([addr_block[10], addr_block[11]]);
|
||||
Ok((
|
||||
ProxyProtocolHeader {
|
||||
source_addr: SocketAddr::new(IpAddr::V4(src_ip), src_port),
|
||||
dest_addr: SocketAddr::new(IpAddr::V4(dst_ip), dst_port),
|
||||
protocol: ProxyProtocol::Tcp4,
|
||||
},
|
||||
total_len,
|
||||
))
|
||||
}
|
||||
// AF_INET (0x1) + DGRAM (0x2) = UDP4
|
||||
(0x1, 0x2) => {
|
||||
if addr_len < 12 {
|
||||
return Err(ProxyProtocolError::Parse("IPv4 address block too short".to_string()));
|
||||
}
|
||||
let src_ip = Ipv4Addr::new(addr_block[0], addr_block[1], addr_block[2], addr_block[3]);
|
||||
let dst_ip = Ipv4Addr::new(addr_block[4], addr_block[5], addr_block[6], addr_block[7]);
|
||||
let src_port = u16::from_be_bytes([addr_block[8], addr_block[9]]);
|
||||
let dst_port = u16::from_be_bytes([addr_block[10], addr_block[11]]);
|
||||
Ok((
|
||||
ProxyProtocolHeader {
|
||||
source_addr: SocketAddr::new(IpAddr::V4(src_ip), src_port),
|
||||
dest_addr: SocketAddr::new(IpAddr::V4(dst_ip), dst_port),
|
||||
protocol: ProxyProtocol::Udp4,
|
||||
},
|
||||
total_len,
|
||||
))
|
||||
}
|
||||
// AF_INET6 (0x2) + STREAM (0x1) = TCP6
|
||||
(0x2, 0x1) => {
|
||||
if addr_len < 36 {
|
||||
return Err(ProxyProtocolError::Parse("IPv6 address block too short".to_string()));
|
||||
}
|
||||
let src_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&addr_block[0..16]).unwrap());
|
||||
let dst_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&addr_block[16..32]).unwrap());
|
||||
let src_port = u16::from_be_bytes([addr_block[32], addr_block[33]]);
|
||||
let dst_port = u16::from_be_bytes([addr_block[34], addr_block[35]]);
|
||||
Ok((
|
||||
ProxyProtocolHeader {
|
||||
source_addr: SocketAddr::new(IpAddr::V6(src_ip), src_port),
|
||||
dest_addr: SocketAddr::new(IpAddr::V6(dst_ip), dst_port),
|
||||
protocol: ProxyProtocol::Tcp6,
|
||||
},
|
||||
total_len,
|
||||
))
|
||||
}
|
||||
// AF_INET6 (0x2) + DGRAM (0x2) = UDP6
|
||||
(0x2, 0x2) => {
|
||||
if addr_len < 36 {
|
||||
return Err(ProxyProtocolError::Parse("IPv6 address block too short".to_string()));
|
||||
}
|
||||
let src_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&addr_block[0..16]).unwrap());
|
||||
let dst_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&addr_block[16..32]).unwrap());
|
||||
let src_port = u16::from_be_bytes([addr_block[32], addr_block[33]]);
|
||||
let dst_port = u16::from_be_bytes([addr_block[34], addr_block[35]]);
|
||||
Ok((
|
||||
ProxyProtocolHeader {
|
||||
source_addr: SocketAddr::new(IpAddr::V6(src_ip), src_port),
|
||||
dest_addr: SocketAddr::new(IpAddr::V6(dst_ip), dst_port),
|
||||
protocol: ProxyProtocol::Udp6,
|
||||
},
|
||||
total_len,
|
||||
))
|
||||
}
|
||||
// AF_UNSPEC or unknown
|
||||
(0x0, _) => Ok((
|
||||
ProxyProtocolHeader {
|
||||
source_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
dest_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
protocol: ProxyProtocol::Unknown,
|
||||
},
|
||||
total_len,
|
||||
)),
|
||||
_ => Err(ProxyProtocolError::Parse(format!(
|
||||
"Unsupported family/transport: 0x{:X}{:X}",
|
||||
family, transport
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a PROXY protocol v2 binary header.
|
||||
pub fn generate_v2(
|
||||
source: &SocketAddr,
|
||||
dest: &SocketAddr,
|
||||
transport: ProxyV2Transport,
|
||||
) -> Vec<u8> {
|
||||
let transport_nibble: u8 = match transport {
|
||||
ProxyV2Transport::Stream => 0x1,
|
||||
ProxyV2Transport::Datagram => 0x2,
|
||||
};
|
||||
|
||||
match (source.ip(), dest.ip()) {
|
||||
(IpAddr::V4(src_ip), IpAddr::V4(dst_ip)) => {
|
||||
let mut buf = Vec::with_capacity(28);
|
||||
buf.extend_from_slice(&PROXY_V2_SIGNATURE);
|
||||
buf.push(0x21); // version 2, PROXY command
|
||||
buf.push(0x10 | transport_nibble); // AF_INET + transport
|
||||
buf.extend_from_slice(&12u16.to_be_bytes()); // addr block length
|
||||
buf.extend_from_slice(&src_ip.octets());
|
||||
buf.extend_from_slice(&dst_ip.octets());
|
||||
buf.extend_from_slice(&source.port().to_be_bytes());
|
||||
buf.extend_from_slice(&dest.port().to_be_bytes());
|
||||
buf
|
||||
}
|
||||
(IpAddr::V6(src_ip), IpAddr::V6(dst_ip)) => {
|
||||
let mut buf = Vec::with_capacity(52);
|
||||
buf.extend_from_slice(&PROXY_V2_SIGNATURE);
|
||||
buf.push(0x21); // version 2, PROXY command
|
||||
buf.push(0x20 | transport_nibble); // AF_INET6 + transport
|
||||
buf.extend_from_slice(&36u16.to_be_bytes()); // addr block length
|
||||
buf.extend_from_slice(&src_ip.octets());
|
||||
buf.extend_from_slice(&dst_ip.octets());
|
||||
buf.extend_from_slice(&source.port().to_be_bytes());
|
||||
buf.extend_from_slice(&dest.port().to_be_bytes());
|
||||
buf
|
||||
}
|
||||
// Mixed IPv4/IPv6: map IPv4 to IPv6-mapped address
|
||||
_ => {
|
||||
let src_v6 = match source.ip() {
|
||||
IpAddr::V4(v4) => v4.to_ipv6_mapped(),
|
||||
IpAddr::V6(v6) => v6,
|
||||
};
|
||||
let dst_v6 = match dest.ip() {
|
||||
IpAddr::V4(v4) => v4.to_ipv6_mapped(),
|
||||
IpAddr::V6(v6) => v6,
|
||||
};
|
||||
let src6 = SocketAddr::new(IpAddr::V6(src_v6), source.port());
|
||||
let dst6 = SocketAddr::new(IpAddr::V6(dst_v6), dest.port());
|
||||
generate_v2(&src6, &dst6, transport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ===== v1 tests =====
|
||||
|
||||
#[test]
|
||||
fn test_parse_v1_tcp4() {
|
||||
let header = b"PROXY TCP4 192.168.1.100 10.0.0.1 12345 443\r\n";
|
||||
@@ -126,4 +351,130 @@ mod tests {
|
||||
assert!(is_proxy_protocol_v1(b"PROXY TCP4 ..."));
|
||||
assert!(!is_proxy_protocol_v1(b"GET / HTTP/1.1"));
|
||||
}
|
||||
|
||||
// ===== v2 tests =====
|
||||
|
||||
#[test]
|
||||
fn test_is_proxy_protocol_v2() {
|
||||
assert!(is_proxy_protocol_v2(&PROXY_V2_SIGNATURE));
|
||||
assert!(!is_proxy_protocol_v2(b"PROXY TCP4 ..."));
|
||||
assert!(!is_proxy_protocol_v2(b"short"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2_tcp4() {
|
||||
let source: SocketAddr = "198.51.100.10:54321".parse().unwrap();
|
||||
let dest: SocketAddr = "203.0.113.25:8443".parse().unwrap();
|
||||
let header = generate_v2(&source, &dest, ProxyV2Transport::Stream);
|
||||
|
||||
assert_eq!(header.len(), 28);
|
||||
let (parsed, consumed) = parse_v2(&header).unwrap();
|
||||
assert_eq!(consumed, 28);
|
||||
assert_eq!(parsed.protocol, ProxyProtocol::Tcp4);
|
||||
assert_eq!(parsed.source_addr, source);
|
||||
assert_eq!(parsed.dest_addr, dest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2_udp4() {
|
||||
let source: SocketAddr = "10.0.0.1:12345".parse().unwrap();
|
||||
let dest: SocketAddr = "10.0.0.2:53".parse().unwrap();
|
||||
let header = generate_v2(&source, &dest, ProxyV2Transport::Datagram);
|
||||
|
||||
assert_eq!(header.len(), 28);
|
||||
assert_eq!(header[13], 0x12); // AF_INET + DGRAM
|
||||
|
||||
let (parsed, consumed) = parse_v2(&header).unwrap();
|
||||
assert_eq!(consumed, 28);
|
||||
assert_eq!(parsed.protocol, ProxyProtocol::Udp4);
|
||||
assert_eq!(parsed.source_addr, source);
|
||||
assert_eq!(parsed.dest_addr, dest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2_tcp6() {
|
||||
let source: SocketAddr = "[2001:db8::1]:54321".parse().unwrap();
|
||||
let dest: SocketAddr = "[2001:db8::2]:443".parse().unwrap();
|
||||
let header = generate_v2(&source, &dest, ProxyV2Transport::Stream);
|
||||
|
||||
assert_eq!(header.len(), 52);
|
||||
assert_eq!(header[13], 0x21); // AF_INET6 + STREAM
|
||||
|
||||
let (parsed, consumed) = parse_v2(&header).unwrap();
|
||||
assert_eq!(consumed, 52);
|
||||
assert_eq!(parsed.protocol, ProxyProtocol::Tcp6);
|
||||
assert_eq!(parsed.source_addr, source);
|
||||
assert_eq!(parsed.dest_addr, dest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_v2_tcp4_byte_layout() {
|
||||
let source: SocketAddr = "1.2.3.4:1000".parse().unwrap();
|
||||
let dest: SocketAddr = "5.6.7.8:443".parse().unwrap();
|
||||
let header = generate_v2(&source, &dest, ProxyV2Transport::Stream);
|
||||
|
||||
assert_eq!(&header[0..12], &PROXY_V2_SIGNATURE);
|
||||
assert_eq!(header[12], 0x21); // v2, PROXY
|
||||
assert_eq!(header[13], 0x11); // AF_INET, STREAM
|
||||
assert_eq!(u16::from_be_bytes([header[14], header[15]]), 12); // addr len
|
||||
assert_eq!(&header[16..20], &[1, 2, 3, 4]); // src ip
|
||||
assert_eq!(&header[20..24], &[5, 6, 7, 8]); // dst ip
|
||||
assert_eq!(u16::from_be_bytes([header[24], header[25]]), 1000); // src port
|
||||
assert_eq!(u16::from_be_bytes([header[26], header[27]]), 443); // dst port
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_v2_udp4_byte_layout() {
|
||||
let source: SocketAddr = "10.0.0.1:5000".parse().unwrap();
|
||||
let dest: SocketAddr = "10.0.0.2:53".parse().unwrap();
|
||||
let header = generate_v2(&source, &dest, ProxyV2Transport::Datagram);
|
||||
|
||||
assert_eq!(header[12], 0x21); // v2, PROXY
|
||||
assert_eq!(header[13], 0x12); // AF_INET, DGRAM (UDP)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2_local_command() {
|
||||
// Build a LOCAL command header (no addresses)
|
||||
let mut header = Vec::new();
|
||||
header.extend_from_slice(&PROXY_V2_SIGNATURE);
|
||||
header.push(0x20); // v2, LOCAL
|
||||
header.push(0x00); // AF_UNSPEC
|
||||
header.extend_from_slice(&0u16.to_be_bytes()); // 0-length address block
|
||||
|
||||
let (parsed, consumed) = parse_v2(&header).unwrap();
|
||||
assert_eq!(consumed, 16);
|
||||
assert_eq!(parsed.protocol, ProxyProtocol::Unknown);
|
||||
assert_eq!(parsed.source_addr.port(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2_incomplete() {
|
||||
let data = &PROXY_V2_SIGNATURE[..8]; // only 8 bytes
|
||||
assert!(parse_v2(data).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2_wrong_version() {
|
||||
let mut header = Vec::new();
|
||||
header.extend_from_slice(&PROXY_V2_SIGNATURE);
|
||||
header.push(0x11); // version 1, not 2
|
||||
header.push(0x11);
|
||||
header.extend_from_slice(&12u16.to_be_bytes());
|
||||
header.extend_from_slice(&[0u8; 12]);
|
||||
assert!(matches!(parse_v2(&header), Err(ProxyProtocolError::UnsupportedVersion)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v2_roundtrip_with_trailing_data() {
|
||||
let source: SocketAddr = "192.168.1.1:8080".parse().unwrap();
|
||||
let dest: SocketAddr = "10.0.0.1:443".parse().unwrap();
|
||||
let mut data = generate_v2(&source, &dest, ProxyV2Transport::Stream);
|
||||
data.extend_from_slice(b"GET / HTTP/1.1\r\n"); // trailing app data
|
||||
|
||||
let (parsed, consumed) = parse_v2(&data).unwrap();
|
||||
assert_eq!(consumed, 28);
|
||||
assert_eq!(parsed.source_addr, source);
|
||||
assert_eq!(&data[consumed..], b"GET / HTTP/1.1\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,18 +573,30 @@ impl TcpListenerManager {
|
||||
Err(_) => return Err("Initial data timeout (proxy protocol peek)".into()),
|
||||
};
|
||||
|
||||
if pn > 0 && crate::proxy_protocol::is_proxy_protocol_v1(&proxy_peek[..pn]) {
|
||||
match crate::proxy_protocol::parse_v1(&proxy_peek[..pn]) {
|
||||
Ok((header, consumed)) => {
|
||||
debug!("PROXY protocol: real client {} -> {}", header.source_addr, header.dest_addr);
|
||||
effective_peer_addr = header.source_addr;
|
||||
// Consume the proxy protocol header bytes (stack buffer, max 108 bytes)
|
||||
let mut discard = [0u8; 128];
|
||||
stream.read_exact(&mut discard[..consumed]).await?;
|
||||
if pn > 0 {
|
||||
if crate::proxy_protocol::is_proxy_protocol_v1(&proxy_peek[..pn]) {
|
||||
match crate::proxy_protocol::parse_v1(&proxy_peek[..pn]) {
|
||||
Ok((header, consumed)) => {
|
||||
debug!("PROXY v1: real client {} -> {}", header.source_addr, header.dest_addr);
|
||||
effective_peer_addr = header.source_addr;
|
||||
let mut discard = [0u8; 128];
|
||||
stream.read_exact(&mut discard[..consumed]).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to parse PROXY v1 header: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to parse PROXY protocol header: {}", e);
|
||||
// Not a PROXY protocol header, continue normally
|
||||
} else if crate::proxy_protocol::is_proxy_protocol_v2(&proxy_peek[..pn]) {
|
||||
match crate::proxy_protocol::parse_v2(&proxy_peek[..pn]) {
|
||||
Ok((header, consumed)) => {
|
||||
debug!("PROXY v2: real client {} -> {} ({:?})", header.source_addr, header.dest_addr, header.protocol);
|
||||
effective_peer_addr = header.source_addr;
|
||||
let mut discard = [0u8; 256];
|
||||
stream.read_exact(&mut discard[..consumed]).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to parse PROXY v2 header: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartproxy from '../ts/index.js';
|
||||
import { ProxyProtocolParser } from '../ts/core/utils/proxy-protocol.js';
|
||||
|
||||
tap.test('PROXY protocol v1 parser - valid headers', async () => {
|
||||
// Test TCP4 format
|
||||
const tcp4Header = Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443\r\n', 'ascii');
|
||||
const tcp4Result = ProxyProtocolParser.parse(tcp4Header);
|
||||
|
||||
expect(tcp4Result.proxyInfo).property('protocol').toEqual('TCP4');
|
||||
expect(tcp4Result.proxyInfo).property('sourceIP').toEqual('192.168.1.1');
|
||||
expect(tcp4Result.proxyInfo).property('sourcePort').toEqual(56324);
|
||||
expect(tcp4Result.proxyInfo).property('destinationIP').toEqual('10.0.0.1');
|
||||
expect(tcp4Result.proxyInfo).property('destinationPort').toEqual(443);
|
||||
expect(tcp4Result.remainingData.length).toEqual(0);
|
||||
|
||||
// Test TCP6 format
|
||||
const tcp6Header = Buffer.from('PROXY TCP6 2001:db8::1 2001:db8::2 56324 443\r\n', 'ascii');
|
||||
const tcp6Result = ProxyProtocolParser.parse(tcp6Header);
|
||||
|
||||
expect(tcp6Result.proxyInfo).property('protocol').toEqual('TCP6');
|
||||
expect(tcp6Result.proxyInfo).property('sourceIP').toEqual('2001:db8::1');
|
||||
expect(tcp6Result.proxyInfo).property('sourcePort').toEqual(56324);
|
||||
expect(tcp6Result.proxyInfo).property('destinationIP').toEqual('2001:db8::2');
|
||||
expect(tcp6Result.proxyInfo).property('destinationPort').toEqual(443);
|
||||
|
||||
// Test UNKNOWN protocol
|
||||
const unknownHeader = Buffer.from('PROXY UNKNOWN\r\n', 'ascii');
|
||||
const unknownResult = ProxyProtocolParser.parse(unknownHeader);
|
||||
|
||||
expect(unknownResult.proxyInfo).property('protocol').toEqual('UNKNOWN');
|
||||
expect(unknownResult.proxyInfo).property('sourceIP').toEqual('');
|
||||
expect(unknownResult.proxyInfo).property('sourcePort').toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PROXY protocol v1 parser - with remaining data', async () => {
|
||||
const headerWithData = Buffer.concat([
|
||||
Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443\r\n', 'ascii'),
|
||||
Buffer.from('GET / HTTP/1.1\r\n', 'ascii')
|
||||
]);
|
||||
|
||||
const result = ProxyProtocolParser.parse(headerWithData);
|
||||
|
||||
expect(result.proxyInfo).property('protocol').toEqual('TCP4');
|
||||
expect(result.proxyInfo).property('sourceIP').toEqual('192.168.1.1');
|
||||
expect(result.remainingData.toString()).toEqual('GET / HTTP/1.1\r\n');
|
||||
});
|
||||
|
||||
tap.test('PROXY protocol v1 parser - invalid headers', async () => {
|
||||
// Not a PROXY protocol header
|
||||
const notProxy = Buffer.from('GET / HTTP/1.1\r\n', 'ascii');
|
||||
const notProxyResult = ProxyProtocolParser.parse(notProxy);
|
||||
expect(notProxyResult.proxyInfo).toBeNull();
|
||||
expect(notProxyResult.remainingData).toEqual(notProxy);
|
||||
|
||||
// Invalid protocol
|
||||
expect(() => {
|
||||
ProxyProtocolParser.parse(Buffer.from('PROXY INVALID 1.1.1.1 2.2.2.2 80 443\r\n', 'ascii'));
|
||||
}).toThrow();
|
||||
|
||||
// Wrong number of fields
|
||||
expect(() => {
|
||||
ProxyProtocolParser.parse(Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324\r\n', 'ascii'));
|
||||
}).toThrow();
|
||||
|
||||
// Invalid port
|
||||
expect(() => {
|
||||
ProxyProtocolParser.parse(Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 99999 443\r\n', 'ascii'));
|
||||
}).toThrow();
|
||||
|
||||
// Invalid IP for protocol
|
||||
expect(() => {
|
||||
ProxyProtocolParser.parse(Buffer.from('PROXY TCP4 2001:db8::1 10.0.0.1 56324 443\r\n', 'ascii'));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
tap.test('PROXY protocol v1 parser - incomplete headers', async () => {
|
||||
// Header without terminator
|
||||
const incomplete = Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443', 'ascii');
|
||||
const result = ProxyProtocolParser.parse(incomplete);
|
||||
|
||||
expect(result.proxyInfo).toBeNull();
|
||||
expect(result.remainingData).toEqual(incomplete);
|
||||
|
||||
// Header exceeding max length - create a buffer that actually starts with PROXY
|
||||
const longHeader = Buffer.from('PROXY TCP4 ' + '1'.repeat(100), 'ascii');
|
||||
expect(() => {
|
||||
ProxyProtocolParser.parse(longHeader);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
tap.test('PROXY protocol v1 generator', async () => {
|
||||
// Generate TCP4 header
|
||||
const tcp4Info = {
|
||||
protocol: 'TCP4' as const,
|
||||
sourceIP: '192.168.1.1',
|
||||
sourcePort: 56324,
|
||||
destinationIP: '10.0.0.1',
|
||||
destinationPort: 443
|
||||
};
|
||||
|
||||
const tcp4Header = ProxyProtocolParser.generate(tcp4Info);
|
||||
expect(tcp4Header.toString('ascii')).toEqual('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443\r\n');
|
||||
|
||||
// Generate TCP6 header
|
||||
const tcp6Info = {
|
||||
protocol: 'TCP6' as const,
|
||||
sourceIP: '2001:db8::1',
|
||||
sourcePort: 56324,
|
||||
destinationIP: '2001:db8::2',
|
||||
destinationPort: 443
|
||||
};
|
||||
|
||||
const tcp6Header = ProxyProtocolParser.generate(tcp6Info);
|
||||
expect(tcp6Header.toString('ascii')).toEqual('PROXY TCP6 2001:db8::1 2001:db8::2 56324 443\r\n');
|
||||
|
||||
// Generate UNKNOWN header
|
||||
const unknownInfo = {
|
||||
protocol: 'UNKNOWN' as const,
|
||||
sourceIP: '',
|
||||
sourcePort: 0,
|
||||
destinationIP: '',
|
||||
destinationPort: 0
|
||||
};
|
||||
|
||||
const unknownHeader = ProxyProtocolParser.generate(unknownInfo);
|
||||
expect(unknownHeader.toString('ascii')).toEqual('PROXY UNKNOWN\r\n');
|
||||
});
|
||||
|
||||
// Skipping integration tests for now - focus on unit tests
|
||||
// Integration tests would require more complex setup and teardown
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '25.11.24',
|
||||
version: '25.12.0',
|
||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
||||
@@ -15,4 +15,3 @@ export * from './lifecycle-component.js';
|
||||
export * from './binary-heap.js';
|
||||
export * from './enhanced-connection-pool.js';
|
||||
export * from './socket-utils.js';
|
||||
export * from './proxy-protocol.js';
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { logger } from './logger.js';
|
||||
import { ProxyProtocolParser as ProtocolParser, type IProxyInfo, type IProxyParseResult } from '../../protocols/proxy/index.js';
|
||||
|
||||
// Re-export types from protocols for backward compatibility
|
||||
export type { IProxyInfo, IProxyParseResult } from '../../protocols/proxy/index.js';
|
||||
|
||||
/**
|
||||
* Parser for PROXY protocol v1 (text format)
|
||||
* Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
*
|
||||
* This class now delegates to the protocol parser but adds
|
||||
* smartproxy-specific features like socket reading and logging
|
||||
*/
|
||||
export class ProxyProtocolParser {
|
||||
static readonly PROXY_V1_SIGNATURE = ProtocolParser.PROXY_V1_SIGNATURE;
|
||||
static readonly MAX_HEADER_LENGTH = ProtocolParser.MAX_HEADER_LENGTH;
|
||||
static readonly HEADER_TERMINATOR = ProtocolParser.HEADER_TERMINATOR;
|
||||
|
||||
/**
|
||||
* Parse PROXY protocol v1 header from buffer
|
||||
* Returns proxy info and remaining data after header
|
||||
*/
|
||||
static parse(data: Buffer): IProxyParseResult {
|
||||
// Delegate to protocol parser
|
||||
return ProtocolParser.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PROXY protocol v1 header
|
||||
*/
|
||||
static generate(info: IProxyInfo): Buffer {
|
||||
// Delegate to protocol parser
|
||||
return ProtocolParser.generate(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IP address format
|
||||
*/
|
||||
private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean {
|
||||
return ProtocolParser.isValidIP(ip, protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to read a complete PROXY protocol header from a socket
|
||||
* Returns null if no PROXY protocol detected or incomplete
|
||||
*/
|
||||
static async readFromSocket(socket: plugins.net.Socket, timeout: number = 5000): Promise<IProxyParseResult | null> {
|
||||
return new Promise((resolve) => {
|
||||
let buffer = Buffer.alloc(0);
|
||||
let resolved = false;
|
||||
|
||||
const cleanup = () => {
|
||||
socket.removeListener('data', onData);
|
||||
socket.removeListener('error', onError);
|
||||
clearTimeout(timer);
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve({
|
||||
proxyInfo: null,
|
||||
remainingData: buffer
|
||||
});
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
const onData = (chunk: Buffer) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
|
||||
// Check if we have enough data
|
||||
if (!buffer.toString('ascii', 0, Math.min(6, buffer.length)).startsWith(this.PROXY_V1_SIGNATURE)) {
|
||||
// Not PROXY protocol
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve({
|
||||
proxyInfo: null,
|
||||
remainingData: buffer
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse
|
||||
try {
|
||||
const result = this.parse(buffer);
|
||||
if (result.proxyInfo) {
|
||||
// Successfully parsed
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve(result);
|
||||
} else if (buffer.length > this.MAX_HEADER_LENGTH) {
|
||||
// Header too long
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve({
|
||||
proxyInfo: null,
|
||||
remainingData: buffer
|
||||
});
|
||||
}
|
||||
// Otherwise continue reading
|
||||
} catch (error) {
|
||||
// Parse error
|
||||
logger.log('error', `PROXY protocol parse error: ${error.message}`);
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve({
|
||||
proxyInfo: null,
|
||||
remainingData: buffer
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (error: Error) => {
|
||||
logger.log('error', `Socket error while reading PROXY protocol: ${error.message}`);
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve({
|
||||
proxyInfo: null,
|
||||
remainingData: buffer
|
||||
});
|
||||
};
|
||||
|
||||
socket.on('data', onData);
|
||||
socket.on('error', onError);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* PROXY Protocol Module
|
||||
* HAProxy PROXY protocol implementation
|
||||
* Type definitions for HAProxy PROXY protocol v1/v2
|
||||
*/
|
||||
|
||||
export * from './types.js';
|
||||
export * from './parser.js';
|
||||
export * from './types.js';
|
||||
@@ -1,183 +0,0 @@
|
||||
/**
|
||||
* PROXY Protocol Parser
|
||||
* Implementation of HAProxy PROXY protocol v1 (text format)
|
||||
* Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
*/
|
||||
|
||||
import type { IProxyInfo, IProxyParseResult, TProxyProtocol } from './types.js';
|
||||
|
||||
/**
|
||||
* PROXY protocol parser
|
||||
*/
|
||||
export class ProxyProtocolParser {
|
||||
static readonly PROXY_V1_SIGNATURE = 'PROXY ';
|
||||
static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header
|
||||
static readonly HEADER_TERMINATOR = '\r\n';
|
||||
|
||||
/**
|
||||
* Parse PROXY protocol v1 header from buffer
|
||||
* Returns proxy info and remaining data after header
|
||||
*/
|
||||
static parse(data: Buffer): IProxyParseResult {
|
||||
// Check if buffer starts with PROXY signature
|
||||
if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) {
|
||||
return {
|
||||
proxyInfo: null,
|
||||
remainingData: data
|
||||
};
|
||||
}
|
||||
|
||||
// Find header terminator
|
||||
const headerEndIndex = data.indexOf(this.HEADER_TERMINATOR);
|
||||
if (headerEndIndex === -1) {
|
||||
// Header incomplete, need more data
|
||||
if (data.length > this.MAX_HEADER_LENGTH) {
|
||||
// Header too long, invalid
|
||||
throw new Error('PROXY protocol header exceeds maximum length');
|
||||
}
|
||||
return {
|
||||
proxyInfo: null,
|
||||
remainingData: data
|
||||
};
|
||||
}
|
||||
|
||||
// Extract header line
|
||||
const headerLine = data.toString('ascii', 0, headerEndIndex);
|
||||
const remainingData = data.slice(headerEndIndex + 2); // Skip \r\n
|
||||
|
||||
// Parse header
|
||||
const parts = headerLine.split(' ');
|
||||
|
||||
if (parts.length < 2) {
|
||||
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
|
||||
}
|
||||
|
||||
const [signature, protocol] = parts;
|
||||
|
||||
// Validate protocol
|
||||
if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(protocol)) {
|
||||
throw new Error(`Invalid PROXY protocol: ${protocol}`);
|
||||
}
|
||||
|
||||
// For UNKNOWN protocol, ignore addresses
|
||||
if (protocol === 'UNKNOWN') {
|
||||
return {
|
||||
proxyInfo: {
|
||||
protocol: 'UNKNOWN',
|
||||
sourceIP: '',
|
||||
sourcePort: 0,
|
||||
destinationIP: '',
|
||||
destinationPort: 0
|
||||
},
|
||||
remainingData
|
||||
};
|
||||
}
|
||||
|
||||
// For TCP4/TCP6, we need all 6 parts
|
||||
if (parts.length !== 6) {
|
||||
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
|
||||
}
|
||||
|
||||
const [, , srcIP, dstIP, srcPort, dstPort] = parts;
|
||||
|
||||
// Validate and parse ports
|
||||
const sourcePort = parseInt(srcPort, 10);
|
||||
const destinationPort = parseInt(dstPort, 10);
|
||||
|
||||
if (isNaN(sourcePort) || sourcePort < 0 || sourcePort > 65535) {
|
||||
throw new Error(`Invalid source port: ${srcPort}`);
|
||||
}
|
||||
|
||||
if (isNaN(destinationPort) || destinationPort < 0 || destinationPort > 65535) {
|
||||
throw new Error(`Invalid destination port: ${dstPort}`);
|
||||
}
|
||||
|
||||
// Validate IP addresses
|
||||
const protocolType = protocol as TProxyProtocol;
|
||||
if (!this.isValidIP(srcIP, protocolType)) {
|
||||
throw new Error(`Invalid source IP for ${protocol}: ${srcIP}`);
|
||||
}
|
||||
|
||||
if (!this.isValidIP(dstIP, protocolType)) {
|
||||
throw new Error(`Invalid destination IP for ${protocol}: ${dstIP}`);
|
||||
}
|
||||
|
||||
return {
|
||||
proxyInfo: {
|
||||
protocol: protocolType,
|
||||
sourceIP: srcIP,
|
||||
sourcePort,
|
||||
destinationIP: dstIP,
|
||||
destinationPort
|
||||
},
|
||||
remainingData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PROXY protocol v1 header
|
||||
*/
|
||||
static generate(info: IProxyInfo): Buffer {
|
||||
if (info.protocol === 'UNKNOWN') {
|
||||
return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii');
|
||||
}
|
||||
|
||||
const header = `PROXY ${info.protocol} ${info.sourceIP} ${info.destinationIP} ${info.sourcePort} ${info.destinationPort}\r\n`;
|
||||
|
||||
if (header.length > this.MAX_HEADER_LENGTH) {
|
||||
throw new Error('Generated PROXY protocol header exceeds maximum length');
|
||||
}
|
||||
|
||||
return Buffer.from(header, 'ascii');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IP address format
|
||||
*/
|
||||
static isValidIP(ip: string, protocol: TProxyProtocol): boolean {
|
||||
if (protocol === 'TCP4') {
|
||||
return this.isIPv4(ip);
|
||||
} else if (protocol === 'TCP6') {
|
||||
return this.isIPv6(ip);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string is valid IPv4
|
||||
*/
|
||||
static isIPv4(ip: string): boolean {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length !== 4) return false;
|
||||
|
||||
for (const part of parts) {
|
||||
const num = parseInt(part, 10);
|
||||
if (isNaN(num) || num < 0 || num > 255 || part !== num.toString()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string is valid IPv6
|
||||
*/
|
||||
static isIPv6(ip: string): boolean {
|
||||
// Basic IPv6 validation
|
||||
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
||||
return ipv6Regex.test(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a connection ID string for tracking
|
||||
*/
|
||||
static createConnectionId(connectionInfo: {
|
||||
sourceIp?: string;
|
||||
sourcePort?: number;
|
||||
destIp?: string;
|
||||
destPort?: number;
|
||||
}): string {
|
||||
const { sourceIp, sourcePort, destIp, destPort } = connectionInfo;
|
||||
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export type TProxyProtocolVersion = 'v1' | 'v2';
|
||||
/**
|
||||
* Connection protocol type
|
||||
*/
|
||||
export type TProxyProtocol = 'TCP4' | 'TCP6' | 'UNKNOWN';
|
||||
export type TProxyProtocol = 'TCP4' | 'TCP6' | 'UDP4' | 'UDP6' | 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Interface representing parsed PROXY protocol information
|
||||
|
||||
Reference in New Issue
Block a user