feat(edge,hub): add hub-controlled nftables firewall configuration for remote ingress edges
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/remoteingress',
|
||||
version: '4.14.3',
|
||||
version: '4.15.0',
|
||||
description: 'Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { EventEmitter } from 'events';
|
||||
import { decodeConnectionToken } from './classes.token.js';
|
||||
import type { IFirewallConfig } from './classes.remoteingresshub.js';
|
||||
|
||||
// Command map for the edge side of remoteingress-bin
|
||||
type TEdgeCommands = {
|
||||
@@ -55,6 +56,7 @@ export class RemoteIngressEdge extends EventEmitter {
|
||||
private restartBackoffMs = 1000;
|
||||
private restartAttempts = 0;
|
||||
private statusInterval: ReturnType<typeof setInterval> | undefined;
|
||||
private nft: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -110,6 +112,83 @@ export class RemoteIngressEdge extends EventEmitter {
|
||||
console.log(`[RemoteIngressEdge] Ports updated by hub: ${data.listenPorts.join(', ')}`);
|
||||
this.emit('portsUpdated', data);
|
||||
});
|
||||
this.bridge.on('management:firewallConfigUpdated', (data: { firewallConfig: IFirewallConfig }) => {
|
||||
console.log(`[RemoteIngressEdge] Firewall config updated from hub`);
|
||||
this.applyFirewallConfig(data.firewallConfig);
|
||||
this.emit('firewallConfigUpdated', data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the nftables manager. Fails gracefully if not running as root.
|
||||
*/
|
||||
private async initNft(): Promise<void> {
|
||||
try {
|
||||
this.nft = new plugins.smartnftables.SmartNftables({
|
||||
tableName: 'remoteingress',
|
||||
dryRun: false,
|
||||
});
|
||||
await this.nft.initialize();
|
||||
console.log('[RemoteIngressEdge] SmartNftables initialized');
|
||||
} catch (err) {
|
||||
console.warn(`[RemoteIngressEdge] Failed to initialize nftables (not root?): ${err}`);
|
||||
this.nft = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply firewall configuration received from the hub.
|
||||
* Performs a full replacement: cleans up existing rules, then applies the new config.
|
||||
*/
|
||||
private async applyFirewallConfig(config: IFirewallConfig): Promise<void> {
|
||||
if (!this.nft) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Full cleanup and reinitialize to replace all rules atomically
|
||||
await this.nft.cleanup();
|
||||
await this.nft.initialize();
|
||||
|
||||
// Apply blocked IPs
|
||||
if (config.blockedIps && config.blockedIps.length > 0) {
|
||||
for (const ip of config.blockedIps) {
|
||||
await this.nft.firewall.blockIP(ip);
|
||||
}
|
||||
console.log(`[RemoteIngressEdge] Blocked ${config.blockedIps.length} IPs`);
|
||||
}
|
||||
|
||||
// Apply rate limits
|
||||
if (config.rateLimits && config.rateLimits.length > 0) {
|
||||
for (const rl of config.rateLimits) {
|
||||
await this.nft.rateLimit.addRateLimit(rl.id, {
|
||||
port: rl.port,
|
||||
protocol: rl.protocol,
|
||||
rate: rl.rate,
|
||||
burst: rl.burst,
|
||||
perSourceIP: rl.perSourceIP,
|
||||
});
|
||||
}
|
||||
console.log(`[RemoteIngressEdge] Applied ${config.rateLimits.length} rate limits`);
|
||||
}
|
||||
|
||||
// Apply firewall rules
|
||||
if (config.rules && config.rules.length > 0) {
|
||||
for (const rule of config.rules) {
|
||||
await this.nft.firewall.addRule(rule.id, {
|
||||
direction: rule.direction,
|
||||
action: rule.action,
|
||||
sourceIP: rule.sourceIP,
|
||||
destPort: rule.destPort,
|
||||
protocol: rule.protocol,
|
||||
comment: rule.comment,
|
||||
});
|
||||
}
|
||||
console.log(`[RemoteIngressEdge] Applied ${config.rules.length} firewall rules`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[RemoteIngressEdge] Failed to apply firewall config: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,6 +235,9 @@ export class RemoteIngressEdge extends EventEmitter {
|
||||
this.restartAttempts = 0;
|
||||
this.restartBackoffMs = 1000;
|
||||
|
||||
// Initialize nftables (graceful degradation if not root)
|
||||
await this.initNft();
|
||||
|
||||
// Start periodic status logging
|
||||
this.statusInterval = setInterval(async () => {
|
||||
try {
|
||||
@@ -180,6 +262,15 @@ export class RemoteIngressEdge extends EventEmitter {
|
||||
clearInterval(this.statusInterval);
|
||||
this.statusInterval = undefined;
|
||||
}
|
||||
// Clean up nftables rules before stopping
|
||||
if (this.nft) {
|
||||
try {
|
||||
await this.nft.cleanup();
|
||||
} catch (err) {
|
||||
console.warn(`[RemoteIngressEdge] nftables cleanup error: ${err}`);
|
||||
}
|
||||
this.nft = null;
|
||||
}
|
||||
if (this.started) {
|
||||
try {
|
||||
await this.bridge.sendCommand('stopEdge', {} as Record<string, never>);
|
||||
@@ -261,6 +352,9 @@ export class RemoteIngressEdge extends EventEmitter {
|
||||
this.restartAttempts = 0;
|
||||
this.restartBackoffMs = 1000;
|
||||
|
||||
// Re-initialize nftables (hub will re-push config via handshake)
|
||||
await this.initNft();
|
||||
|
||||
// Restart periodic status logging
|
||||
this.statusInterval = setInterval(async () => {
|
||||
try {
|
||||
|
||||
@@ -22,7 +22,7 @@ type THubCommands = {
|
||||
};
|
||||
updateAllowedEdges: {
|
||||
params: {
|
||||
edges: Array<{ id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number }>;
|
||||
edges: Array<{ id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig }>;
|
||||
};
|
||||
result: { updated: boolean };
|
||||
};
|
||||
@@ -41,6 +41,31 @@ type THubCommands = {
|
||||
};
|
||||
};
|
||||
|
||||
export interface IFirewallRateLimit {
|
||||
id: string;
|
||||
port: number;
|
||||
protocol?: 'tcp' | 'udp';
|
||||
rate: string;
|
||||
burst?: number;
|
||||
perSourceIP?: boolean;
|
||||
}
|
||||
|
||||
export interface IFirewallRule {
|
||||
id: string;
|
||||
direction: 'input' | 'output' | 'forward';
|
||||
action: 'accept' | 'drop' | 'reject';
|
||||
sourceIP?: string;
|
||||
destPort?: number;
|
||||
protocol?: 'tcp' | 'udp';
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface IFirewallConfig {
|
||||
blockedIps?: string[];
|
||||
rateLimits?: IFirewallRateLimit[];
|
||||
rules?: IFirewallRule[];
|
||||
}
|
||||
|
||||
export interface IHubConfig {
|
||||
tunnelPort?: number;
|
||||
targetHost?: string;
|
||||
@@ -50,7 +75,7 @@ export interface IHubConfig {
|
||||
};
|
||||
}
|
||||
|
||||
type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number };
|
||||
type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig };
|
||||
|
||||
const MAX_RESTART_ATTEMPTS = 10;
|
||||
const MAX_RESTART_BACKOFF_MS = 30_000;
|
||||
|
||||
@@ -3,5 +3,6 @@ import * as path from 'path';
|
||||
export { path };
|
||||
|
||||
// @push.rocks scope
|
||||
import * as smartnftables from '@push.rocks/smartnftables';
|
||||
import * as smartrust from '@push.rocks/smartrust';
|
||||
export { smartrust };
|
||||
export { smartnftables, smartrust };
|
||||
|
||||
Reference in New Issue
Block a user