feat(nftables): move NFTables forwarding management from the Rust engine to @push.rocks/smartnftables
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '26.2.4',
|
||||
version: '26.3.0',
|
||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
||||
@@ -19,12 +19,14 @@ export { tsclass };
|
||||
import * as smartcrypto from '@push.rocks/smartcrypto';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-local';
|
||||
import * as smartnftables from '@push.rocks/smartnftables';
|
||||
import * as smartrust from '@push.rocks/smartrust';
|
||||
|
||||
export {
|
||||
smartcrypto,
|
||||
smartlog,
|
||||
smartlogDestinationLocal,
|
||||
smartnftables,
|
||||
smartrust,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ type TSmartProxyCommands = {
|
||||
renewCertificate: { params: { routeName: string }; result: void };
|
||||
getCertificateStatus: { params: { routeName: string }; result: any };
|
||||
getListeningPorts: { params: Record<string, never>; result: { ports: number[] } };
|
||||
getNftablesStatus: { params: Record<string, never>; result: any };
|
||||
setSocketHandlerRelay: { params: { socketPath: string }; result: void };
|
||||
addListeningPort: { params: { port: number }; result: void };
|
||||
removeListeningPort: { params: { port: number }; result: void };
|
||||
@@ -159,10 +158,6 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
||||
return result?.ports ?? [];
|
||||
}
|
||||
|
||||
public async getNftablesStatus(): Promise<any> {
|
||||
return this.bridge.sendCommand('getNftablesStatus', {} as Record<string, never>);
|
||||
}
|
||||
|
||||
public async setSocketHandlerRelay(socketPath: string): Promise<void> {
|
||||
await this.bridge.sendCommand('setSocketHandlerRelay', { socketPath });
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import type { IMetrics } from './models/metrics-types.js';
|
||||
/**
|
||||
* SmartProxy - Rust-backed proxy engine with TypeScript configuration API.
|
||||
*
|
||||
* All networking (TCP, TLS, HTTP reverse proxy, connection management, security,
|
||||
* NFTables) is handled by the Rust binary. TypeScript is only:
|
||||
* All networking (TCP, TLS, HTTP reverse proxy, connection management, security)
|
||||
* is handled by the Rust binary. TypeScript is only:
|
||||
* - The npm module interface (types, route helpers)
|
||||
* - The thin IPC wrapper (this class)
|
||||
* - Socket-handler callback relay (for JS-defined handlers)
|
||||
@@ -39,6 +39,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
private socketHandlerServer: SocketHandlerServer | null = null;
|
||||
private datagramHandlerServer: DatagramHandlerServer | null = null;
|
||||
private metricsAdapter: RustMetricsAdapter;
|
||||
private nftablesManager: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null;
|
||||
private routeUpdateLock: Mutex;
|
||||
private stopping = false;
|
||||
private certProvisionPromise: Promise<void> | null = null;
|
||||
@@ -211,6 +212,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply NFTables rules for routes using nftables forwarding engine
|
||||
await this.applyNftablesRules(this.settings.routes);
|
||||
|
||||
// Start metrics polling BEFORE cert provisioning — the Rust engine is already
|
||||
// running and accepting connections, so metrics should be available immediately.
|
||||
// Cert provisioning can hang indefinitely (e.g. DNS-01 ACME timeouts) and must
|
||||
@@ -238,6 +242,12 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.certProvisionPromise = null;
|
||||
}
|
||||
|
||||
// Clean up NFTables rules
|
||||
if (this.nftablesManager) {
|
||||
await this.nftablesManager.cleanup();
|
||||
this.nftablesManager = null;
|
||||
}
|
||||
|
||||
// Stop metrics polling
|
||||
this.metricsAdapter.stopPolling();
|
||||
|
||||
@@ -319,6 +329,13 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.datagramHandlerServer = null;
|
||||
}
|
||||
|
||||
// Update NFTables rules
|
||||
if (this.nftablesManager) {
|
||||
await this.nftablesManager.cleanup();
|
||||
this.nftablesManager = null;
|
||||
}
|
||||
await this.applyNftablesRules(newRoutes);
|
||||
|
||||
// Update stored routes
|
||||
this.settings.routes = newRoutes;
|
||||
|
||||
@@ -411,14 +428,59 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NFTables status (async - calls Rust).
|
||||
* Get NFTables status.
|
||||
*/
|
||||
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
||||
return this.bridge.getNftablesStatus();
|
||||
public getNfTablesStatus(): plugins.smartnftables.INftStatus | null {
|
||||
return this.nftablesManager?.status() ?? null;
|
||||
}
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
/**
|
||||
* Apply NFTables rules for routes using the nftables forwarding engine.
|
||||
*/
|
||||
private async applyNftablesRules(routes: IRouteConfig[]): Promise<void> {
|
||||
const nftRoutes = routes.filter(r => r.action.forwardingEngine === 'nftables');
|
||||
if (nftRoutes.length === 0) return;
|
||||
|
||||
const tableName = nftRoutes.find(r => r.action.nftables?.tableName)?.action.nftables?.tableName ?? 'smartproxy';
|
||||
const nft = new plugins.smartnftables.SmartNftables({ tableName });
|
||||
await nft.initialize();
|
||||
|
||||
for (const route of nftRoutes) {
|
||||
const routeId = route.name || 'unnamed';
|
||||
const targets = route.action.targets;
|
||||
if (!targets) continue;
|
||||
|
||||
const nftOpts = route.action.nftables;
|
||||
const protocol: plugins.smartnftables.TNftProtocol = (nftOpts?.protocol as any) ?? 'tcp';
|
||||
const preserveSourceIP = nftOpts?.preserveSourceIP ?? false;
|
||||
|
||||
const ports = Array.isArray(route.match.ports)
|
||||
? route.match.ports.flatMap(p => typeof p === 'number' ? [p] : [])
|
||||
: typeof route.match.ports === 'number' ? [route.match.ports] : [];
|
||||
|
||||
for (const target of targets) {
|
||||
const targetHost = Array.isArray(target.host) ? target.host[0] : target.host;
|
||||
if (typeof targetHost !== 'string') continue;
|
||||
|
||||
for (const sourcePort of ports) {
|
||||
const targetPort = typeof target.port === 'number' ? target.port : sourcePort;
|
||||
await nft.nat.addPortForwarding(`${routeId}-${sourcePort}-${targetPort}`, {
|
||||
sourcePort,
|
||||
targetHost,
|
||||
targetPort,
|
||||
protocol,
|
||||
preserveSourceIP,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.nftablesManager = nft;
|
||||
logger.log('info', `Applied NFTables rules for ${nftRoutes.length} route(s)`, { component: 'smart-proxy' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Rust configuration object from TS settings.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user