feat(classes.portproxy): Add support for port range-based routing with enhanced IP and port validation.
This commit is contained in:
parent
422eb5ec40
commit
f6c3d2d3d0
@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-02-27 - 3.15.0 - feat(classes.portproxy)
|
||||||
|
Add support for port range-based routing with enhanced IP and port validation.
|
||||||
|
|
||||||
|
- Introduced globalPortRanges in IPortProxySettings for routing based on port ranges.
|
||||||
|
- Improved connection handling with port range and domain configuration validations.
|
||||||
|
- Updated connection logging to include the local port information.
|
||||||
|
|
||||||
## 2025-02-26 - 3.14.2 - fix(PortProxy)
|
## 2025-02-26 - 3.14.2 - fix(PortProxy)
|
||||||
Fix cleanup timer reset for PortProxy
|
Fix cleanup timer reset for PortProxy
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.14.2',
|
version: '3.15.0',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
/** Domain configuration with per‐domain allowed port ranges */
|
||||||
export interface IDomainConfig {
|
export interface IDomainConfig {
|
||||||
domain: string; // Glob pattern for domain
|
domain: string; // Glob pattern for domain
|
||||||
allowedIPs: string[]; // Glob patterns for allowed IPs
|
allowedIPs: string[]; // Glob patterns for allowed IPs
|
||||||
targetIP?: string; // Optional target IP for this domain
|
targetIP?: string; // Optional target IP for this domain
|
||||||
|
portRanges: Array<{ from: number; to: number }>; // Domain-specific allowed port ranges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Port proxy settings including global allowed port ranges */
|
||||||
export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
||||||
fromPort: number;
|
fromPort: number;
|
||||||
toPort: number;
|
toPort: number;
|
||||||
@ -14,7 +17,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|||||||
sniEnabled?: boolean;
|
sniEnabled?: boolean;
|
||||||
defaultAllowedIPs?: string[];
|
defaultAllowedIPs?: string[];
|
||||||
preserveSourceIP?: boolean;
|
preserveSourceIP?: boolean;
|
||||||
maxConnectionLifetime?: number; // New option (in milliseconds) to force cleanup of long-lived connections
|
maxConnectionLifetime?: number; // (ms) force cleanup of long-lived connections
|
||||||
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,12 +148,18 @@ export class PortProxy {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find a matching domain config based on the SNI.
|
// 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 aren’t used)
|
||||||
const findMatchingDomain = (serverName: string): IDomainConfig | undefined =>
|
const findMatchingDomain = (serverName: string): IDomainConfig | undefined =>
|
||||||
this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
|
this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
|
||||||
|
|
||||||
this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
|
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 connectionRecord: IConnectionRecord = {
|
const connectionRecord: IConnectionRecord = {
|
||||||
incoming: socket,
|
incoming: socket,
|
||||||
outgoing: null,
|
outgoing: null,
|
||||||
@ -157,7 +167,7 @@ export class PortProxy {
|
|||||||
connectionClosed: false,
|
connectionClosed: false,
|
||||||
};
|
};
|
||||||
this.connectionRecords.add(connectionRecord);
|
this.connectionRecords.add(connectionRecord);
|
||||||
console.log(`New connection from ${remoteIP}. Active connections: ${this.connectionRecords.size}`);
|
console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
|
||||||
|
|
||||||
let initialDataReceived = false;
|
let initialDataReceived = false;
|
||||||
let incomingTerminationReason: string | null = null;
|
let incomingTerminationReason: string | null = null;
|
||||||
@ -225,24 +235,27 @@ export class PortProxy {
|
|||||||
cleanupOnce();
|
cleanupOnce();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupConnection = (serverName: string, initialChunk?: Buffer) => {
|
/**
|
||||||
|
* Sets up the connection to the target host.
|
||||||
|
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
||||||
|
* @param initialChunk - Optional initial data chunk.
|
||||||
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
||||||
|
*/
|
||||||
|
const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig) => {
|
||||||
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
||||||
|
const domainConfig = forcedDomain ? forcedDomain : (serverName ? findMatchingDomain(serverName) : undefined);
|
||||||
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
|
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
|
||||||
|
|
||||||
if (!defaultAllowed && serverName) {
|
if (!defaultAllowed && serverName && !forcedDomain) {
|
||||||
const domainConfig = findMatchingDomain(serverName);
|
|
||||||
if (!domainConfig) {
|
if (!domainConfig) {
|
||||||
return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
|
return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
|
||||||
}
|
}
|
||||||
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
|
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
|
||||||
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
|
||||||
}
|
}
|
||||||
} else if (!defaultAllowed && !serverName) {
|
|
||||||
return rejectIncomingConnection('rejected', `Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
|
|
||||||
} else if (defaultAllowed && !serverName) {
|
} else if (defaultAllowed && !serverName) {
|
||||||
console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
|
console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
|
|
||||||
const targetHost = domainConfig?.targetIP || this.settings.toHost!;
|
const targetHost = domainConfig?.targetIP || this.settings.toHost!;
|
||||||
const connectionOptions: plugins.net.NetConnectOpts = {
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
||||||
host: targetHost,
|
host: targetHost,
|
||||||
@ -258,7 +271,7 @@ export class PortProxy {
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
|
`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
|
||||||
`${serverName ? ` (SNI: ${serverName})` : ''}`
|
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domain})` : ''}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (initialChunk) {
|
if (initialChunk) {
|
||||||
@ -292,7 +305,7 @@ export class PortProxy {
|
|||||||
socket.on('end', handleClose('incoming'));
|
socket.on('end', handleClose('incoming'));
|
||||||
targetSocket.on('end', handleClose('outgoing'));
|
targetSocket.on('end', handleClose('outgoing'));
|
||||||
|
|
||||||
// If maxConnectionLifetime is set, initialize a cleanup timer that will be reset on data flow.
|
// Initialize a cleanup timer for max connection lifetime.
|
||||||
if (this.settings.maxConnectionLifetime) {
|
if (this.settings.maxConnectionLifetime) {
|
||||||
let incomingActive = false;
|
let incomingActive = false;
|
||||||
let outgoingActive = false;
|
let outgoingActive = false;
|
||||||
@ -308,10 +321,8 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start the cleanup timer.
|
|
||||||
resetCleanupTimer();
|
resetCleanupTimer();
|
||||||
|
|
||||||
// Listen for data events on both sides and reset the timer when both are active.
|
|
||||||
socket.on('data', () => {
|
socket.on('data', () => {
|
||||||
incomingActive = true;
|
incomingActive = true;
|
||||||
if (incomingActive && outgoingActive) {
|
if (incomingActive && outgoingActive) {
|
||||||
@ -331,6 +342,37 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- PORT RANGE-BASED HANDLING ---
|
||||||
|
// If global port ranges are defined, enforce port-based routing and ignore SNI.
|
||||||
|
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
|
||||||
|
if (!isPortInRanges(localPort, this.settings.globalPortRanges)) {
|
||||||
|
console.log(`Connection from ${remoteIP} rejected: port ${localPort} is not in global allowed ranges.`);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Find a matching domain config based on the incoming local port.
|
||||||
|
const forcedDomain = this.settings.domains.find(
|
||||||
|
domain => domain.portRanges && domain.portRanges.length > 0 && isPortInRanges(localPort, domain.portRanges)
|
||||||
|
);
|
||||||
|
if (!forcedDomain) {
|
||||||
|
console.log(`Connection from ${remoteIP} rejected: port ${localPort} not configured in any domain's portRanges.`);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check allowed IPs for the forced domain.
|
||||||
|
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
|
||||||
|
if (!defaultAllowed && !isAllowed(remoteIP, forcedDomain.allowedIPs)) {
|
||||||
|
console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domain} on port ${localPort}.`);
|
||||||
|
socket.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domain}.`);
|
||||||
|
// Proceed immediately using the forced domain; ignore SNI.
|
||||||
|
setupConnection('', undefined, forcedDomain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FALLBACK: SNI-BASED HANDLING (if no global port ranges are defined) ---
|
||||||
if (this.settings.sniEnabled) {
|
if (this.settings.sniEnabled) {
|
||||||
socket.setTimeout(5000, () => {
|
socket.setTimeout(5000, () => {
|
||||||
console.log(`Initial data timeout for ${remoteIP}`);
|
console.log(`Initial data timeout for ${remoteIP}`);
|
||||||
@ -363,7 +405,7 @@ export class PortProxy {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Every 10 seconds log active connection count and longest running durations.
|
// Log active connection count and longest running durations every 10 seconds.
|
||||||
this.connectionLogger = setInterval(() => {
|
this.connectionLogger = setInterval(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let maxIncoming = 0;
|
let maxIncoming = 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user