feat(smartproxy.portproxy): Enhance PortProxy with detailed connection statistics and termination tracking
This commit is contained in:
parent
2df2f0ceaf
commit
71b5237cd4
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-02-22 - 3.10.0 - feat(smartproxy.portproxy)
|
||||||
|
Enhance PortProxy with detailed connection statistics and termination tracking
|
||||||
|
|
||||||
|
- Added tracking of termination statistics for incoming and outgoing connections
|
||||||
|
- Enhanced logging to include detailed termination statistics
|
||||||
|
- Introduced helpers to update and log termination stats
|
||||||
|
- Retained detailed connection duration and active connection logging
|
||||||
|
|
||||||
## 2025-02-22 - 3.9.4 - fix(PortProxy)
|
## 2025-02-22 - 3.9.4 - fix(PortProxy)
|
||||||
Ensure proper cleanup on connection rejection in PortProxy
|
Ensure proper cleanup on connection rejection in PortProxy
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.9.4',
|
version: '3.10.0',
|
||||||
description: 'a proxy for handling high workloads of proxying'
|
description: 'a proxy for handling high workloads of proxying'
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,15 @@ export class PortProxy {
|
|||||||
private outgoingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
|
private outgoingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
|
||||||
private connectionLogger: NodeJS.Timeout | null = null;
|
private connectionLogger: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
// Overall termination statistics
|
||||||
|
private terminationStats: {
|
||||||
|
incoming: Record<string, number>;
|
||||||
|
outgoing: Record<string, number>;
|
||||||
|
} = {
|
||||||
|
incoming: {},
|
||||||
|
outgoing: {},
|
||||||
|
};
|
||||||
|
|
||||||
constructor(settings: IProxySettings) {
|
constructor(settings: IProxySettings) {
|
||||||
this.settings = {
|
this.settings = {
|
||||||
...settings,
|
...settings,
|
||||||
@ -130,6 +139,15 @@ export class PortProxy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to update termination stats.
|
||||||
|
private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
|
||||||
|
if (!this.terminationStats[side][reason]) {
|
||||||
|
this.terminationStats[side][reason] = 1;
|
||||||
|
} else {
|
||||||
|
this.terminationStats[side][reason]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
// Adjusted cleanUpSockets to allow an optional outgoing socket.
|
// Adjusted cleanUpSockets to allow an optional outgoing socket.
|
||||||
const cleanUpSockets = (from: plugins.net.Socket, to?: plugins.net.Socket) => {
|
const cleanUpSockets = (from: plugins.net.Socket, to?: plugins.net.Socket) => {
|
||||||
@ -183,6 +201,10 @@ export class PortProxy {
|
|||||||
// Flag to detect if we've received the first data chunk.
|
// Flag to detect if we've received the first data chunk.
|
||||||
let initialDataReceived = false;
|
let initialDataReceived = false;
|
||||||
|
|
||||||
|
// Local termination reason trackers for each side.
|
||||||
|
let incomingTermReason: string | null = null;
|
||||||
|
let outgoingTermReason: string | null = null;
|
||||||
|
|
||||||
// Immediately attach an error handler to catch early errors.
|
// Immediately attach an error handler to catch early errors.
|
||||||
socket.on('error', (err: Error) => {
|
socket.on('error', (err: Error) => {
|
||||||
if (!initialDataReceived) {
|
if (!initialDataReceived) {
|
||||||
@ -192,7 +214,7 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Flag to ensure cleanup happens only once.
|
// Ensure cleanup happens only once.
|
||||||
let connectionClosed = false;
|
let connectionClosed = false;
|
||||||
const cleanupOnce = () => {
|
const cleanupOnce = () => {
|
||||||
if (!connectionClosed) {
|
if (!connectionClosed) {
|
||||||
@ -209,21 +231,39 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Declare the outgoing connection as possibly null.
|
// Outgoing connection placeholder.
|
||||||
let to: plugins.net.Socket | null = null;
|
let to: plugins.net.Socket | null = null;
|
||||||
|
|
||||||
|
// Handle errors by recording termination reason and cleaning up.
|
||||||
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
||||||
const code = (err as any).code;
|
const code = (err as any).code;
|
||||||
|
let reason = 'error';
|
||||||
if (code === 'ECONNRESET') {
|
if (code === 'ECONNRESET') {
|
||||||
|
reason = 'econnreset';
|
||||||
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
|
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
|
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
if (side === 'incoming' && incomingTermReason === null) {
|
||||||
|
incomingTermReason = reason;
|
||||||
|
this.incrementTerminationStat('incoming', reason);
|
||||||
|
} else if (side === 'outgoing' && outgoingTermReason === null) {
|
||||||
|
outgoingTermReason = reason;
|
||||||
|
this.incrementTerminationStat('outgoing', reason);
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle close events. If no termination reason was recorded, mark as "normal".
|
||||||
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
||||||
console.log(`Connection closed on ${side} side from ${remoteIP}`);
|
console.log(`Connection closed on ${side} side from ${remoteIP}`);
|
||||||
|
if (side === 'incoming' && incomingTermReason === null) {
|
||||||
|
incomingTermReason = 'normal';
|
||||||
|
this.incrementTerminationStat('incoming', 'normal');
|
||||||
|
} else if (side === 'outgoing' && outgoingTermReason === null) {
|
||||||
|
outgoingTermReason = 'normal';
|
||||||
|
this.incrementTerminationStat('outgoing', 'normal');
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -236,18 +276,30 @@ export class PortProxy {
|
|||||||
if (!domainConfig) {
|
if (!domainConfig) {
|
||||||
console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
|
console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
|
||||||
socket.end();
|
socket.end();
|
||||||
|
if (incomingTermReason === null) {
|
||||||
|
incomingTermReason = 'rejected';
|
||||||
|
this.incrementTerminationStat('incoming', 'rejected');
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
|
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
|
||||||
console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
|
console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
|
||||||
socket.end();
|
socket.end();
|
||||||
|
if (incomingTermReason === null) {
|
||||||
|
incomingTermReason = 'rejected';
|
||||||
|
this.incrementTerminationStat('incoming', 'rejected');
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (!isDefaultAllowed && !serverName) {
|
} else if (!isDefaultAllowed && !serverName) {
|
||||||
console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
|
console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
|
||||||
socket.end();
|
socket.end();
|
||||||
|
if (incomingTermReason === null) {
|
||||||
|
incomingTermReason = 'rejected';
|
||||||
|
this.incrementTerminationStat('incoming', 'rejected');
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@ -269,7 +321,6 @@ export class PortProxy {
|
|||||||
|
|
||||||
// Establish outgoing connection.
|
// Establish outgoing connection.
|
||||||
to = plugins.net.connect(connectionOptions);
|
to = plugins.net.connect(connectionOptions);
|
||||||
// Record start time for the outgoing connection.
|
|
||||||
if (to) {
|
if (to) {
|
||||||
this.outgoingConnectionTimes.set(to, Date.now());
|
this.outgoingConnectionTimes.set(to, Date.now());
|
||||||
}
|
}
|
||||||
@ -280,21 +331,28 @@ export class PortProxy {
|
|||||||
socket.unshift(initialChunk);
|
socket.unshift(initialChunk);
|
||||||
}
|
}
|
||||||
socket.setTimeout(120000);
|
socket.setTimeout(120000);
|
||||||
// Since 'to' is not null here, we can use the non-null assertion.
|
|
||||||
socket.pipe(to!);
|
socket.pipe(to!);
|
||||||
to!.pipe(socket);
|
to!.pipe(socket);
|
||||||
|
|
||||||
// Attach error and close handlers for both sockets.
|
// Attach event handlers for both sockets.
|
||||||
socket.on('error', handleError('incoming'));
|
socket.on('error', handleError('incoming'));
|
||||||
to!.on('error', handleError('outgoing'));
|
to!.on('error', handleError('outgoing'));
|
||||||
socket.on('close', handleClose('incoming'));
|
socket.on('close', handleClose('incoming'));
|
||||||
to!.on('close', handleClose('outgoing'));
|
to!.on('close', handleClose('outgoing'));
|
||||||
socket.on('timeout', () => {
|
socket.on('timeout', () => {
|
||||||
console.log(`Timeout on incoming side from ${remoteIP}`);
|
console.log(`Timeout on incoming side from ${remoteIP}`);
|
||||||
|
if (incomingTermReason === null) {
|
||||||
|
incomingTermReason = 'timeout';
|
||||||
|
this.incrementTerminationStat('incoming', 'timeout');
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
});
|
});
|
||||||
to!.on('timeout', () => {
|
to!.on('timeout', () => {
|
||||||
console.log(`Timeout on outgoing side from ${remoteIP}`);
|
console.log(`Timeout on outgoing side from ${remoteIP}`);
|
||||||
|
if (outgoingTermReason === null) {
|
||||||
|
outgoingTermReason = 'timeout';
|
||||||
|
this.incrementTerminationStat('outgoing', 'timeout');
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
});
|
});
|
||||||
socket.on('end', handleClose('incoming'));
|
socket.on('end', handleClose('incoming'));
|
||||||
@ -305,7 +363,6 @@ export class PortProxy {
|
|||||||
if (this.settings.sniEnabled) {
|
if (this.settings.sniEnabled) {
|
||||||
socket.once('data', (chunk: Buffer) => {
|
socket.once('data', (chunk: Buffer) => {
|
||||||
initialDataReceived = true;
|
initialDataReceived = true;
|
||||||
// Try to extract the server name from the ClientHello.
|
|
||||||
const serverName = extractSNI(chunk) || '';
|
const serverName = extractSNI(chunk) || '';
|
||||||
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
||||||
setupConnection(serverName, chunk);
|
setupConnection(serverName, chunk);
|
||||||
@ -316,6 +373,10 @@ export class PortProxy {
|
|||||||
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
||||||
console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
||||||
socket.end();
|
socket.end();
|
||||||
|
if (incomingTermReason === null) {
|
||||||
|
incomingTermReason = 'rejected';
|
||||||
|
this.incrementTerminationStat('incoming', 'rejected');
|
||||||
|
}
|
||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -329,7 +390,8 @@ export class PortProxy {
|
|||||||
console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
|
console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log active connection count and longest running connections every 10 seconds.
|
// Log active connection count, longest running connection durations,
|
||||||
|
// and termination statistics every 10 seconds.
|
||||||
this.connectionLogger = setInterval(() => {
|
this.connectionLogger = setInterval(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let maxIncoming = 0;
|
let maxIncoming = 0;
|
||||||
@ -346,7 +408,7 @@ export class PortProxy {
|
|||||||
maxOutgoing = duration;
|
maxOutgoing = duration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`(Interval Log) Active connections: ${this.activeConnections.size}. Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}`);
|
console.log(`(Interval Log) Active connections: ${this.activeConnections.size}. Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, (outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user