Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c2042a2f5 | |||
| 3514260316 | |||
| f171cc8c5d | |||
| c7722c30f3 |
17
changelog.md
17
changelog.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-19 - 25.7.7 - fix(proxy)
|
||||||
|
restrict PROXY protocol parsing to configured trusted proxy IPs and parse PROXY headers before metrics/fast-path so client IPs reflect the real source
|
||||||
|
|
||||||
|
- Add proxy_ips: Vec<std::net::IpAddr> to ConnectionConfig with a default empty Vec
|
||||||
|
- Populate proxy_ips from options.proxy_ips strings in rust/crates/rustproxy/src/lib.rs, parsing each to IpAddr
|
||||||
|
- Only peek for and parse PROXY v1 headers when the remote IP is contained in proxy_ips (prevents untrusted clients from injecting PROXY headers)
|
||||||
|
- Move PROXY protocol parsing earlier so metrics and fast-path logic use the effective (real client) IP after PROXY parsing
|
||||||
|
- If proxy_ips is empty, behavior remains unchanged (no PROXY parsing)
|
||||||
|
|
||||||
|
## 2026-02-19 - 25.7.6 - fix(throughput)
|
||||||
|
add tests for per-IP connection tracking and throughput history; assert per-IP eviction after connection close to prevent memory leak
|
||||||
|
|
||||||
|
- Adds runtime assertions for per-IP TCP connection tracking (m.connections.byIP) while a connection is active
|
||||||
|
- Adds checks for throughput history (m.throughput.history) to ensure history length and timestamps are recorded
|
||||||
|
- Asserts that per-IP tracking data is evicted after connection close (byIP.size === 0) to verify memory leak fix
|
||||||
|
- Reorders test checks so per-IP and history metrics are validated during the active connection and totals are validated after close
|
||||||
|
|
||||||
## 2026-02-19 - 25.7.5 - fix(rustproxy)
|
## 2026-02-19 - 25.7.5 - fix(rustproxy)
|
||||||
prune stale per-route metrics, add per-route rate limiter caching and regex cache, and improve connection tracking cleanup to prevent memory growth
|
prune stale per-route metrics, add per-route rate limiter caching and regex cache, and improve connection tracking cleanup to prevent memory growth
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "25.7.5",
|
"version": "25.7.7",
|
||||||
"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",
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ pub struct ConnectionConfig {
|
|||||||
pub accept_proxy_protocol: bool,
|
pub accept_proxy_protocol: bool,
|
||||||
/// Whether to send PROXY protocol
|
/// Whether to send PROXY protocol
|
||||||
pub send_proxy_protocol: bool,
|
pub send_proxy_protocol: bool,
|
||||||
|
/// Trusted IPs that may send PROXY protocol headers.
|
||||||
|
/// When non-empty, only connections from these IPs will have PROXY headers parsed.
|
||||||
|
pub proxy_ips: Vec<std::net::IpAddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConnectionConfig {
|
impl Default for ConnectionConfig {
|
||||||
@@ -101,6 +104,7 @@ impl Default for ConnectionConfig {
|
|||||||
extended_keep_alive_lifetime_ms: None,
|
extended_keep_alive_lifetime_ms: None,
|
||||||
accept_proxy_protocol: false,
|
accept_proxy_protocol: false,
|
||||||
send_proxy_protocol: false,
|
send_proxy_protocol: false,
|
||||||
|
proxy_ips: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,7 +419,41 @@ impl TcpListenerManager {
|
|||||||
|
|
||||||
stream.set_nodelay(true)?;
|
stream.set_nodelay(true)?;
|
||||||
|
|
||||||
// Extract source IP once for all metric calls
|
// --- PROXY protocol: must happen BEFORE ip_str and fast path ---
|
||||||
|
// Only parse PROXY headers from trusted proxy IPs (security).
|
||||||
|
// Non-proxy connections skip the peek entirely (no latency cost).
|
||||||
|
let mut effective_peer_addr = peer_addr;
|
||||||
|
if !conn_config.proxy_ips.is_empty() && conn_config.proxy_ips.contains(&peer_addr.ip()) {
|
||||||
|
// Trusted proxy IP — peek for PROXY protocol header
|
||||||
|
let mut proxy_peek = vec![0u8; 256];
|
||||||
|
let pn = match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(conn_config.initial_data_timeout_ms),
|
||||||
|
stream.peek(&mut proxy_peek),
|
||||||
|
).await {
|
||||||
|
Ok(Ok(n)) => n,
|
||||||
|
Ok(Err(e)) => return Err(e.into()),
|
||||||
|
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
|
||||||
|
let mut discard = vec![0u8; consumed];
|
||||||
|
stream.read_exact(&mut discard).await?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to parse PROXY protocol header: {}", e);
|
||||||
|
// Not a PROXY protocol header, continue normally
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let peer_addr = effective_peer_addr;
|
||||||
|
|
||||||
|
// Extract source IP once for all metric calls (reflects real client IP after PROXY parsing)
|
||||||
let ip_str = peer_addr.ip().to_string();
|
let ip_str = peer_addr.ip().to_string();
|
||||||
|
|
||||||
// === Fast path: try port-only matching before peeking at data ===
|
// === Fast path: try port-only matching before peeking at data ===
|
||||||
@@ -548,37 +586,6 @@ impl TcpListenerManager {
|
|||||||
}
|
}
|
||||||
// === End fast path ===
|
// === End fast path ===
|
||||||
|
|
||||||
// Handle PROXY protocol if configured
|
|
||||||
let mut effective_peer_addr = peer_addr;
|
|
||||||
if conn_config.accept_proxy_protocol {
|
|
||||||
let mut proxy_peek = vec![0u8; 256];
|
|
||||||
let pn = match tokio::time::timeout(
|
|
||||||
std::time::Duration::from_millis(conn_config.initial_data_timeout_ms),
|
|
||||||
stream.peek(&mut proxy_peek),
|
|
||||||
).await {
|
|
||||||
Ok(Ok(n)) => n,
|
|
||||||
Ok(Err(e)) => return Err(e.into()),
|
|
||||||
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
|
|
||||||
let mut discard = vec![0u8; consumed];
|
|
||||||
stream.read_exact(&mut discard).await?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to parse PROXY protocol header: {}", e);
|
|
||||||
// Not a PROXY protocol header, continue normally
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let peer_addr = effective_peer_addr;
|
|
||||||
|
|
||||||
// Peek at initial bytes with timeout
|
// Peek at initial bytes with timeout
|
||||||
let mut peek_buf = vec![0u8; 4096];
|
let mut peek_buf = vec![0u8; 4096];
|
||||||
let n = match tokio::time::timeout(
|
let n = match tokio::time::timeout(
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ impl RustProxy {
|
|||||||
extended_keep_alive_lifetime_ms: options.extended_keep_alive_lifetime,
|
extended_keep_alive_lifetime_ms: options.extended_keep_alive_lifetime,
|
||||||
accept_proxy_protocol: options.accept_proxy_protocol.unwrap_or(false),
|
accept_proxy_protocol: options.accept_proxy_protocol.unwrap_or(false),
|
||||||
send_proxy_protocol: options.send_proxy_protocol.unwrap_or(false),
|
send_proxy_protocol: options.send_proxy_protocol.unwrap_or(false),
|
||||||
|
proxy_ips: options.proxy_ips.as_deref().unwrap_or(&[])
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.parse::<std::net::IpAddr>().ok())
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,28 @@ tap.test('TCP forward - real-time byte tracking', async (tools) => {
|
|||||||
console.log(`TCP forward (during) — recent throughput: in=${tpDuring.in}, out=${tpDuring.out}`);
|
console.log(`TCP forward (during) — recent throughput: in=${tpDuring.in}, out=${tpDuring.out}`);
|
||||||
expect(tpDuring.in + tpDuring.out).toBeGreaterThan(0);
|
expect(tpDuring.in + tpDuring.out).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// ── v25.2.0: Per-IP tracking (TCP connections) ──
|
||||||
|
// Must check WHILE connection is active — per-IP data is evicted on last close
|
||||||
|
const byIP = mDuring.connections.byIP();
|
||||||
|
console.log('TCP forward — connections byIP:', Array.from(byIP.entries()));
|
||||||
|
expect(byIP.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const topIPs = mDuring.connections.topIPs(10);
|
||||||
|
console.log('TCP forward — topIPs:', topIPs);
|
||||||
|
expect(topIPs.length).toBeGreaterThan(0);
|
||||||
|
expect(topIPs[0].ip).toBeTruthy();
|
||||||
|
|
||||||
|
// ── v25.2.0: Throughput history ──
|
||||||
|
const history = mDuring.throughput.history(10);
|
||||||
|
console.log('TCP forward — throughput history length:', history.length);
|
||||||
|
expect(history.length).toBeGreaterThan(0);
|
||||||
|
expect(history[0].timestamp).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Close connection
|
// Close connection
|
||||||
client.destroy();
|
client.destroy();
|
||||||
await tools.delayFor(500);
|
await tools.delayFor(500);
|
||||||
|
|
||||||
// Final check
|
// Final check — totals persist even after connection close
|
||||||
await pollMetrics(proxy);
|
await pollMetrics(proxy);
|
||||||
const m = proxy.getMetrics();
|
const m = proxy.getMetrics();
|
||||||
const bytesIn = m.totals.bytesIn();
|
const bytesIn = m.totals.bytesIn();
|
||||||
@@ -168,21 +185,10 @@ tap.test('TCP forward - real-time byte tracking', async (tools) => {
|
|||||||
const byRoute = m.throughput.byRoute();
|
const byRoute = m.throughput.byRoute();
|
||||||
console.log('TCP forward — throughput byRoute:', Array.from(byRoute.entries()));
|
console.log('TCP forward — throughput byRoute:', Array.from(byRoute.entries()));
|
||||||
|
|
||||||
// ── v25.2.0: Per-IP tracking (TCP connections) ──
|
// After close, per-IP data should be evicted (memory leak fix)
|
||||||
const byIP = m.connections.byIP();
|
const byIPAfter = m.connections.byIP();
|
||||||
console.log('TCP forward — connections byIP:', Array.from(byIP.entries()));
|
console.log('TCP forward — connections byIP after close:', Array.from(byIPAfter.entries()));
|
||||||
expect(byIP.size).toBeGreaterThan(0);
|
expect(byIPAfter.size).toEqual(0);
|
||||||
|
|
||||||
const topIPs = m.connections.topIPs(10);
|
|
||||||
console.log('TCP forward — topIPs:', topIPs);
|
|
||||||
expect(topIPs.length).toBeGreaterThan(0);
|
|
||||||
expect(topIPs[0].ip).toBeTruthy();
|
|
||||||
|
|
||||||
// ── v25.2.0: Throughput history ──
|
|
||||||
const history = m.throughput.history(10);
|
|
||||||
console.log('TCP forward — throughput history length:', history.length);
|
|
||||||
expect(history.length).toBeGreaterThan(0);
|
|
||||||
expect(history[0].timestamp).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
await proxy.stop();
|
await proxy.stop();
|
||||||
await tools.delayFor(200);
|
await tools.delayFor(200);
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '25.7.5',
|
version: '25.7.7',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user