feat(routes,email): persist system DNS routes with runtime hydration and add reusable email ops DNS helpers

This commit is contained in:
2026-04-15 19:59:04 +00:00
parent e0386beb15
commit 39f449cbe4
24 changed files with 1221 additions and 2525 deletions

View File

@@ -14,6 +14,11 @@ import type { ReferenceResolver } from './classes.reference-resolver.js';
/** An IP allow entry: plain IP/CIDR or domain-scoped. */
export type TIpAllowEntry = string | { ip: string; domains: string[] };
export interface IRouteMutationResult {
success: boolean;
message?: string;
}
/**
* Simple async mutex — serializes concurrent applyRoutes() calls so the Rust engine
* never receives rapid overlapping route updates that can churn UDP/QUIC listeners.
@@ -56,6 +61,7 @@ export class RouteConfigManager {
private referenceResolver?: ReferenceResolver,
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
) {}
/** Expose routes map for reference resolution lookups. */
@@ -63,6 +69,10 @@ export class RouteConfigManager {
return this.routes;
}
public getRoute(id: string): IRoute | undefined {
return this.routes.get(id);
}
/**
* Load persisted routes, seed serializable config/email/dns routes,
* compute warnings, and apply the combined DB-backed + runtime route set to SmartProxy.
@@ -94,6 +104,7 @@ export class RouteConfigManager {
id: route.id,
enabled: route.enabled,
origin: route.origin,
systemKey: route.systemKey,
createdAt: route.createdAt,
updatedAt: route.updatedAt,
metadata: route.metadata,
@@ -153,9 +164,21 @@ export class RouteConfigManager {
enabled?: boolean;
metadata?: Partial<IRouteMetadata>;
},
): Promise<boolean> {
): Promise<IRouteMutationResult> {
const stored = this.routes.get(id);
if (!stored) return false;
if (!stored) {
return { success: false, message: 'Route not found' };
}
const isToggleOnlyPatch = patch.enabled !== undefined
&& patch.route === undefined
&& patch.metadata === undefined;
if (stored.origin !== 'api' && !isToggleOnlyPatch) {
return {
success: false,
message: 'System routes are managed by the system and can only be toggled',
};
}
if (patch.route) {
const mergedAction = patch.route.action
@@ -189,19 +212,29 @@ export class RouteConfigManager {
await this.persistRoute(stored);
await this.applyRoutes();
return true;
return { success: true };
}
public async deleteRoute(id: string): Promise<boolean> {
if (!this.routes.has(id)) return false;
public async deleteRoute(id: string): Promise<IRouteMutationResult> {
const stored = this.routes.get(id);
if (!stored) {
return { success: false, message: 'Route not found' };
}
if (stored.origin !== 'api') {
return {
success: false,
message: 'System routes are managed by the system and cannot be deleted',
};
}
this.routes.delete(id);
const doc = await RouteDoc.findById(id);
if (doc) await doc.delete();
await this.applyRoutes();
return true;
return { success: true };
}
public async toggleRoute(id: string, enabled: boolean): Promise<boolean> {
public async toggleRoute(id: string, enabled: boolean): Promise<IRouteMutationResult> {
return this.updateRoute(id, { enabled });
}
@@ -217,29 +250,28 @@ export class RouteConfigManager {
seedRoutes: IDcRouterRouteConfig[],
origin: 'config' | 'email' | 'dns',
): Promise<void> {
if (seedRoutes.length === 0) return;
const seedSystemKeys = new Set<string>();
const seedNames = new Set<string>();
let seeded = 0;
let updated = 0;
for (const route of seedRoutes) {
const name = route.name || '';
seedNames.add(name);
// Check if a route with this name+origin already exists in memory
let existingId: string | undefined;
for (const [id, r] of this.routes) {
if (r.origin === origin && r.route.name === name) {
existingId = id;
break;
}
if (name) {
seedNames.add(name);
}
const systemKey = this.buildSystemRouteKey(origin, route);
if (systemKey) {
seedSystemKeys.add(systemKey);
}
const existingId = this.findExistingSeedRouteId(origin, route, systemKey);
if (existingId) {
// Update route config but preserve enabled state
const existing = this.routes.get(existingId)!;
existing.route = route;
existing.systemKey = systemKey;
existing.updatedAt = Date.now();
await this.persistRoute(existing);
updated++;
@@ -255,6 +287,7 @@ export class RouteConfigManager {
updatedAt: now,
createdBy: 'system',
origin,
systemKey,
};
this.routes.set(id, newRoute);
await this.persistRoute(newRoute);
@@ -265,7 +298,12 @@ export class RouteConfigManager {
// Delete stale routes: same origin but name not in current seed set
const staleIds: string[] = [];
for (const [id, r] of this.routes) {
if (r.origin === origin && !seedNames.has(r.route.name || '')) {
if (r.origin !== origin) continue;
const routeName = r.route.name || '';
const matchesSeedSystemKey = r.systemKey ? seedSystemKeys.has(r.systemKey) : false;
const matchesSeedName = routeName ? seedNames.has(routeName) : false;
if (!matchesSeedSystemKey && !matchesSeedName) {
staleIds.push(id);
}
}
@@ -284,9 +322,39 @@ export class RouteConfigManager {
// Private: persistence
// =========================================================================
private buildSystemRouteKey(
origin: 'config' | 'email' | 'dns',
route: IDcRouterRouteConfig,
): string | undefined {
const name = route.name?.trim();
if (!name) return undefined;
return `${origin}:${name}`;
}
private findExistingSeedRouteId(
origin: 'config' | 'email' | 'dns',
route: IDcRouterRouteConfig,
systemKey?: string,
): string | undefined {
const routeName = route.name || '';
for (const [id, storedRoute] of this.routes) {
if (storedRoute.origin !== origin) continue;
if (systemKey && storedRoute.systemKey === systemKey) {
return id;
}
if (storedRoute.route.name === routeName) {
return id;
}
}
return undefined;
}
private async loadRoutes(): Promise<void> {
const docs = await RouteDoc.findAll();
let prunedRuntimeRoutes = 0;
for (const doc of docs) {
if (!doc.id) continue;
@@ -299,27 +367,15 @@ export class RouteConfigManager {
updatedAt: doc.updatedAt,
createdBy: doc.createdBy,
origin: doc.origin || 'api',
systemKey: doc.systemKey,
metadata: doc.metadata,
};
if (this.isPersistedRuntimeRoute(storedRoute)) {
await doc.delete();
prunedRuntimeRoutes++;
logger.log(
'warn',
`Removed persisted runtime-only route '${storedRoute.route.name || storedRoute.id}' (${storedRoute.id}) from RouteDoc`,
);
continue;
}
this.routes.set(doc.id, storedRoute);
}
if (this.routes.size > 0) {
logger.log('info', `Loaded ${this.routes.size} route(s) from database`);
}
if (prunedRuntimeRoutes > 0) {
logger.log('info', `Pruned ${prunedRuntimeRoutes} persisted runtime-only route(s) from RouteDoc`);
}
}
private async persistRoute(stored: IRoute): Promise<void> {
@@ -330,6 +386,7 @@ export class RouteConfigManager {
existingDoc.updatedAt = stored.updatedAt;
existingDoc.createdBy = stored.createdBy;
existingDoc.origin = stored.origin;
existingDoc.systemKey = stored.systemKey;
existingDoc.metadata = stored.metadata;
await existingDoc.save();
} else {
@@ -341,6 +398,7 @@ export class RouteConfigManager {
doc.updatedAt = stored.updatedAt;
doc.createdBy = stored.createdBy;
doc.origin = stored.origin;
doc.systemKey = stored.systemKey;
doc.metadata = stored.metadata;
await doc.save();
}
@@ -411,7 +469,7 @@ export class RouteConfigManager {
// Add all enabled routes with HTTP/3 and VPN augmentation
for (const route of this.routes.values()) {
if (route.enabled) {
enabledRoutes.push(this.prepareRouteForApply(route.route, route.id));
enabledRoutes.push(this.prepareStoredRouteForApply(route));
}
}
@@ -431,6 +489,11 @@ export class RouteConfigManager {
});
}
private prepareStoredRouteForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig {
const hydratedRoute = this.hydrateStoredRoute?.(storedRoute);
return this.prepareRouteForApply(hydratedRoute || storedRoute.route, storedRoute.id);
}
private prepareRouteForApply(
route: plugins.smartproxy.IRouteConfig,
routeId?: string,
@@ -465,12 +528,4 @@ export class RouteConfigManager {
},
};
}
private isPersistedRuntimeRoute(storedRoute: IRoute): boolean {
const routeName = storedRoute.route.name || '';
const actionType = storedRoute.route.action?.type;
return (routeName.startsWith('dns-over-https-') && actionType === 'socket-handler')
|| (storedRoute.origin === 'dns' && actionType === 'socket-handler');
}
}