2022-07-29 00:49:46 +02:00
|
|
|
import * as plugins from './smartproxy.plugins.js';
|
2025-02-21 15:14:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
export interface DomainConfig {
|
|
|
|
domain: string; // glob pattern for domain
|
|
|
|
allowedIPs: string[]; // glob patterns for IPs allowed to access this domain
|
|
|
|
}
|
|
|
|
|
2025-02-21 17:01:02 +00:00
|
|
|
export interface ProxySettings extends plugins.tls.TlsOptions {
|
|
|
|
// Port configuration
|
|
|
|
fromPort: number;
|
|
|
|
toPort: number;
|
|
|
|
toHost?: string; // Target host to proxy to, defaults to 'localhost'
|
|
|
|
|
|
|
|
// Domain and security settings
|
2025-02-21 15:14:02 +00:00
|
|
|
domains: DomainConfig[];
|
|
|
|
sniEnabled?: boolean;
|
|
|
|
defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found
|
|
|
|
}
|
2019-08-22 15:09:48 +02:00
|
|
|
|
2022-07-29 00:49:46 +02:00
|
|
|
export class PortProxy {
|
2025-02-21 15:17:19 +00:00
|
|
|
netServer: plugins.net.Server | plugins.tls.Server;
|
2025-02-21 15:14:02 +00:00
|
|
|
settings: ProxySettings;
|
2022-07-29 00:49:46 +02:00
|
|
|
|
2025-02-21 17:01:02 +00:00
|
|
|
constructor(settings: ProxySettings) {
|
|
|
|
this.settings = {
|
|
|
|
...settings,
|
|
|
|
toHost: settings.toHost || 'localhost'
|
|
|
|
};
|
2022-07-29 00:49:46 +02:00
|
|
|
}
|
|
|
|
|
2022-07-29 01:52:34 +02:00
|
|
|
public async start() {
|
2021-02-02 21:59:24 +00:00
|
|
|
const cleanUpSockets = (from: plugins.net.Socket, to: plugins.net.Socket) => {
|
|
|
|
from.end();
|
|
|
|
to.end();
|
|
|
|
from.removeAllListeners();
|
2021-02-02 21:59:54 +00:00
|
|
|
to.removeAllListeners();
|
2021-02-02 21:59:24 +00:00
|
|
|
from.unpipe();
|
|
|
|
to.unpipe();
|
2021-02-03 00:13:29 +00:00
|
|
|
from.destroy();
|
|
|
|
to.destroy();
|
2022-07-29 01:52:34 +02:00
|
|
|
};
|
2025-02-21 18:43:08 +00:00
|
|
|
const normalizeIP = (ip: string): string[] => {
|
|
|
|
// Handle IPv4-mapped IPv6 addresses
|
|
|
|
if (ip.startsWith('::ffff:')) {
|
|
|
|
const ipv4 = ip.slice(7); // Remove '::ffff:' prefix
|
|
|
|
return [ip, ipv4];
|
|
|
|
}
|
|
|
|
// Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
|
|
|
|
if (ip.match(/^\d{1,3}(\.\d{1,3}){3}$/)) {
|
|
|
|
return [ip, `::ffff:${ip}`];
|
|
|
|
}
|
|
|
|
return [ip];
|
|
|
|
};
|
|
|
|
|
2025-02-21 15:14:02 +00:00
|
|
|
const isAllowed = (value: string, patterns: string[]): boolean => {
|
2025-02-21 18:43:08 +00:00
|
|
|
// Expand patterns to include both IPv4 and IPv6 variants
|
|
|
|
const expandedPatterns = patterns.flatMap(normalizeIP);
|
|
|
|
// Check if any variant of the IP matches any expanded pattern
|
|
|
|
return normalizeIP(value).some(ip =>
|
|
|
|
expandedPatterns.some(pattern => plugins.minimatch(ip, pattern))
|
|
|
|
);
|
2025-02-21 15:14:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const findMatchingDomain = (serverName: string): DomainConfig | undefined => {
|
|
|
|
return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
|
|
|
|
};
|
|
|
|
|
2025-02-21 17:01:02 +00:00
|
|
|
const server = this.settings.sniEnabled
|
2025-02-21 18:47:18 +00:00
|
|
|
? plugins.tls.createServer({
|
|
|
|
...this.settings,
|
|
|
|
SNICallback: (serverName: string, cb: (err: Error | null, ctx?: plugins.tls.SecureContext) => void) => {
|
|
|
|
console.log(`SNI request for domain: ${serverName}`);
|
|
|
|
const domainConfig = findMatchingDomain(serverName);
|
|
|
|
if (!domainConfig) {
|
|
|
|
console.log(`SNI rejected: No matching domain config for ${serverName}`);
|
|
|
|
cb(new Error(`No configuration for domain: ${serverName}`));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Create context with the provided TLS settings
|
|
|
|
const ctx = plugins.tls.createSecureContext(this.settings);
|
|
|
|
cb(null, ctx);
|
|
|
|
}
|
|
|
|
})
|
2025-02-21 17:01:02 +00:00
|
|
|
: plugins.net.createServer();
|
2025-02-21 15:14:02 +00:00
|
|
|
|
2025-02-21 18:47:18 +00:00
|
|
|
const handleConnection = (from: plugins.net.Socket | plugins.tls.TLSSocket) => {
|
2025-02-21 15:14:02 +00:00
|
|
|
const remoteIP = from.remoteAddress || '';
|
2025-02-21 18:47:18 +00:00
|
|
|
let serverName = '';
|
2025-02-21 15:14:02 +00:00
|
|
|
|
2025-02-21 18:47:18 +00:00
|
|
|
if (this.settings.sniEnabled && from instanceof plugins.tls.TLSSocket) {
|
|
|
|
serverName = (from as any).servername || '';
|
|
|
|
console.log(`TLS Connection from ${remoteIP} for domain: ${serverName}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// For TLS connections, we've already validated the domain in SNICallback
|
|
|
|
if (!this.settings.sniEnabled || from instanceof plugins.tls.TLSSocket) {
|
|
|
|
const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
|
|
|
|
|
|
|
|
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 || 'non-SNI'} from IP ${remoteIP}`);
|
|
|
|
from.end();
|
|
|
|
return;
|
2025-02-21 15:14:02 +00:00
|
|
|
}
|
2025-02-21 18:47:18 +00:00
|
|
|
} 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)) {
|
2025-02-21 18:48:39 +00:00
|
|
|
console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
|
|
|
from.end();
|
|
|
|
return;
|
|
|
|
}
|
2025-02-21 15:14:02 +00:00
|
|
|
|
2025-02-21 18:47:18 +00:00
|
|
|
const to = plugins.net.createConnection({
|
|
|
|
host: this.settings.toHost!,
|
|
|
|
port: this.settings.toPort,
|
|
|
|
});
|
|
|
|
console.log(`Connection established: ${remoteIP} -> ${this.settings.toHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
|
|
|
|
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);
|
|
|
|
});
|
2025-02-21 18:48:39 +00:00
|
|
|
};
|
|
|
|
|
2025-02-21 18:47:18 +00:00
|
|
|
this.netServer = server
|
|
|
|
.on('connection', handleConnection)
|
|
|
|
.on('secureConnection', handleConnection)
|
|
|
|
.on('tlsClientError', (err, tlsSocket) => {
|
|
|
|
console.log(`TLS Client Error: ${err.message}`);
|
|
|
|
})
|
|
|
|
.on('error', (err) => {
|
|
|
|
console.log(`Server Error: ${err.message}`);
|
2020-02-07 13:04:11 +00:00
|
|
|
})
|
2025-02-21 17:01:02 +00:00
|
|
|
.listen(this.settings.fromPort);
|
2025-02-21 18:47:18 +00:00
|
|
|
console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI enabled)' : ''}`);
|
2022-07-29 00:49:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async stop() {
|
2019-08-22 15:09:48 +02:00
|
|
|
const done = plugins.smartpromise.defer();
|
2022-07-29 03:39:05 +02:00
|
|
|
this.netServer.close(() => {
|
|
|
|
done.resolve();
|
2019-08-22 15:09:48 +02:00
|
|
|
});
|
|
|
|
await done.promise;
|
2022-07-29 00:49:46 +02:00
|
|
|
}
|
2022-07-29 01:52:34 +02:00
|
|
|
}
|