|
|
|
@ -4,6 +4,7 @@ import * as plugins from './plugins.js';
|
|
|
|
|
export interface IDomainConfig {
|
|
|
|
|
domains: string[]; // Glob patterns for domain(s)
|
|
|
|
|
allowedIPs: string[]; // Glob patterns for allowed IPs
|
|
|
|
|
blockedIPs?: string[]; // Glob patterns for blocked IPs
|
|
|
|
|
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
|
|
|
|
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
|
|
|
}
|
|
|
|
@ -16,6 +17,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
|
|
domainConfigs: IDomainConfig[];
|
|
|
|
|
sniEnabled?: boolean;
|
|
|
|
|
defaultAllowedIPs?: string[];
|
|
|
|
|
defaultBlockedIPs?: string[];
|
|
|
|
|
preserveSourceIP?: boolean;
|
|
|
|
|
maxConnectionLifetime?: number; // (ms) force cleanup of long-lived connections
|
|
|
|
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
|
|
@ -95,6 +97,36 @@ interface IConnectionRecord {
|
|
|
|
|
cleanupTimer?: NodeJS.Timeout; // Timer to force cleanup after max lifetime/inactivity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper: Check if an IP is allowed considering allowed and blocked glob patterns.
|
|
|
|
|
const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
|
|
|
|
|
if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
|
|
|
|
|
return isAllowed(ip, allowed);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class PortProxy {
|
|
|
|
|
private netServers: plugins.net.Server[] = [];
|
|
|
|
|
settings: IPortProxySettings;
|
|
|
|
@ -231,17 +263,25 @@ export class PortProxy {
|
|
|
|
|
config.domains.some(d => plugins.minimatch(serverName, d))
|
|
|
|
|
) : undefined);
|
|
|
|
|
|
|
|
|
|
// If a matching domain config exists, check its allowedIPs.
|
|
|
|
|
// Effective IP check: merge allowed IPs with default allowed, and remove blocked IPs.
|
|
|
|
|
if (domainConfig) {
|
|
|
|
|
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
|
|
|
|
|
const effectiveAllowedIPs: string[] = [
|
|
|
|
|
...domainConfig.allowedIPs,
|
|
|
|
|
...(this.settings.defaultAllowedIPs || [])
|
|
|
|
|
];
|
|
|
|
|
const effectiveBlockedIPs: string[] = [
|
|
|
|
|
...(domainConfig.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || [])
|
|
|
|
|
];
|
|
|
|
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
|
|
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
|
|
|
|
|
}
|
|
|
|
|
} else if (this.settings.defaultAllowedIPs) {
|
|
|
|
|
// Only check default allowed IPs if no domain config matched.
|
|
|
|
|
if (!isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
|
|
|
if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
|
|
|
|
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
|
|
|
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
|
|
|
host: targetHost,
|
|
|
|
@ -341,6 +381,7 @@ export class PortProxy {
|
|
|
|
|
setupConnection('', undefined, {
|
|
|
|
|
domains: ['global'],
|
|
|
|
|
allowedIPs: this.settings.defaultAllowedIPs || [],
|
|
|
|
|
blockedIPs: this.settings.defaultBlockedIPs || [],
|
|
|
|
|
targetIPs: [this.settings.targetIP!],
|
|
|
|
|
portRanges: []
|
|
|
|
|
}, localPort);
|
|
|
|
@ -466,28 +507,4 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
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))
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
}
|