fix(port-manager, certificate-manager): Improve port binding and ACME challenge route integration in SmartProxy

This commit is contained in:
2025-05-20 15:32:19 +00:00
parent 3b1531d4a2
commit 669cc2809c
7 changed files with 746 additions and 26 deletions

View File

@ -64,6 +64,9 @@ export class SmartProxy extends plugins.EventEmitter {
private routeUpdateLock: any = null; // Will be initialized as AsyncMutex
private acmeStateManager: AcmeStateManager;
// Track port usage across route updates
private portUsageMap: Map<number, Set<string>> = new Map();
/**
* Constructor for SmartProxy
*
@ -342,6 +345,16 @@ export class SmartProxy extends plugins.EventEmitter {
// Get listening ports from RouteManager
const listeningPorts = this.routeManager.getListeningPorts();
// Initialize port usage tracking
this.portUsageMap = this.updatePortUsageMap(this.settings.routes);
// Log port usage for startup
logger.log('info', `SmartProxy starting with ${listeningPorts.length} ports: ${listeningPorts.join(', ')}`, {
portCount: listeningPorts.length,
ports: listeningPorts,
component: 'smart-proxy'
});
// Provision NFTables rules for routes that use NFTables
for (const route of this.settings.routes) {
if (route.action.forwardingEngine === 'nftables') {
@ -548,6 +561,18 @@ export class SmartProxy extends plugins.EventEmitter {
return this.routeUpdateLock.runExclusive(async () => {
logger.log('info', `Updating routes (${newRoutes.length} routes)`, { routeCount: newRoutes.length, component: 'route-manager' });
// Track port usage before and after updates
const oldPortUsage = this.updatePortUsageMap(this.settings.routes);
const newPortUsage = this.updatePortUsageMap(newRoutes);
// Find orphaned ports - ports that no longer have any routes
const orphanedPorts = this.findOrphanedPorts(oldPortUsage, newPortUsage);
// Find new ports that need binding
const currentPorts = new Set(this.portManager.getListeningPorts());
const newPortsSet = new Set(newPortUsage.keys());
const newBindingPorts = Array.from(newPortsSet).filter(p => !currentPorts.has(p));
// Get existing routes that use NFTables
const oldNfTablesRoutes = this.settings.routes.filter(
r => r.action.forwardingEngine === 'nftables'
@ -584,14 +609,29 @@ export class SmartProxy extends plugins.EventEmitter {
// Update routes in RouteManager
this.routeManager.updateRoutes(newRoutes);
// Get the new set of required ports
const requiredPorts = this.routeManager.getListeningPorts();
// Update port listeners to match the new configuration
await this.portManager.updatePorts(requiredPorts);
// Release orphaned ports first
if (orphanedPorts.length > 0) {
logger.log('info', `Releasing ${orphanedPorts.length} orphaned ports: ${orphanedPorts.join(', ')}`, {
ports: orphanedPorts,
component: 'smart-proxy'
});
await this.portManager.removePorts(orphanedPorts);
}
// Add new ports
if (newBindingPorts.length > 0) {
logger.log('info', `Binding to ${newBindingPorts.length} new ports: ${newBindingPorts.join(', ')}`, {
ports: newBindingPorts,
component: 'smart-proxy'
});
await this.portManager.addPorts(newBindingPorts);
}
// Update settings with the new routes
this.settings.routes = newRoutes;
// Save the new port usage map for future reference
this.portUsageMap = newPortUsage;
// If HttpProxy is initialized, resync the configurations
if (this.httpProxyBridge.getHttpProxy()) {
@ -637,6 +677,78 @@ export class SmartProxy extends plugins.EventEmitter {
await this.certManager.provisionCertificate(route);
}
/**
* Update the port usage map based on the provided routes
*
* This tracks which ports are used by which routes, allowing us to
* detect when a port is no longer needed and can be released.
*/
private updatePortUsageMap(routes: IRouteConfig[]): Map<number, Set<string>> {
// Reset the usage map
const portUsage = new Map<number, Set<string>>();
for (const route of routes) {
// Get the ports for this route
const portsConfig = Array.isArray(route.match.ports)
? route.match.ports
: [route.match.ports];
// Expand port range objects to individual port numbers
const expandedPorts: number[] = [];
for (const portConfig of portsConfig) {
if (typeof portConfig === 'number') {
expandedPorts.push(portConfig);
} else if (typeof portConfig === 'object' && 'from' in portConfig && 'to' in portConfig) {
// Expand the port range
for (let p = portConfig.from; p <= portConfig.to; p++) {
expandedPorts.push(p);
}
}
}
// Use route name if available, otherwise generate a unique ID
const routeName = route.name || `unnamed_${Math.random().toString(36).substring(2, 9)}`;
// Add each port to the usage map
for (const port of expandedPorts) {
if (!portUsage.has(port)) {
portUsage.set(port, new Set());
}
portUsage.get(port)!.add(routeName);
}
}
// Log port usage for debugging
for (const [port, routes] of portUsage.entries()) {
logger.log('debug', `Port ${port} is used by ${routes.size} routes: ${Array.from(routes).join(', ')}`, {
port,
routeCount: routes.size,
component: 'smart-proxy'
});
}
return portUsage;
}
/**
* Find ports that have no routes in the new configuration
*/
private findOrphanedPorts(oldUsage: Map<number, Set<string>>, newUsage: Map<number, Set<string>>): number[] {
const orphanedPorts: number[] = [];
for (const [port, routes] of oldUsage.entries()) {
if (!newUsage.has(port) || newUsage.get(port)!.size === 0) {
orphanedPorts.push(port);
logger.log('info', `Port ${port} no longer has any associated routes, will be released`, {
port,
component: 'smart-proxy'
});
}
}
return orphanedPorts;
}
/**
* Force renewal of a certificate