import * as plugins from './smartproxy.plugins.js'; export interface DomainConfig { domain: string; // glob pattern for domain allowedIPs: string[]; // glob patterns for IPs allowed to access this domain } export interface ProxySettings { domains: DomainConfig[]; sniEnabled?: boolean; tlsOptions?: plugins.tls.TlsOptions; defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found } export class PortProxy { netServer: plugins.net.Server | plugins.tls.Server; fromPort: number; toPort: number; settings: ProxySettings; constructor(fromPortArg: number, toPortArg: number, settings: plugins.tls.TlsOptions & ProxySettings) { this.fromPort = fromPortArg; this.toPort = toPortArg; this.settings = settings; } public async start() { const cleanUpSockets = (from: plugins.net.Socket, to: plugins.net.Socket) => { from.end(); to.end(); from.removeAllListeners(); to.removeAllListeners(); from.unpipe(); to.unpipe(); from.destroy(); to.destroy(); }; const isAllowed = (value: string, patterns: string[]): boolean => { return patterns.some(pattern => plugins.minimatch(value, pattern)); }; const findMatchingDomain = (serverName: string): DomainConfig | undefined => { return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain)); }; const server = this.settings.sniEnabled ? plugins.tls.createServer(this.settings.tlsOptions || {}) : plugins.net.createServer(); this.netServer = server.on('connection', (from: plugins.net.Socket) => { const remoteIP = from.remoteAddress || ''; if (this.settings.sniEnabled && from instanceof plugins.tls.TLSSocket) { const serverName = (from as any).servername || ''; const domainConfig = findMatchingDomain(serverName); if (!domainConfig) { // If no matching domain config found, check default IPs if available if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`); from.end(); return; } } else { // Check if IP is allowed for this domain if (!isAllowed(remoteIP, domainConfig.allowedIPs)) { console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`); from.end(); return; } } } else if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`); from.end(); return; } const to = plugins.net.createConnection({ host: 'localhost', port: this.toPort, }); from.setTimeout(120000); from.pipe(to); to.pipe(from); from.on('error', () => { cleanUpSockets(from, to); }); to.on('error', () => { cleanUpSockets(from, to); }); from.on('close', () => { cleanUpSockets(from, to); }); to.on('close', () => { cleanUpSockets(from, to); }); from.on('timeout', () => { cleanUpSockets(from, to); }); to.on('timeout', () => { cleanUpSockets(from, to); }); from.on('end', () => { cleanUpSockets(from, to); }); to.on('end', () => { cleanUpSockets(from, to); }); }) .listen(this.fromPort); console.log(`PortProxy -> OK: Now listening on port ${this.fromPort}`); } public async stop() { const done = plugins.smartpromise.defer(); this.netServer.close(() => { done.resolve(); }); await done.promise; } }