@serve.zone/remoteingress 4.17.0 → 4.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,7 @@ export class RemoteIngressEdge extends EventEmitter {
57
57
  private restartAttempts = 0;
58
58
  private statusInterval: ReturnType<typeof setInterval> | undefined;
59
59
  private nft: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null;
60
+ private pendingFirewallConfig: IFirewallConfig | null = null;
60
61
 
61
62
  constructor() {
62
63
  super();
@@ -114,7 +115,9 @@ export class RemoteIngressEdge extends EventEmitter {
114
115
  });
115
116
  this.bridge.on('management:firewallConfigUpdated', (data: { firewallConfig: IFirewallConfig }) => {
116
117
  console.log(`[RemoteIngressEdge] Firewall config updated from hub`);
117
- this.applyFirewallConfig(data.firewallConfig);
118
+ void this.applyFirewallConfig(data.firewallConfig).catch((err) => {
119
+ console.error(`[RemoteIngressEdge] Failed to apply firewall config: ${err}`);
120
+ });
118
121
  this.emit('firewallConfigUpdated', data);
119
122
  });
120
123
  }
@@ -122,14 +125,22 @@ export class RemoteIngressEdge extends EventEmitter {
122
125
  /**
123
126
  * Initialize the nftables manager. Fails gracefully if not running as root.
124
127
  */
125
- private async initNft(): Promise<void> {
128
+ private async initNft(options: { reset?: boolean } = {}): Promise<void> {
126
129
  try {
127
130
  this.nft = new plugins.smartnftables.SmartNftables({
128
131
  tableName: 'remoteingress',
129
132
  dryRun: false,
130
133
  });
134
+ if (options.reset) {
135
+ await (this.nft as any).cleanup({ force: true });
136
+ }
131
137
  await this.nft.initialize();
132
138
  console.log('[RemoteIngressEdge] SmartNftables initialized');
139
+ if (this.pendingFirewallConfig) {
140
+ const pending = this.pendingFirewallConfig;
141
+ this.pendingFirewallConfig = null;
142
+ await this.applyFirewallConfig(pending);
143
+ }
133
144
  } catch (err) {
134
145
  console.warn(`[RemoteIngressEdge] Failed to initialize nftables (not root?): ${err}`);
135
146
  this.nft = null;
@@ -142,19 +153,22 @@ export class RemoteIngressEdge extends EventEmitter {
142
153
  */
143
154
  private async applyFirewallConfig(config: IFirewallConfig): Promise<void> {
144
155
  if (!this.nft) {
156
+ this.pendingFirewallConfig = config;
145
157
  return;
146
158
  }
147
159
 
148
160
  try {
149
161
  // Full cleanup and reinitialize to replace all rules atomically
150
- await this.nft.cleanup();
162
+ await (this.nft as any).cleanup({ force: true });
151
163
  await this.nft.initialize();
152
164
 
153
165
  // Apply blocked IPs
154
166
  if (config.blockedIps && config.blockedIps.length > 0) {
155
- for (const ip of config.blockedIps) {
156
- await this.nft.firewall.blockIP(ip);
157
- }
167
+ await (this.nft.firewall as any).blockIPSet('hub-blocklist', {
168
+ setName: 'blocked_ipv4',
169
+ ips: config.blockedIps,
170
+ comment: 'RemoteIngress hub blocklist',
171
+ });
158
172
  console.log(`[RemoteIngressEdge] Blocked ${config.blockedIps.length} IPs`);
159
173
  }
160
174
 
@@ -213,6 +227,10 @@ export class RemoteIngressEdge extends EventEmitter {
213
227
  this.savedConfig = edgeConfig;
214
228
  this.stopping = false;
215
229
 
230
+ // Clear any stale nftables state left by a prior process before the edge
231
+ // can accept hub config or bind public listener ports.
232
+ await this.initNft({ reset: true });
233
+
216
234
  const spawned = await this.bridge.spawn();
217
235
  if (!spawned) {
218
236
  throw new Error('Failed to spawn remoteingress-bin');
@@ -242,9 +260,6 @@ export class RemoteIngressEdge extends EventEmitter {
242
260
  this.restartAttempts = 0;
243
261
  this.restartBackoffMs = 1000;
244
262
 
245
- // Initialize nftables (graceful degradation if not root)
246
- await this.initNft();
247
-
248
263
  // Start periodic status logging
249
264
  this.statusInterval = setInterval(async () => {
250
265
  try {
@@ -272,7 +287,7 @@ export class RemoteIngressEdge extends EventEmitter {
272
287
  // Clean up nftables rules before stopping
273
288
  if (this.nft) {
274
289
  try {
275
- await this.nft.cleanup();
290
+ await (this.nft as any).cleanup({ force: true });
276
291
  } catch (err) {
277
292
  console.warn(`[RemoteIngressEdge] nftables cleanup error: ${err}`);
278
293
  }
@@ -289,6 +304,7 @@ export class RemoteIngressEdge extends EventEmitter {
289
304
  this.started = false;
290
305
  }
291
306
  this.savedConfig = null;
307
+ this.pendingFirewallConfig = null;
292
308
  // Remove all listeners to prevent memory buildup
293
309
  this.bridge.removeAllListeners();
294
310
  this.removeAllListeners();
@@ -344,6 +360,10 @@ export class RemoteIngressEdge extends EventEmitter {
344
360
  this.restartAttempts++;
345
361
 
346
362
  try {
363
+ // Drop stale kernel rules before reconnecting. The hub will send the
364
+ // current full firewall snapshot during handshake/config refresh.
365
+ await this.initNft({ reset: true });
366
+
347
367
  const spawned = await this.bridge.spawn();
348
368
  if (!spawned) {
349
369
  console.error('[RemoteIngressEdge] Failed to respawn binary');
@@ -366,9 +386,6 @@ export class RemoteIngressEdge extends EventEmitter {
366
386
  this.restartAttempts = 0;
367
387
  this.restartBackoffMs = 1000;
368
388
 
369
- // Re-initialize nftables (hub will re-push config via handshake)
370
- await this.initNft();
371
-
372
389
  // Restart periodic status logging
373
390
  this.statusInterval = setInterval(async () => {
374
391
  try {
@@ -135,7 +135,7 @@ export interface IUdpStatus {
135
135
  droppedDatagrams: number;
136
136
  }
137
137
 
138
- type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig; performance?: IPerformanceConfig };
138
+ export type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig; performance?: IPerformanceConfig };
139
139
 
140
140
  const MAX_RESTART_ATTEMPTS = 10;
141
141
  const MAX_RESTART_BACKOFF_MS = 30_000;
package/ts/index.ts CHANGED
@@ -1,3 +1,147 @@
1
+ import { RemoteIngressEdge } from './classes.remoteingressedge.js';
2
+ import { RemoteIngressHub, type IHubConfig, type TAllowedEdge } from './classes.remoteingresshub.js';
3
+
1
4
  export * from './classes.remoteingresshub.js';
2
5
  export * from './classes.remoteingressedge.js';
3
6
  export * from './classes.token.js';
7
+
8
+ const usage = `remoteingress
9
+
10
+ Usage:
11
+ remoteingress hub [--tunnel-port 8443] [--target-host 127.0.0.1]
12
+ remoteingress edge --token <connection-token>
13
+ remoteingress edge --hub-host <host> --edge-id <id> --secret <secret> [--hub-port 8443]
14
+
15
+ Environment:
16
+ REMOTEINGRESS_MODE=hub|edge
17
+ REMOTEINGRESS_TOKEN=<connection-token>
18
+ REMOTEINGRESS_HUB_HOST=<host>
19
+ REMOTEINGRESS_HUB_PORT=8443
20
+ REMOTEINGRESS_EDGE_ID=<id>
21
+ REMOTEINGRESS_SECRET=<secret>
22
+ REMOTEINGRESS_TARGET_HOST=127.0.0.1
23
+ REMOTEINGRESS_ALLOWED_EDGES_JSON='[{"id":"edge-1","secret":"secret","listenPorts":[80,443]}]'
24
+ `;
25
+
26
+ const readArg = (args: string[], name: string): string | undefined => {
27
+ const prefix = `--${name}=`;
28
+ const inlineValue = args.find((arg) => arg.startsWith(prefix));
29
+ if (inlineValue) {
30
+ return inlineValue.slice(prefix.length);
31
+ }
32
+
33
+ const index = args.indexOf(`--${name}`);
34
+ if (index >= 0) {
35
+ return args[index + 1];
36
+ }
37
+
38
+ return undefined;
39
+ };
40
+
41
+ const readNumber = (value: string | undefined, fallback: number): number => {
42
+ if (!value) {
43
+ return fallback;
44
+ }
45
+
46
+ const parsed = Number(value);
47
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
48
+ throw new Error(`Invalid port: ${value}`);
49
+ }
50
+
51
+ return parsed;
52
+ };
53
+
54
+ const readJson = <T>(value: string | undefined, fallback: T): T => {
55
+ if (!value) {
56
+ return fallback;
57
+ }
58
+
59
+ return JSON.parse(value) as T;
60
+ };
61
+
62
+ const waitForever = async (stop: () => Promise<void>) => {
63
+ let stopping = false;
64
+ const handleStop = async () => {
65
+ if (stopping) {
66
+ return;
67
+ }
68
+ stopping = true;
69
+ await stop();
70
+ process.exit(0);
71
+ };
72
+
73
+ process.once('SIGINT', () => void handleStop());
74
+ process.once('SIGTERM', () => void handleStop());
75
+
76
+ await new Promise(() => {});
77
+ };
78
+
79
+ export const runCli = async () => {
80
+ const args = process.argv.slice(2);
81
+ if (args.includes('--help') || args.includes('-h')) {
82
+ console.log(usage);
83
+ return;
84
+ }
85
+
86
+ const positionalMode = args[0]?.startsWith('--') ? undefined : args[0];
87
+ const mode = readArg(args, 'mode') ?? positionalMode ?? process.env.REMOTEINGRESS_MODE;
88
+
89
+ if (mode === 'hub') {
90
+ const hub = new RemoteIngressHub();
91
+ const config: IHubConfig = {
92
+ tunnelPort: readNumber(readArg(args, 'tunnel-port') ?? process.env.REMOTEINGRESS_TUNNEL_PORT, 8443),
93
+ targetHost: readArg(args, 'target-host') ?? process.env.REMOTEINGRESS_TARGET_HOST ?? '127.0.0.1',
94
+ tls: {
95
+ certPem: readArg(args, 'tls-cert-pem') ?? process.env.REMOTEINGRESS_TLS_CERT_PEM,
96
+ keyPem: readArg(args, 'tls-key-pem') ?? process.env.REMOTEINGRESS_TLS_KEY_PEM,
97
+ },
98
+ performance: readJson(readArg(args, 'performance-json') ?? process.env.REMOTEINGRESS_PERFORMANCE_JSON, undefined),
99
+ };
100
+
101
+ await hub.start(config);
102
+
103
+ const allowedEdges = readJson<TAllowedEdge[]>(
104
+ readArg(args, 'allowed-edges-json') ?? process.env.REMOTEINGRESS_ALLOWED_EDGES_JSON,
105
+ [],
106
+ );
107
+ if (allowedEdges.length > 0) {
108
+ await hub.updateAllowedEdges(allowedEdges);
109
+ }
110
+
111
+ console.log(`RemoteIngress hub listening on ${config.tunnelPort}`);
112
+ await waitForever(() => hub.stop());
113
+ return;
114
+ }
115
+
116
+ if (mode === 'edge') {
117
+ const edge = new RemoteIngressEdge();
118
+ const token = readArg(args, 'token') ?? process.env.REMOTEINGRESS_TOKEN;
119
+
120
+ if (token) {
121
+ await edge.start({ token });
122
+ } else {
123
+ const hubHost = readArg(args, 'hub-host') ?? process.env.REMOTEINGRESS_HUB_HOST;
124
+ const edgeId = readArg(args, 'edge-id') ?? process.env.REMOTEINGRESS_EDGE_ID;
125
+ const secret = readArg(args, 'secret') ?? process.env.REMOTEINGRESS_SECRET;
126
+
127
+ if (!hubHost || !edgeId || !secret) {
128
+ throw new Error('Edge mode requires --token or --hub-host, --edge-id, and --secret');
129
+ }
130
+
131
+ await edge.start({
132
+ hubHost,
133
+ hubPort: readNumber(readArg(args, 'hub-port') ?? process.env.REMOTEINGRESS_HUB_PORT, 8443),
134
+ edgeId,
135
+ secret,
136
+ bindAddress: readArg(args, 'bind-address') ?? process.env.REMOTEINGRESS_BIND_ADDRESS,
137
+ transportMode: readArg(args, 'transport-mode') as any,
138
+ });
139
+ }
140
+
141
+ console.log('RemoteIngress edge started');
142
+ await waitForever(() => edge.stop());
143
+ return;
144
+ }
145
+
146
+ console.log(usage);
147
+ };