Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc9e76fade | |||
8df0333dc3 | |||
22418cd65e | |||
86b016cac3 | |||
e81d0386d6 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "19.6.8",
|
"version": "19.6.10",
|
||||||
"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",
|
||||||
|
169
readme.byte-counting-audit.md
Normal file
169
readme.byte-counting-audit.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# SmartProxy Byte Counting Audit Report
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
After a comprehensive audit of the SmartProxy codebase, I can confirm that **byte counting is implemented correctly** with no instances of double counting. Each byte transferred through the proxy is counted exactly once in each direction.
|
||||||
|
|
||||||
|
## Byte Counting Implementation
|
||||||
|
|
||||||
|
### 1. Core Tracking Mechanisms
|
||||||
|
|
||||||
|
SmartProxy uses two complementary tracking systems:
|
||||||
|
|
||||||
|
1. **Connection Records** (`IConnectionRecord`):
|
||||||
|
- `bytesReceived`: Total bytes received from client
|
||||||
|
- `bytesSent`: Total bytes sent to client
|
||||||
|
|
||||||
|
2. **MetricsCollector**:
|
||||||
|
- Global throughput tracking via `ThroughputTracker`
|
||||||
|
- Per-connection byte tracking for route/IP metrics
|
||||||
|
- Called via `recordBytes(connectionId, bytesIn, bytesOut)`
|
||||||
|
|
||||||
|
### 2. Where Bytes Are Counted
|
||||||
|
|
||||||
|
Bytes are counted in only two files:
|
||||||
|
|
||||||
|
#### a) `route-connection-handler.ts`
|
||||||
|
- **Line 351**: TLS alert bytes when no SNI is provided
|
||||||
|
- **Lines 1286-1301**: Data forwarding callbacks in `setupBidirectionalForwarding()`
|
||||||
|
|
||||||
|
#### b) `http-proxy-bridge.ts`
|
||||||
|
- **Line 127**: Initial TLS chunk for HttpProxy connections
|
||||||
|
- **Lines 142-154**: Data forwarding callbacks in `setupBidirectionalForwarding()`
|
||||||
|
|
||||||
|
## Connection Flow Analysis
|
||||||
|
|
||||||
|
### 1. Direct TCP Connection (No TLS)
|
||||||
|
|
||||||
|
```
|
||||||
|
Client → SmartProxy → Target Server
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Connection arrives at `RouteConnectionHandler.handleConnection()`
|
||||||
|
2. For non-TLS ports, immediately routes via `routeConnection()`
|
||||||
|
3. `setupDirectConnection()` creates target connection
|
||||||
|
4. `setupBidirectionalForwarding()` handles all data transfer:
|
||||||
|
- `onClientData`: `bytesReceived += chunk.length` + `recordBytes(chunk.length, 0)`
|
||||||
|
- `onServerData`: `bytesSent += chunk.length` + `recordBytes(0, chunk.length)`
|
||||||
|
|
||||||
|
**Result**: ✅ Each byte counted exactly once
|
||||||
|
|
||||||
|
### 2. TLS Passthrough Connection
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (TLS) → SmartProxy → Target Server (TLS)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Connection waits for initial data to detect TLS
|
||||||
|
2. TLS handshake detected, SNI extracted
|
||||||
|
3. Route matched, `setupDirectConnection()` called
|
||||||
|
4. Initial chunk stored in `pendingData` (NOT counted yet)
|
||||||
|
5. On target connect, `pendingData` written to target (still not counted)
|
||||||
|
6. `setupBidirectionalForwarding()` counts ALL bytes including initial chunk
|
||||||
|
|
||||||
|
**Result**: ✅ Each byte counted exactly once
|
||||||
|
|
||||||
|
### 3. TLS Termination via HttpProxy
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (TLS) → SmartProxy → HttpProxy (localhost) → Target Server
|
||||||
|
```
|
||||||
|
|
||||||
|
1. TLS connection detected with `tls.mode = "terminate"`
|
||||||
|
2. `forwardToHttpProxy()` called:
|
||||||
|
- Initial chunk: `bytesReceived += chunk.length` + `recordBytes(chunk.length, 0)`
|
||||||
|
3. Proxy connection created to HttpProxy on localhost
|
||||||
|
4. `setupBidirectionalForwarding()` handles subsequent data
|
||||||
|
|
||||||
|
**Result**: ✅ Each byte counted exactly once
|
||||||
|
|
||||||
|
### 4. HTTP Connection via HttpProxy
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (HTTP) → SmartProxy → HttpProxy (localhost) → Target Server
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Connection on configured HTTP port (`useHttpProxy` ports)
|
||||||
|
2. Same flow as TLS termination
|
||||||
|
3. All byte counting identical to TLS termination
|
||||||
|
|
||||||
|
**Result**: ✅ Each byte counted exactly once
|
||||||
|
|
||||||
|
### 5. NFTables Forwarding
|
||||||
|
|
||||||
|
```
|
||||||
|
Client → [Kernel NFTables] → Target Server
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Connection detected, route matched with `forwardingEngine: 'nftables'`
|
||||||
|
2. Connection marked as `usingNetworkProxy = true`
|
||||||
|
3. NO application-level forwarding (kernel handles packet routing)
|
||||||
|
4. NO byte counting in application layer
|
||||||
|
|
||||||
|
**Result**: ✅ No counting (correct - kernel handles everything)
|
||||||
|
|
||||||
|
## Special Cases
|
||||||
|
|
||||||
|
### PROXY Protocol
|
||||||
|
- PROXY protocol headers sent to backend servers are NOT counted in client metrics
|
||||||
|
- Only actual client data is counted
|
||||||
|
- **Correct behavior**: Protocol overhead is not client data
|
||||||
|
|
||||||
|
### TLS Alerts
|
||||||
|
- TLS alerts (e.g., for missing SNI) are counted as sent bytes
|
||||||
|
- **Correct behavior**: Alerts are actual data sent to the client
|
||||||
|
|
||||||
|
### Initial Chunks
|
||||||
|
- **Direct connections**: Stored in `pendingData`, counted when forwarded
|
||||||
|
- **HttpProxy connections**: Counted immediately upon receipt
|
||||||
|
- **Both approaches**: Count each byte exactly once
|
||||||
|
|
||||||
|
## Verification Methodology
|
||||||
|
|
||||||
|
1. **Code Analysis**: Searched for all instances of:
|
||||||
|
- `bytesReceived +=` and `bytesSent +=`
|
||||||
|
- `recordBytes()` calls
|
||||||
|
- Data forwarding implementations
|
||||||
|
|
||||||
|
2. **Flow Tracing**: Followed data path for each connection type from entry to exit
|
||||||
|
|
||||||
|
3. **Handler Review**: Examined all forwarding handlers to ensure no additional counting
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### ✅ No Double Counting Detected
|
||||||
|
|
||||||
|
- Each byte is counted exactly once in the direction it flows
|
||||||
|
- Connection records and metrics are updated consistently
|
||||||
|
- No overlapping or duplicate counting logic found
|
||||||
|
|
||||||
|
### Areas of Excellence
|
||||||
|
|
||||||
|
1. **Centralized Counting**: All byte counting happens in just two files
|
||||||
|
2. **Consistent Pattern**: Uses `setupBidirectionalForwarding()` with callbacks
|
||||||
|
3. **Clear Separation**: Forwarding handlers don't interfere with proxy metrics
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
1. **Debug Logging**: Add optional debug logging to verify byte counts in production:
|
||||||
|
```typescript
|
||||||
|
if (settings.debugByteCount) {
|
||||||
|
logger.log('debug', `Bytes counted: ${connectionId} +${bytes} (total: ${record.bytesReceived})`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Unit Tests**: Create specific tests to ensure byte counting accuracy:
|
||||||
|
- Test initial chunk handling
|
||||||
|
- Test PROXY protocol overhead exclusion
|
||||||
|
- Test HttpProxy forwarding accuracy
|
||||||
|
|
||||||
|
3. **Protocol Overhead Tracking**: Consider separately tracking:
|
||||||
|
- PROXY protocol headers
|
||||||
|
- TLS handshake bytes
|
||||||
|
- HTTP headers vs body
|
||||||
|
|
||||||
|
4. **NFTables Documentation**: Clearly document that NFTables-forwarded connections are not included in application metrics
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
SmartProxy's byte counting implementation is **robust and accurate**. The design ensures that each byte is counted exactly once, with clear separation between connection tracking and metrics collection. No remediation is required.
|
123
readme.hints.md
123
readme.hints.md
@ -0,0 +1,123 @@
|
|||||||
|
# SmartProxy Development Hints
|
||||||
|
|
||||||
|
## Byte Tracking and Metrics
|
||||||
|
|
||||||
|
### What Gets Counted (Network Interface Throughput)
|
||||||
|
|
||||||
|
The byte tracking is designed to match network interface throughput (what Unifi/network monitoring tools show):
|
||||||
|
|
||||||
|
**Counted bytes include:**
|
||||||
|
- All application data
|
||||||
|
- TLS handshakes and protocol overhead
|
||||||
|
- TLS record headers and encryption padding
|
||||||
|
- HTTP headers and protocol data
|
||||||
|
- WebSocket frames and protocol overhead
|
||||||
|
- TLS alerts sent to clients
|
||||||
|
|
||||||
|
**NOT counted:**
|
||||||
|
- PROXY protocol headers (sent to backend, not client)
|
||||||
|
- TCP/IP headers (handled by OS, not visible at application layer)
|
||||||
|
|
||||||
|
**Byte direction:**
|
||||||
|
- `bytesReceived`: All bytes received FROM the client on the incoming connection
|
||||||
|
- `bytesSent`: All bytes sent TO the client on the incoming connection
|
||||||
|
- Backend connections are separate and not mixed with client metrics
|
||||||
|
|
||||||
|
### Double Counting Issue (Fixed)
|
||||||
|
|
||||||
|
**Problem**: Initial data chunks were being counted twice in the byte tracking:
|
||||||
|
1. Once when stored in `pendingData` in `setupDirectConnection()`
|
||||||
|
2. Again when the data flowed through bidirectional forwarding
|
||||||
|
|
||||||
|
**Solution**: Removed the byte counting when storing initial chunks. Bytes are now only counted when they actually flow through the `setupBidirectionalForwarding()` callbacks.
|
||||||
|
|
||||||
|
### HttpProxy Metrics (Fixed)
|
||||||
|
|
||||||
|
**Problem**: HttpProxy forwarding was updating connection record byte counts but not calling `metricsCollector.recordBytes()`, resulting in missing throughput data.
|
||||||
|
|
||||||
|
**Solution**: Added `metricsCollector.recordBytes()` calls to the HttpProxy bidirectional forwarding callbacks.
|
||||||
|
|
||||||
|
### Metrics Architecture
|
||||||
|
|
||||||
|
The metrics system has three layers:
|
||||||
|
1. **Connection Records** (`record.bytesReceived/bytesSent`): Track total bytes per connection
|
||||||
|
2. **ThroughputTracker**: Accumulates bytes between samples for rate calculations (bytes/second)
|
||||||
|
3. **connectionByteTrackers**: Track bytes per connection with timestamps for per-route/IP metrics
|
||||||
|
|
||||||
|
Total byte counts come from connection records only, preventing double counting.
|
||||||
|
|
||||||
|
### Understanding "High" Byte Counts
|
||||||
|
|
||||||
|
If byte counts seem high compared to actual application data, remember:
|
||||||
|
- TLS handshakes can be 1-5KB depending on cipher suites and certificates
|
||||||
|
- Each TLS record has 5 bytes of header overhead
|
||||||
|
- TLS encryption adds 16-48 bytes of padding/MAC per record
|
||||||
|
- HTTP/2 has additional framing overhead
|
||||||
|
- WebSocket has frame headers (2-14 bytes per message)
|
||||||
|
|
||||||
|
This overhead is real network traffic and should be counted for accurate throughput metrics.
|
||||||
|
|
||||||
|
### Byte Counting Paths
|
||||||
|
|
||||||
|
There are two mutually exclusive paths for connections:
|
||||||
|
|
||||||
|
1. **Direct forwarding** (route-connection-handler.ts):
|
||||||
|
- Used for TCP passthrough, TLS passthrough, and direct connections
|
||||||
|
- Bytes counted in `setupBidirectionalForwarding` callbacks
|
||||||
|
- Initial chunk NOT counted separately (flows through bidirectional forwarding)
|
||||||
|
|
||||||
|
2. **HttpProxy forwarding** (http-proxy-bridge.ts):
|
||||||
|
- Used for TLS termination (terminate, terminate-and-reencrypt)
|
||||||
|
- Initial chunk counted when written to proxy
|
||||||
|
- All subsequent bytes counted in `setupBidirectionalForwarding` callbacks
|
||||||
|
- This is the ONLY counting point for these connections
|
||||||
|
|
||||||
|
### Byte Counting Audit (2025-01-06)
|
||||||
|
|
||||||
|
A comprehensive audit was performed to verify byte counting accuracy:
|
||||||
|
|
||||||
|
**Audit Results:**
|
||||||
|
- ✅ No double counting detected in any connection flow
|
||||||
|
- ✅ Each byte counted exactly once in each direction
|
||||||
|
- ✅ Connection records and metrics updated consistently
|
||||||
|
- ✅ PROXY protocol headers correctly excluded from client metrics
|
||||||
|
- ✅ NFTables forwarded connections correctly not counted (kernel handles)
|
||||||
|
|
||||||
|
**Key Implementation Points:**
|
||||||
|
- All byte counting happens in only 2 files: `route-connection-handler.ts` and `http-proxy-bridge.ts`
|
||||||
|
- Both use the same pattern: increment `record.bytesReceived/Sent` AND call `metricsCollector.recordBytes()`
|
||||||
|
- Initial chunks handled correctly: stored but not counted until forwarded
|
||||||
|
- TLS alerts counted as sent bytes (correct - they are sent to client)
|
||||||
|
|
||||||
|
For full audit details, see `readme.byte-counting-audit.md`
|
||||||
|
|
||||||
|
## Connection Cleanup
|
||||||
|
|
||||||
|
### Zombie Connection Detection
|
||||||
|
|
||||||
|
The connection manager performs comprehensive zombie detection every 10 seconds:
|
||||||
|
- **Full zombies**: Both incoming and outgoing sockets destroyed but connection not cleaned up
|
||||||
|
- **Half zombies**: One socket destroyed, grace period expired (5 minutes for TLS, 30 seconds for non-TLS)
|
||||||
|
- **Stuck connections**: Data received but none sent back after threshold (5 minutes for TLS, 60 seconds for non-TLS)
|
||||||
|
|
||||||
|
### Cleanup Queue
|
||||||
|
|
||||||
|
Connections are cleaned up through a batched queue system:
|
||||||
|
- Batch size: 100 connections
|
||||||
|
- Processing triggered immediately when batch size reached
|
||||||
|
- Otherwise processed after 100ms delay
|
||||||
|
- Prevents overwhelming the system during mass disconnections
|
||||||
|
|
||||||
|
## Keep-Alive Handling
|
||||||
|
|
||||||
|
Keep-alive connections receive special treatment based on `keepAliveTreatment` setting:
|
||||||
|
- **standard**: Normal timeout applies
|
||||||
|
- **extended**: Timeout multiplied by `keepAliveInactivityMultiplier` (default 6x)
|
||||||
|
- **immortal**: No timeout, connections persist indefinitely
|
||||||
|
|
||||||
|
## PROXY Protocol
|
||||||
|
|
||||||
|
The system supports both receiving and sending PROXY protocol:
|
||||||
|
- **Receiving**: Automatically detected from trusted proxy IPs (configured in `proxyIPs`)
|
||||||
|
- **Sending**: Enabled per-route or globally via `sendProxyProtocol` setting
|
||||||
|
- Real client IP is preserved and used for all connection tracking and security checks
|
@ -123,6 +123,11 @@ export class HttpProxyBridge {
|
|||||||
|
|
||||||
// Send initial chunk if present
|
// Send initial chunk if present
|
||||||
if (initialChunk) {
|
if (initialChunk) {
|
||||||
|
// Count the initial chunk bytes
|
||||||
|
record.bytesReceived += initialChunk.length;
|
||||||
|
if (this.smartProxy.metricsCollector) {
|
||||||
|
this.smartProxy.metricsCollector.recordBytes(record.id, initialChunk.length, 0);
|
||||||
|
}
|
||||||
proxySocket.write(initialChunk);
|
proxySocket.write(initialChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,15 +137,21 @@ export class HttpProxyBridge {
|
|||||||
|
|
||||||
setupBidirectionalForwarding(underlyingSocket, proxySocket, {
|
setupBidirectionalForwarding(underlyingSocket, proxySocket, {
|
||||||
onClientData: (chunk) => {
|
onClientData: (chunk) => {
|
||||||
// Update stats if needed
|
// Update stats - this is the ONLY place bytes are counted for HttpProxy connections
|
||||||
if (record) {
|
if (record) {
|
||||||
record.bytesReceived += chunk.length;
|
record.bytesReceived += chunk.length;
|
||||||
|
if (this.smartProxy.metricsCollector) {
|
||||||
|
this.smartProxy.metricsCollector.recordBytes(record.id, chunk.length, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onServerData: (chunk) => {
|
onServerData: (chunk) => {
|
||||||
// Update stats if needed
|
// Update stats - this is the ONLY place bytes are counted for HttpProxy connections
|
||||||
if (record) {
|
if (record) {
|
||||||
record.bytesSent += chunk.length;
|
record.bytesSent += chunk.length;
|
||||||
|
if (this.smartProxy.metricsCollector) {
|
||||||
|
this.smartProxy.metricsCollector.recordBytes(record.id, 0, chunk.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCleanup: (reason) => {
|
onCleanup: (reason) => {
|
||||||
|
@ -347,6 +347,12 @@ export class RouteConnectionHandler {
|
|||||||
}
|
}
|
||||||
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
|
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
|
||||||
try {
|
try {
|
||||||
|
// Count the alert bytes being sent
|
||||||
|
record.bytesSent += alert.length;
|
||||||
|
if (this.smartProxy.metricsCollector) {
|
||||||
|
this.smartProxy.metricsCollector.recordBytes(record.id, 0, alert.length);
|
||||||
|
}
|
||||||
|
|
||||||
socket.cork();
|
socket.cork();
|
||||||
socket.write(alert);
|
socket.write(alert);
|
||||||
socket.uncork();
|
socket.uncork();
|
||||||
@ -1114,14 +1120,9 @@ export class RouteConnectionHandler {
|
|||||||
|
|
||||||
// Store initial data if provided
|
// Store initial data if provided
|
||||||
if (initialChunk) {
|
if (initialChunk) {
|
||||||
record.bytesReceived += initialChunk.length;
|
// Don't count bytes here - they will be counted when actually forwarded through bidirectional forwarding
|
||||||
record.pendingData.push(Buffer.from(initialChunk));
|
record.pendingData.push(Buffer.from(initialChunk));
|
||||||
record.pendingDataSize = initialChunk.length;
|
record.pendingDataSize = initialChunk.length;
|
||||||
|
|
||||||
// Record bytes for metrics
|
|
||||||
if (this.smartProxy.metricsCollector) {
|
|
||||||
this.smartProxy.metricsCollector.recordBytes(record.id, initialChunk.length, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the target socket with immediate error handling
|
// Create the target socket with immediate error handling
|
||||||
@ -1213,6 +1214,9 @@ export class RouteConnectionHandler {
|
|||||||
|
|
||||||
const proxyHeader = ProxyProtocolParser.generate(proxyInfo);
|
const proxyHeader = ProxyProtocolParser.generate(proxyInfo);
|
||||||
|
|
||||||
|
// Note: PROXY protocol headers are sent to the backend, not to the client
|
||||||
|
// They are internal protocol overhead and shouldn't be counted in client-facing metrics
|
||||||
|
|
||||||
// Send PROXY protocol header first
|
// Send PROXY protocol header first
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
targetSocket.write(proxyHeader, (err) => {
|
targetSocket.write(proxyHeader, (err) => {
|
||||||
|
Reference in New Issue
Block a user