fix(acme): Fix port 80 ACME management and challenge route concurrency issues by deduplicating port listeners, preserving challenge route state across certificate manager recreations, and adding mutex locks to route updates.

This commit is contained in:
2025-05-19 03:40:58 +00:00
parent 0bd35c4fb3
commit 3fcdce611c
13 changed files with 1494 additions and 351 deletions

View File

@ -20,6 +20,12 @@ import type {
} from './models/interfaces.js';
import type { IRouteConfig } from './models/route-types.js';
// Import mutex for route update synchronization
import { Mutex } from './utils/mutex.js';
// Import ACME state manager
import { AcmeStateManager } from './acme-state-manager.js';
/**
* SmartProxy - Pure route-based API
*
@ -52,6 +58,11 @@ export class SmartProxy extends plugins.EventEmitter {
// Certificate manager for ACME and static certificates
private certManager: SmartCertManager | null = null;
// Global challenge route tracking
private globalChallengeRouteActive: boolean = false;
private routeUpdateLock: any = null; // Will be initialized as AsyncMutex
private acmeStateManager: AcmeStateManager;
/**
* Constructor for SmartProxy
*
@ -171,6 +182,12 @@ export class SmartProxy extends plugins.EventEmitter {
// Initialize NFTablesManager
this.nftablesManager = new NFTablesManager(this.settings);
// Initialize route update mutex for synchronization
this.routeUpdateLock = new Mutex();
// Initialize ACME state manager
this.acmeStateManager = new AcmeStateManager();
}
/**
@ -185,9 +202,10 @@ export class SmartProxy extends plugins.EventEmitter {
private async createCertificateManager(
routes: IRouteConfig[],
certStore: string = './certs',
acmeOptions?: any
acmeOptions?: any,
initialState?: { challengeRouteActive?: boolean }
): Promise<SmartCertManager> {
const certManager = new SmartCertManager(routes, certStore, acmeOptions);
const certManager = new SmartCertManager(routes, certStore, acmeOptions, initialState);
// Always set up the route update callback for ACME challenges
certManager.setUpdateRoutesCallback(async (routes) => {
@ -199,6 +217,9 @@ export class SmartProxy extends plugins.EventEmitter {
certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
}
// Set the ACME state manager
certManager.setAcmeStateManager(this.acmeStateManager);
// Pass down the global ACME config if available
if (this.settings.acme) {
certManager.setGlobalAcmeDefaults(this.settings.acme);
@ -441,7 +462,9 @@ export class SmartProxy extends plugins.EventEmitter {
// Stop NetworkProxy
await this.networkProxyBridge.stop();
// Clear ACME state manager
this.acmeStateManager.clear();
console.log('SmartProxy shutdown complete.');
}
@ -456,6 +479,29 @@ export class SmartProxy extends plugins.EventEmitter {
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
}
/**
* Verify the challenge route has been properly removed from routes
*/
private async verifyChallengeRouteRemoved(): Promise<void> {
const maxRetries = 10;
const retryDelay = 100; // milliseconds
for (let i = 0; i < maxRetries; i++) {
// Check if the challenge route is still in the active routes
const challengeRouteExists = this.settings.routes.some(r => r.name === 'acme-challenge');
if (!challengeRouteExists) {
console.log('Challenge route successfully removed from routes');
return;
}
// Wait before retrying
await plugins.smartdelay.delayFor(retryDelay);
}
throw new Error('Failed to verify challenge route removal after ' + maxRetries + ' attempts');
}
/**
* Update routes with new configuration
*
@ -480,70 +526,81 @@ export class SmartProxy extends plugins.EventEmitter {
* ```
*/
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
console.log(`Updating routes (${newRoutes.length} routes)`);
return this.routeUpdateLock.runExclusive(async () => {
console.log(`Updating routes (${newRoutes.length} routes)`);
// Get existing routes that use NFTables
const oldNfTablesRoutes = this.settings.routes.filter(
r => r.action.forwardingEngine === 'nftables'
);
// Get new routes that use NFTables
const newNfTablesRoutes = newRoutes.filter(
r => r.action.forwardingEngine === 'nftables'
);
// Find routes to remove, update, or add
for (const oldRoute of oldNfTablesRoutes) {
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
if (!newRoute) {
// Route was removed
await this.nftablesManager.deprovisionRoute(oldRoute);
} else {
// Route was updated
await this.nftablesManager.updateRoute(oldRoute, newRoute);
}
}
// Find new routes to add
for (const newRoute of newNfTablesRoutes) {
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
if (!oldRoute) {
// New route
await this.nftablesManager.provisionRoute(newRoute);
}
}
// 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);
// Update settings with the new routes
this.settings.routes = newRoutes;
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) {
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
}
// Update certificate manager with new routes
if (this.certManager) {
const existingAcmeOptions = this.certManager.getAcmeOptions();
await this.certManager.stop();
// Use the helper method to create and configure the certificate manager
this.certManager = await this.createCertificateManager(
newRoutes,
'./certs',
existingAcmeOptions
// Get existing routes that use NFTables
const oldNfTablesRoutes = this.settings.routes.filter(
r => r.action.forwardingEngine === 'nftables'
);
}
// Get new routes that use NFTables
const newNfTablesRoutes = newRoutes.filter(
r => r.action.forwardingEngine === 'nftables'
);
// Find routes to remove, update, or add
for (const oldRoute of oldNfTablesRoutes) {
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
if (!newRoute) {
// Route was removed
await this.nftablesManager.deprovisionRoute(oldRoute);
} else {
// Route was updated
await this.nftablesManager.updateRoute(oldRoute, newRoute);
}
}
// Find new routes to add
for (const newRoute of newNfTablesRoutes) {
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
if (!oldRoute) {
// New route
await this.nftablesManager.provisionRoute(newRoute);
}
}
// 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);
// Update settings with the new routes
this.settings.routes = newRoutes;
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) {
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
}
// Update certificate manager with new routes
if (this.certManager) {
const existingAcmeOptions = this.certManager.getAcmeOptions();
const existingState = this.certManager.getState();
// Store global state before stopping
this.globalChallengeRouteActive = existingState.challengeRouteActive;
await this.certManager.stop();
// Verify the challenge route has been properly removed
await this.verifyChallengeRouteRemoved();
// Create new certificate manager with preserved state
this.certManager = await this.createCertificateManager(
newRoutes,
'./certs',
existingAcmeOptions,
{ challengeRouteActive: this.globalChallengeRouteActive }
);
}
});
}
/**