@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.
- package/.smartconfig.json +8 -1
- package/dist_rust/remoteingress-bin_linux_amd64 +0 -0
- package/dist_rust/remoteingress-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.remoteingressedge.d.ts +1 -0
- package/dist_ts/classes.remoteingressedge.js +29 -12
- package/dist_ts/classes.remoteingresshub.d.ts +1 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +118 -1
- package/license +21 -0
- package/package.json +18 -17
- package/readme.md +117 -391
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.remoteingressedge.ts +30 -13
- package/ts/classes.remoteingresshub.ts +1 -1
- package/ts/index.ts +144 -0
|
@@ -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
|
-
|
|
156
|
-
|
|
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
|
+
};
|