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:
@ -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 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user