fix(network-proxy, route-utils, route-manager): Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports.
This commit is contained in:
@ -291,12 +291,15 @@ export class RouteManager {
|
||||
|
||||
/**
|
||||
* Match an IP pattern against an IP
|
||||
* Supports exact matches, wildcard patterns, and CIDR notation
|
||||
*/
|
||||
private matchIp(pattern: string, ip: string): boolean {
|
||||
// Exact match
|
||||
if (pattern === ip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard matching (e.g., 192.168.0.*)
|
||||
if (pattern.includes('*')) {
|
||||
const regexPattern = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
@ -306,10 +309,65 @@ export class RouteManager {
|
||||
return regex.test(ip);
|
||||
}
|
||||
|
||||
// TODO: Implement CIDR matching
|
||||
// CIDR matching (e.g., 192.168.0.0/24)
|
||||
if (pattern.includes('/')) {
|
||||
try {
|
||||
const [subnet, bits] = pattern.split('/');
|
||||
|
||||
// Convert IP addresses to numeric format for comparison
|
||||
const ipBinary = this.ipToBinary(ip);
|
||||
const subnetBinary = this.ipToBinary(subnet);
|
||||
|
||||
if (!ipBinary || !subnetBinary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the subnet mask from CIDR notation
|
||||
const mask = parseInt(bits, 10);
|
||||
if (isNaN(mask) || mask < 0 || mask > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the first 'mask' bits match between IP and subnet
|
||||
return ipBinary.slice(0, mask) === subnetBinary.slice(0, mask);
|
||||
} catch (error) {
|
||||
// If we encounter any error during CIDR matching, return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IP address to its binary representation
|
||||
* @param ip The IP address to convert
|
||||
* @returns Binary string representation or null if invalid
|
||||
*/
|
||||
private ipToBinary(ip: string): string | null {
|
||||
// Handle IPv4 addresses only for now
|
||||
const parts = ip.split('.');
|
||||
|
||||
// Validate IP format
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert each octet to 8-bit binary and concatenate
|
||||
try {
|
||||
return parts
|
||||
.map(part => {
|
||||
const num = parseInt(part, 10);
|
||||
if (isNaN(num) || num < 0 || num > 255) {
|
||||
throw new Error('Invalid IP octet');
|
||||
}
|
||||
return num.toString(2).padStart(8, '0');
|
||||
})
|
||||
.join('');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -500,68 +500,8 @@ export class NetworkProxy implements IMetricsTracker {
|
||||
this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use updateRouteConfigs instead
|
||||
* Legacy method for updating proxy configurations using IReverseProxyConfig
|
||||
* This method is maintained for backward compatibility
|
||||
*/
|
||||
public async updateProxyConfigs(
|
||||
proxyConfigsArg: IReverseProxyConfig[]
|
||||
): Promise<void> {
|
||||
this.logger.info(`Converting ${proxyConfigsArg.length} legacy configs to route configs`);
|
||||
|
||||
// Convert legacy configs to route configs
|
||||
const routes: IRouteConfig[] = proxyConfigsArg.map(config =>
|
||||
convertLegacyConfigToRouteConfig(config, this.options.port)
|
||||
);
|
||||
|
||||
// Use the primary method
|
||||
return this.updateRouteConfigs(routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use route-based configuration instead
|
||||
* Converts SmartProxy domain configurations to NetworkProxy configs
|
||||
* This method is maintained for backward compatibility
|
||||
*/
|
||||
public convertSmartProxyConfigs(
|
||||
domainConfigs: Array<{
|
||||
domains: string[];
|
||||
targetIPs?: string[];
|
||||
allowedIPs?: string[];
|
||||
}>,
|
||||
sslKeyPair?: { key: string; cert: string }
|
||||
): IReverseProxyConfig[] {
|
||||
this.logger.warn('convertSmartProxyConfigs is deprecated - use route-based configuration instead');
|
||||
|
||||
const proxyConfigs: IReverseProxyConfig[] = [];
|
||||
|
||||
// Use default certificates if not provided
|
||||
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
||||
const sslKey = sslKeyPair?.key || defaultCerts.key;
|
||||
const sslCert = sslKeyPair?.cert || defaultCerts.cert;
|
||||
|
||||
for (const domainConfig of domainConfigs) {
|
||||
// Each domain in the domains array gets its own config
|
||||
for (const domain of domainConfig.domains) {
|
||||
// Skip non-hostname patterns (like IP addresses)
|
||||
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
|
||||
continue;
|
||||
}
|
||||
|
||||
proxyConfigs.push({
|
||||
hostName: domain,
|
||||
destinationIps: domainConfig.targetIPs || ['localhost'],
|
||||
destinationPorts: [this.options.port], // Use the NetworkProxy port
|
||||
privateKey: sslKey,
|
||||
publicKey: sslCert
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Converted ${domainConfigs.length} SmartProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
|
||||
return proxyConfigs;
|
||||
}
|
||||
// Legacy methods have been removed.
|
||||
// Please use updateRouteConfigs() directly with modern route-based configuration.
|
||||
|
||||
/**
|
||||
* Adds default headers to be included in all responses
|
||||
@ -650,62 +590,4 @@ export class NetworkProxy implements IMetricsTracker {
|
||||
public getRouteConfigs(): IRouteConfig[] {
|
||||
return this.routeManager.getRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getRouteConfigs instead
|
||||
* Gets all proxy configurations currently in use in the legacy format
|
||||
* This method is maintained for backward compatibility
|
||||
*/
|
||||
public getProxyConfigs(): IReverseProxyConfig[] {
|
||||
this.logger.warn('getProxyConfigs is deprecated - use getRouteConfigs instead');
|
||||
|
||||
// Create legacy proxy configs from our route configurations
|
||||
const legacyConfigs: IReverseProxyConfig[] = [];
|
||||
const currentRoutes = this.routeManager.getRoutes();
|
||||
|
||||
for (const route of currentRoutes) {
|
||||
// Skip non-forward routes or routes without domains
|
||||
if (route.action.type !== 'forward' || !route.match.domains || !route.action.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip routes with function-based targets
|
||||
if (typeof route.action.target.host === 'function' || typeof route.action.target.port === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get domains
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains.filter(d => !d.includes('*'))
|
||||
: route.match.domains.includes('*') ? [] : [route.match.domains];
|
||||
|
||||
// Get certificate
|
||||
let privateKey = '';
|
||||
let publicKey = '';
|
||||
|
||||
if (route.action.tls?.certificate && route.action.tls.certificate !== 'auto') {
|
||||
privateKey = route.action.tls.certificate.key;
|
||||
publicKey = route.action.tls.certificate.cert;
|
||||
} else {
|
||||
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
||||
privateKey = defaultCerts.key;
|
||||
publicKey = defaultCerts.cert;
|
||||
}
|
||||
|
||||
// Create legacy config for each domain
|
||||
for (const domain of domains) {
|
||||
legacyConfigs.push({
|
||||
hostName: domain,
|
||||
destinationIps: Array.isArray(route.action.target.host)
|
||||
? route.action.target.host
|
||||
: [route.action.target.host],
|
||||
destinationPorts: [route.action.target.port],
|
||||
privateKey,
|
||||
publicKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return legacyConfigs;
|
||||
}
|
||||
}
|
@ -661,150 +661,6 @@ export class RequestHandler {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Find target based on hostname
|
||||
const proxyConfig = this.router.routeReq(req);
|
||||
|
||||
if (!proxyConfig) {
|
||||
// No matching proxy configuration
|
||||
this.logger.warn(`No proxy configuration for host: ${req.headers.host}`);
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found: No proxy configuration for this host');
|
||||
|
||||
// Increment failed requests counter
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get destination IP using round-robin if multiple IPs configured
|
||||
const destination = this.connectionPool.getNextTarget(
|
||||
proxyConfig.destinationIps,
|
||||
proxyConfig.destinationPorts[0]
|
||||
);
|
||||
|
||||
// Create options for the proxy request
|
||||
const options: plugins.http.RequestOptions = {
|
||||
hostname: destination.host,
|
||||
port: destination.port,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: { ...req.headers }
|
||||
};
|
||||
|
||||
// Remove host header to avoid issues with virtual hosts on target server
|
||||
// The host header should match the target server's expected hostname
|
||||
if (options.headers && options.headers.host) {
|
||||
if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
||||
options.headers.host = `${destination.host}:${destination.port}`;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Proxying request to ${destination.host}:${destination.port}${req.url}`,
|
||||
{ method: req.method }
|
||||
);
|
||||
|
||||
// Create proxy request
|
||||
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
||||
// Copy status code
|
||||
res.statusCode = proxyRes.statusCode || 500;
|
||||
|
||||
// Copy headers from proxy response to client response
|
||||
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
||||
if (value !== undefined) {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe proxy response to client response
|
||||
proxyRes.pipe(res);
|
||||
|
||||
// Increment served requests counter when the response finishes
|
||||
res.on('finish', () => {
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementRequestsServed();
|
||||
}
|
||||
|
||||
// Log the completed request
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.debug(
|
||||
`Request completed in ${duration}ms: ${req.method} ${req.url} ${res.statusCode}`,
|
||||
{ duration, statusCode: res.statusCode }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle proxy request errors
|
||||
proxyReq.on('error', (error) => {
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.error(
|
||||
`Proxy error for ${req.method} ${req.url}: ${error.message}`,
|
||||
{ duration, error: error.message }
|
||||
);
|
||||
|
||||
// Increment failed requests counter
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
|
||||
// Check if headers have already been sent
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 502;
|
||||
res.end(`Bad Gateway: ${error.message}`);
|
||||
} else {
|
||||
// If headers already sent, just close the connection
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Pipe request body to proxy request and handle client-side errors
|
||||
req.pipe(proxyReq);
|
||||
|
||||
// Handle client disconnection
|
||||
req.on('error', (error) => {
|
||||
this.logger.debug(`Client connection error: ${error.message}`);
|
||||
proxyReq.destroy();
|
||||
|
||||
// Increment failed requests counter on client errors
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle response errors
|
||||
res.on('error', (error) => {
|
||||
this.logger.debug(`Response error: ${error.message}`);
|
||||
proxyReq.destroy();
|
||||
|
||||
// Increment failed requests counter on response errors
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Handle any unexpected errors
|
||||
this.logger.error(
|
||||
`Unexpected error handling request: ${error.message}`,
|
||||
{ error: error.stack }
|
||||
);
|
||||
|
||||
// Increment failed requests counter
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.end('Internal Server Error');
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user