From 627603532dde1b2372b5ee9f1b4c80d19dc9fd60 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 26 Apr 2026 15:08:37 +0000 Subject: [PATCH] fix(remoteingressedge): reset nftables state on startup and restart before reapplying hub firewall config --- changelog.md | 8 ++++++ package.json | 2 +- pnpm-lock.yaml | 10 ++++---- ts/00_commitinfo_data.ts | 2 +- ts/classes.remoteingressedge.ts | 43 +++++++++++++++++++++++---------- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/changelog.md b/changelog.md index 27cd549..7aa58f3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-26 - 4.17.1 - fix(remoteingressedge) +reset nftables state on startup and restart before reapplying hub firewall config + +- upgrade @push.rocks/smartnftables to ^1.2.0 to use forced cleanup and IP set blocking +- queue firewall updates until nftables is initialized and apply pending config afterward +- replace per-IP blocking with blockIPSet for the hub blocklist +- force nftables cleanup during startup, restart, firewall replacement, and shutdown to remove stale kernel rules + ## 2026-04-26 - 4.17.0 - feat(core) add performance profiles, transport observability, and edge stream budget controls diff --git a/package.json b/package.json index d8a53e1..fce3ad2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@push.rocks/qenv": "^6.1.3", - "@push.rocks/smartnftables": "^1.0.1", + "@push.rocks/smartnftables": "^1.2.0", "@push.rocks/smartrust": "^1.3.2" }, "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9db17ac..0bbf0fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^6.1.3 version: 6.1.3 '@push.rocks/smartnftables': - specifier: ^1.0.1 - version: 1.0.1 + specifier: ^1.2.0 + version: 1.2.0 '@push.rocks/smartrust': specifier: ^1.3.2 version: 1.3.2 @@ -1207,8 +1207,8 @@ packages: '@push.rocks/smartnetwork@4.4.0': resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==} - '@push.rocks/smartnftables@1.0.1': - resolution: {integrity: sha512-o822GH4J8dlEBvNLbm+CwU4h6isMUEh03tf2ZnOSWXc5iewRDdKdOCDwI/e+WdnGYWyv7gvH0DHztCmne6rTCg==} + '@push.rocks/smartnftables@1.2.0': + resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==} '@push.rocks/smartnpm@2.0.6': resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==} @@ -6439,7 +6439,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@push.rocks/smartnftables@1.0.1': + '@push.rocks/smartnftables@1.2.0': dependencies: '@push.rocks/smartlog': 3.2.1 '@push.rocks/smartpromise': 4.2.3 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 76566d2..b2c13ef 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/remoteingress', - version: '4.17.0', + version: '4.17.1', 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.' } diff --git a/ts/classes.remoteingressedge.ts b/ts/classes.remoteingressedge.ts index 8a1c7c4..867f489 100644 --- a/ts/classes.remoteingressedge.ts +++ b/ts/classes.remoteingressedge.ts @@ -57,6 +57,7 @@ export class RemoteIngressEdge extends EventEmitter { private restartAttempts = 0; private statusInterval: ReturnType | undefined; private nft: InstanceType | null = null; + private pendingFirewallConfig: IFirewallConfig | null = null; constructor() { super(); @@ -114,7 +115,9 @@ export class RemoteIngressEdge extends EventEmitter { }); this.bridge.on('management:firewallConfigUpdated', (data: { firewallConfig: IFirewallConfig }) => { console.log(`[RemoteIngressEdge] Firewall config updated from hub`); - this.applyFirewallConfig(data.firewallConfig); + void this.applyFirewallConfig(data.firewallConfig).catch((err) => { + console.error(`[RemoteIngressEdge] Failed to apply firewall config: ${err}`); + }); this.emit('firewallConfigUpdated', data); }); } @@ -122,14 +125,22 @@ export class RemoteIngressEdge extends EventEmitter { /** * Initialize the nftables manager. Fails gracefully if not running as root. */ - private async initNft(): Promise { + private async initNft(options: { reset?: boolean } = {}): Promise { try { this.nft = new plugins.smartnftables.SmartNftables({ tableName: 'remoteingress', dryRun: false, }); + if (options.reset) { + await (this.nft as any).cleanup({ force: true }); + } await this.nft.initialize(); console.log('[RemoteIngressEdge] SmartNftables initialized'); + if (this.pendingFirewallConfig) { + const pending = this.pendingFirewallConfig; + this.pendingFirewallConfig = null; + await this.applyFirewallConfig(pending); + } } catch (err) { console.warn(`[RemoteIngressEdge] Failed to initialize nftables (not root?): ${err}`); this.nft = null; @@ -142,19 +153,22 @@ export class RemoteIngressEdge extends EventEmitter { */ private async applyFirewallConfig(config: IFirewallConfig): Promise { if (!this.nft) { + this.pendingFirewallConfig = config; return; } try { // Full cleanup and reinitialize to replace all rules atomically - await this.nft.cleanup(); + await (this.nft as any).cleanup({ force: true }); 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); - } + await (this.nft.firewall as any).blockIPSet('hub-blocklist', { + setName: 'blocked_ipv4', + ips: config.blockedIps, + comment: 'RemoteIngress hub blocklist', + }); console.log(`[RemoteIngressEdge] Blocked ${config.blockedIps.length} IPs`); } @@ -213,6 +227,10 @@ export class RemoteIngressEdge extends EventEmitter { this.savedConfig = edgeConfig; this.stopping = false; + // Clear any stale nftables state left by a prior process before the edge + // can accept hub config or bind public listener ports. + await this.initNft({ reset: true }); + const spawned = await this.bridge.spawn(); if (!spawned) { throw new Error('Failed to spawn remoteingress-bin'); @@ -242,9 +260,6 @@ 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 { @@ -272,7 +287,7 @@ export class RemoteIngressEdge extends EventEmitter { // Clean up nftables rules before stopping if (this.nft) { try { - await this.nft.cleanup(); + await (this.nft as any).cleanup({ force: true }); } catch (err) { console.warn(`[RemoteIngressEdge] nftables cleanup error: ${err}`); } @@ -289,6 +304,7 @@ export class RemoteIngressEdge extends EventEmitter { this.started = false; } this.savedConfig = null; + this.pendingFirewallConfig = null; // Remove all listeners to prevent memory buildup this.bridge.removeAllListeners(); this.removeAllListeners(); @@ -344,6 +360,10 @@ export class RemoteIngressEdge extends EventEmitter { this.restartAttempts++; try { + // Drop stale kernel rules before reconnecting. The hub will send the + // current full firewall snapshot during handshake/config refresh. + await this.initNft({ reset: true }); + const spawned = await this.bridge.spawn(); if (!spawned) { console.error('[RemoteIngressEdge] Failed to respawn binary'); @@ -366,9 +386,6 @@ 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 {