Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfa958cf3d | |||
| db2e586da2 |
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-03-17 - 25.11.24 - fix(rustproxy-http)
|
||||||
improve async static file serving, websocket handshake buffering, and shared metric metadata handling
|
improve async static file serving, websocket handshake buffering, and shared metric metadata handling
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "25.11.24",
|
"version": "25.12.0",
|
||||||
"private": false,
|
"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.",
|
"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",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -9,9 +9,11 @@ pub enum ProxyProtocolError {
|
|||||||
UnsupportedVersion,
|
UnsupportedVersion,
|
||||||
#[error("Parse error: {0}")]
|
#[error("Parse error: {0}")]
|
||||||
Parse(String),
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProxyProtocolHeader {
|
pub struct ProxyProtocolHeader {
|
||||||
pub source_addr: SocketAddr,
|
pub source_addr: SocketAddr,
|
||||||
@@ -24,14 +26,29 @@ pub struct ProxyProtocolHeader {
|
|||||||
pub enum ProxyProtocol {
|
pub enum ProxyProtocol {
|
||||||
Tcp4,
|
Tcp4,
|
||||||
Tcp6,
|
Tcp6,
|
||||||
|
Udp4,
|
||||||
|
Udp6,
|
||||||
Unknown,
|
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.
|
/// Parse a PROXY protocol v1 header from data.
|
||||||
///
|
///
|
||||||
/// Format: `PROXY TCP4 <src_ip> <dst_ip> <src_port> <dst_port>\r\n`
|
/// Format: `PROXY TCP4 <src_ip> <dst_ip> <src_port> <dst_port>\r\n`
|
||||||
pub fn parse_v1(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtocolError> {
|
pub fn parse_v1(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtocolError> {
|
||||||
// Find the end of the header line
|
|
||||||
let line_end = data
|
let line_end = data
|
||||||
.windows(2)
|
.windows(2)
|
||||||
.position(|w| w == b"\r\n")
|
.position(|w| w == b"\r\n")
|
||||||
@@ -56,10 +73,10 @@ pub fn parse_v1(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtoc
|
|||||||
_ => return Err(ProxyProtocolError::UnsupportedVersion),
|
_ => return Err(ProxyProtocolError::UnsupportedVersion),
|
||||||
};
|
};
|
||||||
|
|
||||||
let src_ip: std::net::IpAddr = parts[2]
|
let src_ip: IpAddr = parts[2]
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| ProxyProtocolError::Parse("Invalid source IP".to_string()))?;
|
.map_err(|_| ProxyProtocolError::Parse("Invalid source IP".to_string()))?;
|
||||||
let dst_ip: std::net::IpAddr = parts[3]
|
let dst_ip: IpAddr = parts[3]
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| ProxyProtocolError::Parse("Invalid destination IP".to_string()))?;
|
.map_err(|_| ProxyProtocolError::Parse("Invalid destination IP".to_string()))?;
|
||||||
let src_port: u16 = parts[4]
|
let src_port: u16 = parts[4]
|
||||||
@@ -75,7 +92,6 @@ pub fn parse_v1(data: &[u8]) -> Result<(ProxyProtocolHeader, usize), ProxyProtoc
|
|||||||
protocol,
|
protocol,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Consumed bytes = line + \r\n
|
|
||||||
Ok((header, line_end + 2))
|
Ok((header, line_end + 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +113,219 @@ pub fn is_proxy_protocol_v1(data: &[u8]) -> bool {
|
|||||||
data.starts_with(b"PROXY ")
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
// ===== v1 tests =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_v1_tcp4() {
|
fn test_parse_v1_tcp4() {
|
||||||
let header = b"PROXY TCP4 192.168.1.100 10.0.0.1 12345 443\r\n";
|
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"PROXY TCP4 ..."));
|
||||||
assert!(!is_proxy_protocol_v1(b"GET / HTTP/1.1"));
|
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()),
|
Err(_) => return Err("Initial data timeout (proxy protocol peek)".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if pn > 0 && crate::proxy_protocol::is_proxy_protocol_v1(&proxy_peek[..pn]) {
|
if pn > 0 {
|
||||||
match crate::proxy_protocol::parse_v1(&proxy_peek[..pn]) {
|
if crate::proxy_protocol::is_proxy_protocol_v1(&proxy_peek[..pn]) {
|
||||||
Ok((header, consumed)) => {
|
match crate::proxy_protocol::parse_v1(&proxy_peek[..pn]) {
|
||||||
debug!("PROXY protocol: real client {} -> {}", header.source_addr, header.dest_addr);
|
Ok((header, consumed)) => {
|
||||||
effective_peer_addr = header.source_addr;
|
debug!("PROXY v1: real client {} -> {}", header.source_addr, header.dest_addr);
|
||||||
// Consume the proxy protocol header bytes (stack buffer, max 108 bytes)
|
effective_peer_addr = header.source_addr;
|
||||||
let mut discard = [0u8; 128];
|
let mut discard = [0u8; 128];
|
||||||
stream.read_exact(&mut discard[..consumed]).await?;
|
stream.read_exact(&mut discard[..consumed]).await?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to parse PROXY v1 header: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
} else if crate::proxy_protocol::is_proxy_protocol_v2(&proxy_peek[..pn]) {
|
||||||
debug!("Failed to parse PROXY protocol header: {}", e);
|
match crate::proxy_protocol::parse_v2(&proxy_peek[..pn]) {
|
||||||
// Not a PROXY protocol header, continue normally
|
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 = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
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.'
|
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 './binary-heap.js';
|
||||||
export * from './enhanced-connection-pool.js';
|
export * from './enhanced-connection-pool.js';
|
||||||
export * from './socket-utils.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
|
* PROXY Protocol Module
|
||||||
* HAProxy PROXY protocol implementation
|
* Type definitions for HAProxy PROXY protocol v1/v2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './types.js';
|
export * from './types.js';
|
||||||
export * from './parser.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
|
* Connection protocol type
|
||||||
*/
|
*/
|
||||||
export type TProxyProtocol = 'TCP4' | 'TCP6' | 'UNKNOWN';
|
export type TProxyProtocol = 'TCP4' | 'TCP6' | 'UDP4' | 'UDP6' | 'UNKNOWN';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface representing parsed PROXY protocol information
|
* Interface representing parsed PROXY protocol information
|
||||||
|
|||||||
Reference in New Issue
Block a user