|
|
|
@ -5,20 +5,21 @@ export interface IDomainConfig {
|
|
|
|
|
domain: string; // Glob pattern for domain
|
|
|
|
|
allowedIPs: string[]; // Glob patterns for allowed IPs
|
|
|
|
|
targetIP?: string; // Optional target IP for this domain
|
|
|
|
|
portRanges: Array<{ from: number; to: number }>; // Domain-specific allowed port ranges
|
|
|
|
|
portRanges?: Array<{ from: number; to: number }>; // Optional domain-specific allowed port ranges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Port proxy settings including global allowed port ranges */
|
|
|
|
|
export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
|
|
fromPort: number;
|
|
|
|
|
toPort: number;
|
|
|
|
|
toHost?: string; // Target host to proxy to, defaults to 'localhost'
|
|
|
|
|
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
|
|
|
|
|
domains: IDomainConfig[];
|
|
|
|
|
sniEnabled?: boolean;
|
|
|
|
|
defaultAllowedIPs?: string[];
|
|
|
|
|
preserveSourceIP?: boolean;
|
|
|
|
|
maxConnectionLifetime?: number; // (ms) force cleanup of long-lived connections
|
|
|
|
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
|
|
|
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -111,7 +112,7 @@ export class PortProxy {
|
|
|
|
|
constructor(settingsArg: IPortProxySettings) {
|
|
|
|
|
this.settings = {
|
|
|
|
|
...settingsArg,
|
|
|
|
|
toHost: settingsArg.toHost || 'localhost',
|
|
|
|
|
targetIP: settingsArg.targetIP || 'localhost',
|
|
|
|
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
@ -256,7 +257,7 @@ export class PortProxy {
|
|
|
|
|
} else if (defaultAllowed && !serverName) {
|
|
|
|
|
console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
|
|
|
|
|
}
|
|
|
|
|
const targetHost = domainConfig?.targetIP || this.settings.toHost!;
|
|
|
|
|
const targetHost = domainConfig?.targetIP || this.settings.targetIP!;
|
|
|
|
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
|
|
|
host: targetHost,
|
|
|
|
|
port: this.settings.toPort,
|
|
|
|
@ -350,26 +351,43 @@ export class PortProxy {
|
|
|
|
|
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();
|
|
|
|
|
if (this.settings.forwardAllGlobalRanges) {
|
|
|
|
|
// Forward connection to the global targetIP regardless of domain config.
|
|
|
|
|
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
|
|
|
console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
|
|
|
|
|
socket.end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
|
|
|
|
|
setupConnection('', undefined, {
|
|
|
|
|
domain: 'global',
|
|
|
|
|
allowedIPs: this.settings.defaultAllowedIPs || [],
|
|
|
|
|
targetIP: this.settings.targetIP,
|
|
|
|
|
portRanges: []
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
// 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) ---
|
|
|
|
|