fix(PortProxy): Refactored PortProxy to support multiple listening ports and improved modularity.

This commit is contained in:
Philipp Kunz 2025-02-27 13:04:01 +00:00
parent fcd0f61b5c
commit 659aae297b
3 changed files with 79 additions and 63 deletions

View File

@ -1,5 +1,11 @@
# Changelog # Changelog
## 2025-02-27 - 3.16.3 - fix(PortProxy)
Refactored PortProxy to support multiple listening ports and improved modularity.
- Updated PortProxy to allow multiple listening ports with flexible configuration.
- Moved helper functions for IP and port range checks outside the class for cleaner code structure.
## 2025-02-27 - 3.16.2 - fix(PortProxy) ## 2025-02-27 - 3.16.2 - fix(PortProxy)
Fix port-based routing logic in PortProxy Fix port-based routing logic in PortProxy

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '3.16.2', version: '3.16.3',
description: 'A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.' description: 'A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.'
} }

View File

@ -95,7 +95,7 @@ interface IConnectionRecord {
} }
export class PortProxy { export class PortProxy {
netServer: plugins.net.Server; private netServers: plugins.net.Server[] = [];
settings: IPortProxySettings; settings: IPortProxySettings;
// Unified record tracking each connection pair. // Unified record tracking each connection pair.
private connectionRecords: Set<IConnectionRecord> = new Set(); private connectionRecords: Set<IConnectionRecord> = new Set();
@ -122,43 +122,8 @@ export class PortProxy {
} }
public async start() { public async start() {
// Helper to forcefully destroy sockets. // Define a unified connection handler for all listening ports.
const cleanUpSockets = (socketA: plugins.net.Socket, socketB?: plugins.net.Socket) => { const connectionHandler = (socket: plugins.net.Socket) => {
if (!socketA.destroyed) socketA.destroy();
if (socketB && !socketB.destroyed) socketB.destroy();
};
// Normalize an IP to include both IPv4 and IPv6 representations.
const normalizeIP = (ip: string): string[] => {
if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7);
return [ip, ipv4];
}
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
return [ip, `::ffff:${ip}`];
}
return [ip];
};
// Check if a given IP matches any of the glob patterns.
const isAllowed = (ip: string, patterns: string[]): boolean => {
const normalizedIPVariants = normalizeIP(ip);
const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some(ipVariant =>
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
);
};
// Check if a port falls within any of the given port ranges.
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
return ranges.some(range => port >= range.from && port <= range.to);
};
// Find a matching domain config based on SNI (fallback when port ranges arent used)
const findMatchingDomain = (serverName: string): IDomainConfig | undefined =>
this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
const remoteIP = socket.remoteAddress || ''; const remoteIP = socket.remoteAddress || '';
const localPort = socket.localPort; // The port on which this connection was accepted. const localPort = socket.localPort; // The port on which this connection was accepted.
const connectionRecord: IConnectionRecord = { const connectionRecord: IConnectionRecord = {
@ -181,7 +146,8 @@ export class PortProxy {
if (connectionRecord.cleanupTimer) { if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer); clearTimeout(connectionRecord.cleanupTimer);
} }
cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined); if (!socket.destroyed) socket.destroy();
if (connectionRecord.outgoing && !connectionRecord.outgoing.destroyed) connectionRecord.outgoing.destroy();
this.connectionRecords.delete(connectionRecord); this.connectionRecords.delete(connectionRecord);
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`); console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
} }
@ -244,7 +210,7 @@ export class PortProxy {
*/ */
const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig) => { const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig) => {
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup. // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
const domainConfig = forcedDomain ? forcedDomain : (serverName ? findMatchingDomain(serverName) : undefined); const domainConfig = forcedDomain ? forcedDomain : (serverName ? this.settings.domains.find(config => plugins.minimatch(serverName, config.domain)) : undefined);
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs); const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
if (!defaultAllowed && serverName && !forcedDomain) { if (!defaultAllowed && serverName && !forcedDomain) {
@ -344,13 +310,10 @@ export class PortProxy {
}; };
// --- PORT RANGE-BASED HANDLING --- // --- PORT RANGE-BASED HANDLING ---
// Check if the local port falls within any of the global port ranges. // If the local port is one of the globally listened ports, we may have port-based rules.
const isLocalPortInGlobalRange = if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges); // If forwardAllGlobalRanges is enabled, always forward using the global targetIP.
if (isLocalPortInGlobalRange) {
if (this.settings.forwardAllGlobalRanges) { if (this.settings.forwardAllGlobalRanges) {
// Forward connection to the global targetIP regardless of domain config.
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`); console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
socket.end(); socket.end();
@ -380,7 +343,7 @@ export class PortProxy {
setupConnection('', undefined, forcedDomain); setupConnection('', undefined, forcedDomain);
return; return;
} }
// If no forced domain config is found for this port, fall through to SNI/default handling. // Fall through to SNI/default handling if no forced domain config is found.
} }
} }
@ -406,16 +369,36 @@ export class PortProxy {
} }
setupConnection(''); setupConnection('');
} }
}) };
// --- SETUP LISTENERS ---
// Determine which ports to listen on.
const listeningPorts = new Set<number>();
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
// Listen on every port defined by the global ranges.
for (const range of this.settings.globalPortRanges) {
for (let port = range.from; port <= range.to; port++) {
listeningPorts.add(port);
}
}
// Also ensure the default fromPort is listened to if it isnt already in the ranges.
listeningPorts.add(this.settings.fromPort);
} else {
listeningPorts.add(this.settings.fromPort);
}
// Create a server for each port.
for (const port of listeningPorts) {
const server = plugins.net
.createServer(connectionHandler)
.on('error', (err: Error) => { .on('error', (err: Error) => {
console.log(`Server Error: ${err.message}`); console.log(`Server Error on port ${port}: ${err.message}`);
})
.listen(this.settings.fromPort, () => {
console.log(
`PortProxy -> OK: Now listening on port ${this.settings.fromPort}` +
`${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`
);
}); });
server.listen(port, () => {
console.log(`PortProxy -> OK: Now listening on port ${port}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
});
this.netServers.push(server);
}
// Log active connection count and longest running durations every 10 seconds. // Log active connection count and longest running durations every 10 seconds.
this.connectionLogger = setInterval(() => { this.connectionLogger = setInterval(() => {
@ -438,14 +421,41 @@ export class PortProxy {
} }
public async stop() { public async stop() {
const done = plugins.smartpromise.defer(); // Close all servers.
this.netServer.close(() => { const closePromises: Promise<void>[] = this.netServers.map(
done.resolve(); server =>
}); new Promise<void>((resolve) => {
server.close(() => resolve());
})
);
if (this.connectionLogger) { if (this.connectionLogger) {
clearInterval(this.connectionLogger); clearInterval(this.connectionLogger);
this.connectionLogger = null; this.connectionLogger = null;
} }
await done.promise; await Promise.all(closePromises);
} }
} }
// Helper: Check if a port falls within any of the given port ranges.
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
return ranges.some(range => port >= range.from && port <= range.to);
};
// Helper: Check if a given IP matches any of the glob patterns.
const isAllowed = (ip: string, patterns: string[]): boolean => {
const normalizeIP = (ip: string): string[] => {
if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7);
return [ip, ipv4];
}
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
return [ip, `::ffff:${ip}`];
}
return [ip];
};
const normalizedIPVariants = normalizeIP(ip);
const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some(ipVariant =>
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
);
};