fix(remoteingressedge): reset nftables state on startup and restart before reapplying hub firewall config

This commit is contained in:
2026-04-26 15:08:37 +00:00
parent dd0cd479d5
commit 627603532d
5 changed files with 45 additions and 20 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog # 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) ## 2026-04-26 - 4.17.0 - feat(core)
add performance profiles, transport observability, and edge stream budget controls add performance profiles, transport observability, and edge stream budget controls
+1 -1
View File
@@ -24,7 +24,7 @@
}, },
"dependencies": { "dependencies": {
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartnftables": "^1.0.1", "@push.rocks/smartnftables": "^1.2.0",
"@push.rocks/smartrust": "^1.3.2" "@push.rocks/smartrust": "^1.3.2"
}, },
"repository": { "repository": {
+5 -5
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^6.1.3 specifier: ^6.1.3
version: 6.1.3 version: 6.1.3
'@push.rocks/smartnftables': '@push.rocks/smartnftables':
specifier: ^1.0.1 specifier: ^1.2.0
version: 1.0.1 version: 1.2.0
'@push.rocks/smartrust': '@push.rocks/smartrust':
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2 version: 1.3.2
@@ -1207,8 +1207,8 @@ packages:
'@push.rocks/smartnetwork@4.4.0': '@push.rocks/smartnetwork@4.4.0':
resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==} resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==}
'@push.rocks/smartnftables@1.0.1': '@push.rocks/smartnftables@1.2.0':
resolution: {integrity: sha512-o822GH4J8dlEBvNLbm+CwU4h6isMUEh03tf2ZnOSWXc5iewRDdKdOCDwI/e+WdnGYWyv7gvH0DHztCmne6rTCg==} resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==}
'@push.rocks/smartnpm@2.0.6': '@push.rocks/smartnpm@2.0.6':
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==} resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
@@ -6439,7 +6439,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@push.rocks/smartnftables@1.0.1': '@push.rocks/smartnftables@1.2.0':
dependencies: dependencies:
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/remoteingress', 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.' 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.'
} }
+30 -13
View File
@@ -57,6 +57,7 @@ export class RemoteIngressEdge extends EventEmitter {
private restartAttempts = 0; private restartAttempts = 0;
private statusInterval: ReturnType<typeof setInterval> | undefined; private statusInterval: ReturnType<typeof setInterval> | undefined;
private nft: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null; private nft: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null;
private pendingFirewallConfig: IFirewallConfig | null = null;
constructor() { constructor() {
super(); super();
@@ -114,7 +115,9 @@ export class RemoteIngressEdge extends EventEmitter {
}); });
this.bridge.on('management:firewallConfigUpdated', (data: { firewallConfig: IFirewallConfig }) => { this.bridge.on('management:firewallConfigUpdated', (data: { firewallConfig: IFirewallConfig }) => {
console.log(`[RemoteIngressEdge] Firewall config updated from hub`); 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); this.emit('firewallConfigUpdated', data);
}); });
} }
@@ -122,14 +125,22 @@ export class RemoteIngressEdge extends EventEmitter {
/** /**
* Initialize the nftables manager. Fails gracefully if not running as root. * Initialize the nftables manager. Fails gracefully if not running as root.
*/ */
private async initNft(): Promise<void> { private async initNft(options: { reset?: boolean } = {}): Promise<void> {
try { try {
this.nft = new plugins.smartnftables.SmartNftables({ this.nft = new plugins.smartnftables.SmartNftables({
tableName: 'remoteingress', tableName: 'remoteingress',
dryRun: false, dryRun: false,
}); });
if (options.reset) {
await (this.nft as any).cleanup({ force: true });
}
await this.nft.initialize(); await this.nft.initialize();
console.log('[RemoteIngressEdge] SmartNftables initialized'); console.log('[RemoteIngressEdge] SmartNftables initialized');
if (this.pendingFirewallConfig) {
const pending = this.pendingFirewallConfig;
this.pendingFirewallConfig = null;
await this.applyFirewallConfig(pending);
}
} catch (err) { } catch (err) {
console.warn(`[RemoteIngressEdge] Failed to initialize nftables (not root?): ${err}`); console.warn(`[RemoteIngressEdge] Failed to initialize nftables (not root?): ${err}`);
this.nft = null; this.nft = null;
@@ -142,19 +153,22 @@ export class RemoteIngressEdge extends EventEmitter {
*/ */
private async applyFirewallConfig(config: IFirewallConfig): Promise<void> { private async applyFirewallConfig(config: IFirewallConfig): Promise<void> {
if (!this.nft) { if (!this.nft) {
this.pendingFirewallConfig = config;
return; return;
} }
try { try {
// Full cleanup and reinitialize to replace all rules atomically // 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(); await this.nft.initialize();
// Apply blocked IPs // Apply blocked IPs
if (config.blockedIps && config.blockedIps.length > 0) { if (config.blockedIps && config.blockedIps.length > 0) {
for (const ip of config.blockedIps) { await (this.nft.firewall as any).blockIPSet('hub-blocklist', {
await this.nft.firewall.blockIP(ip); setName: 'blocked_ipv4',
} ips: config.blockedIps,
comment: 'RemoteIngress hub blocklist',
});
console.log(`[RemoteIngressEdge] Blocked ${config.blockedIps.length} IPs`); console.log(`[RemoteIngressEdge] Blocked ${config.blockedIps.length} IPs`);
} }
@@ -213,6 +227,10 @@ export class RemoteIngressEdge extends EventEmitter {
this.savedConfig = edgeConfig; this.savedConfig = edgeConfig;
this.stopping = false; 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(); const spawned = await this.bridge.spawn();
if (!spawned) { if (!spawned) {
throw new Error('Failed to spawn remoteingress-bin'); throw new Error('Failed to spawn remoteingress-bin');
@@ -242,9 +260,6 @@ export class RemoteIngressEdge extends EventEmitter {
this.restartAttempts = 0; this.restartAttempts = 0;
this.restartBackoffMs = 1000; this.restartBackoffMs = 1000;
// Initialize nftables (graceful degradation if not root)
await this.initNft();
// Start periodic status logging // Start periodic status logging
this.statusInterval = setInterval(async () => { this.statusInterval = setInterval(async () => {
try { try {
@@ -272,7 +287,7 @@ export class RemoteIngressEdge extends EventEmitter {
// Clean up nftables rules before stopping // Clean up nftables rules before stopping
if (this.nft) { if (this.nft) {
try { try {
await this.nft.cleanup(); await (this.nft as any).cleanup({ force: true });
} catch (err) { } catch (err) {
console.warn(`[RemoteIngressEdge] nftables cleanup error: ${err}`); console.warn(`[RemoteIngressEdge] nftables cleanup error: ${err}`);
} }
@@ -289,6 +304,7 @@ export class RemoteIngressEdge extends EventEmitter {
this.started = false; this.started = false;
} }
this.savedConfig = null; this.savedConfig = null;
this.pendingFirewallConfig = null;
// Remove all listeners to prevent memory buildup // Remove all listeners to prevent memory buildup
this.bridge.removeAllListeners(); this.bridge.removeAllListeners();
this.removeAllListeners(); this.removeAllListeners();
@@ -344,6 +360,10 @@ export class RemoteIngressEdge extends EventEmitter {
this.restartAttempts++; this.restartAttempts++;
try { 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(); const spawned = await this.bridge.spawn();
if (!spawned) { if (!spawned) {
console.error('[RemoteIngressEdge] Failed to respawn binary'); console.error('[RemoteIngressEdge] Failed to respawn binary');
@@ -366,9 +386,6 @@ export class RemoteIngressEdge extends EventEmitter {
this.restartAttempts = 0; this.restartAttempts = 0;
this.restartBackoffMs = 1000; this.restartBackoffMs = 1000;
// Re-initialize nftables (hub will re-push config via handshake)
await this.initNft();
// Restart periodic status logging // Restart periodic status logging
this.statusInterval = setInterval(async () => { this.statusInterval = setInterval(async () => {
try { try {