feat(edge,hub): add hub-controlled nftables firewall configuration for remote ingress edges

This commit is contained in:
2026-03-26 16:39:53 +00:00
parent c2c9dd195d
commit e9a08bdd0f
11 changed files with 349 additions and 47 deletions

View File

@@ -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 {