@camstack/system 1.2.116 → 1.2.118
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/dist/addon-utils.d.ts +1 -1
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/ipv6-lan.d.ts +26 -0
- package/dist/builtins/local-network/local-network.addon.d.ts +50 -14
- package/dist/builtins/local-network/local-network.addon.js +254 -38
- package/dist/builtins/local-network/local-network.addon.mjs +248 -39
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-Cri60Jd0.js → dist-C2_1HCpW.js} +149 -23
- package/dist/{dist-BpzbLPel.mjs → dist-DCTpBXm4.mjs} +149 -23
- package/dist/download/model-downloader.d.ts +18 -1
- package/dist/{file-data-plane-CuE_hBli.mjs → file-data-plane-BhKdxJgf.mjs} +49 -13
- package/dist/{file-data-plane-DUHPHa-Y.js → file-data-plane-DO8KbxCe.js} +49 -13
- package/dist/http/lan-http-bind.d.ts +12 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -3
- package/dist/index.mjs +4 -4
- package/dist/{lan-http-bind-CZ4EBeD_.js → lan-http-bind-DmgpFP6_.js} +53 -11
- package/dist/{lan-http-bind-BDJm4d-K.mjs → lan-http-bind-jKrj6OjQ.mjs} +48 -12
- package/dist/{retired-settings-keys-SrxMH3OT.mjs → retired-settings-keys-Czd76yLZ.mjs} +1 -1
- package/dist/{retired-settings-keys-BVAxT5XL.js → retired-settings-keys-DHRXLMPn.js} +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
4
|
-
const require_lan_http_bind = require("../../lan-http-bind-
|
|
3
|
+
const require_dist = require("../../dist-C2_1HCpW.js");
|
|
4
|
+
const require_lan_http_bind = require("../../lan-http-bind-DmgpFP6_.js");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
let node_path = require("node:path");
|
|
7
7
|
let node_os = require("node:os");
|
|
8
8
|
node_os = require_chunk.__toESM(node_os);
|
|
9
|
+
let node_fs = require("node:fs");
|
|
9
10
|
let node_net = require("node:net");
|
|
10
11
|
//#region src/builtins/local-network/local-hostname.ts
|
|
11
12
|
var DNS_NAME = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])$/;
|
|
@@ -41,6 +42,111 @@ function parseLocalHostname(raw) {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
//#endregion
|
|
45
|
+
//#region src/builtins/local-network/ipv6-lan.ts
|
|
46
|
+
/**
|
|
47
|
+
* IPv6 helpers for LAN advertisement: prefix matching (privacy addresses
|
|
48
|
+
* rotate inside a /64), Linux `/proc/net/if_inet6` flags, and the gate
|
|
49
|
+
* that keeps temporary GUA off the connection candidate list.
|
|
50
|
+
*/
|
|
51
|
+
var IFA_F_TEMPORARY = 1;
|
|
52
|
+
var IFA_F_DEPRECATED = 32;
|
|
53
|
+
var IFA_F_TENTATIVE = 64;
|
|
54
|
+
/** First 64 bits, canonical hex groups, or null if the address is not IPv6. */
|
|
55
|
+
function ipv6Prefix64Key(address) {
|
|
56
|
+
const groups = parseIpv6Groups(address);
|
|
57
|
+
if (groups === null) return null;
|
|
58
|
+
return groups.slice(0, 4).map((g) => g.toString(16).padStart(4, "0")).join(":");
|
|
59
|
+
}
|
|
60
|
+
/** 32-char lowercase hex (same encoding as `/proc/net/if_inet6`). */
|
|
61
|
+
function ipv6ToHex32(address) {
|
|
62
|
+
const groups = parseIpv6Groups(address);
|
|
63
|
+
if (groups === null) return null;
|
|
64
|
+
return groups.map((g) => g.toString(16).padStart(4, "0")).join("");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse Linux `if_inet6` into hex32 → flags. Unknown / malformed lines
|
|
68
|
+
* are skipped. Keys match {@link ipv6ToHex32}.
|
|
69
|
+
*/
|
|
70
|
+
function parseProcIfInet6(text) {
|
|
71
|
+
const out = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const line of text.split("\n")) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (trimmed === "") continue;
|
|
75
|
+
const parts = trimmed.split(/\s+/);
|
|
76
|
+
const hex = parts[0]?.toLowerCase();
|
|
77
|
+
const flagsRaw = parts[4];
|
|
78
|
+
if (!hex || hex.length !== 32 || flagsRaw === void 0) continue;
|
|
79
|
+
const flags = Number.parseInt(flagsRaw, 16);
|
|
80
|
+
if (!Number.isFinite(flags)) continue;
|
|
81
|
+
out.set(hex, flags);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether this IPv6 should be auto-seeded / advertised.
|
|
87
|
+
* `flags === undefined` means the platform did not tell us (macOS, tests
|
|
88
|
+
* without a proc snapshot) — keep every non-link-local plausible address.
|
|
89
|
+
*
|
|
90
|
+
* On Linux, drop addresses marked TEMPORARY / DEPRECATED / TENTATIVE, and
|
|
91
|
+
* `flags === 0` which is how this kernel reports RFC 4941 privacy temps
|
|
92
|
+
* (the stable GUA is PERMANENT|NODAD). Do **not** require PERMANENT —
|
|
93
|
+
* stateful DHCPv6 GUA often has only NODAD (0x02).
|
|
94
|
+
*/
|
|
95
|
+
function shouldAdvertiseIpv6(address, flags) {
|
|
96
|
+
const a = address.toLowerCase().split("%")[0] ?? address;
|
|
97
|
+
if (a === "::" || a === "::1") return false;
|
|
98
|
+
if (a.startsWith("fe80:")) return false;
|
|
99
|
+
if (a.startsWith("ff")) return false;
|
|
100
|
+
if (flags === void 0) return true;
|
|
101
|
+
if ((flags & IFA_F_TEMPORARY) !== 0) return false;
|
|
102
|
+
if ((flags & IFA_F_DEPRECATED) !== 0) return false;
|
|
103
|
+
if ((flags & IFA_F_TENTATIVE) !== 0) return false;
|
|
104
|
+
if (flags === 0) return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function ipv6AllowlistMatches(address, allowed) {
|
|
108
|
+
if (allowed.includes(address)) return true;
|
|
109
|
+
const key = ipv6Prefix64Key(address);
|
|
110
|
+
if (key === null) return false;
|
|
111
|
+
for (const entry of allowed) {
|
|
112
|
+
if (!entry.includes(":")) continue;
|
|
113
|
+
if (ipv6Prefix64Key(entry) === key) return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function parseIpv6Groups(address) {
|
|
118
|
+
const raw = address.toLowerCase().split("%")[0];
|
|
119
|
+
if (!raw || raw.includes(".")) return null;
|
|
120
|
+
const sides = raw.split("::");
|
|
121
|
+
if (sides.length > 2) return null;
|
|
122
|
+
const parsePart = (part) => {
|
|
123
|
+
if (part === void 0 || part === "") return [];
|
|
124
|
+
const groups = part.split(":");
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const g of groups) {
|
|
127
|
+
if (g === "") return null;
|
|
128
|
+
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
129
|
+
out.push(Number.parseInt(g, 16));
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
};
|
|
133
|
+
if (sides.length === 1) {
|
|
134
|
+
const groups = parsePart(sides[0]);
|
|
135
|
+
if (groups === null || groups.length !== 8) return null;
|
|
136
|
+
return groups;
|
|
137
|
+
}
|
|
138
|
+
const left = parsePart(sides[0]);
|
|
139
|
+
const right = parsePart(sides[1]);
|
|
140
|
+
if (left === null || right === null) return null;
|
|
141
|
+
const fill = 8 - left.length - right.length;
|
|
142
|
+
if (fill < 0) return null;
|
|
143
|
+
return [
|
|
144
|
+
...left,
|
|
145
|
+
...Array(fill).fill(0),
|
|
146
|
+
...right
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
44
150
|
//#region src/builtins/local-network/local-network.addon.ts
|
|
45
151
|
/**
|
|
46
152
|
* local-network — hub-only system cap implementation.
|
|
@@ -66,6 +172,8 @@ function parseLocalHostname(raw) {
|
|
|
66
172
|
*/
|
|
67
173
|
var MESH_PROVIDER_ADDON_IDS = ["tailscale-client"];
|
|
68
174
|
var POLL_INTERVAL_MS = 3e4;
|
|
175
|
+
/** Drop rotated IPv6 / dead extras from the persisted allowlist. */
|
|
176
|
+
var ALLOWLIST_RECONCILE_INTERVAL_MS = 3600 * 1e3;
|
|
69
177
|
/**
|
|
70
178
|
* Compiled-in boot default — mirrors `CONFIG_DEFAULTS.server.port` in
|
|
71
179
|
* `server/backend/src/boot/boot-config.ts`. Used only when neither the live
|
|
@@ -74,6 +182,8 @@ var POLL_INTERVAL_MS = 3e4;
|
|
|
74
182
|
var DEFAULT_HUB_PORT = 4443;
|
|
75
183
|
var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
76
184
|
pollTimer = null;
|
|
185
|
+
allowlistTimer = null;
|
|
186
|
+
allowlistReconcilePending = false;
|
|
77
187
|
lastSnapshotKey = "";
|
|
78
188
|
/** Optional public hostname tracked from `NetworkTunnelStarted`/
|
|
79
189
|
* `Stopped` events on the bus. */
|
|
@@ -90,6 +200,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
90
200
|
allowedAddresses: [],
|
|
91
201
|
bootSeeded: false,
|
|
92
202
|
notificationBaseUrl: "",
|
|
203
|
+
viewerBaseUrls: [],
|
|
93
204
|
httpEnabled: true,
|
|
94
205
|
httpPort: require_lan_http_bind.DEFAULT_LAN_HTTP_PORT,
|
|
95
206
|
localHostname: ""
|
|
@@ -110,6 +221,31 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
110
221
|
if (mesh !== null && mesh.host !== "") return `https://${mesh.host}:${mesh.port}`;
|
|
111
222
|
return null;
|
|
112
223
|
}
|
|
224
|
+
async snapshotViewerEndpoints() {
|
|
225
|
+
const selected = selectViewerEndpoints(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.config.viewerBaseUrls);
|
|
226
|
+
return {
|
|
227
|
+
baseUrls: this.config.viewerBaseUrls,
|
|
228
|
+
resolved: selected.map((e) => e.baseUrl)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Palette shown in the Network tab: LAN IPv4, stable LAN IPv6, public,
|
|
233
|
+
* mesh. The viewer HTTP/WS race is a subset (`selectViewerEndpoints`) —
|
|
234
|
+
* IPv6 stays in this list because ICE/WebRTC gathers it regardless.
|
|
235
|
+
*/
|
|
236
|
+
async buildAdvertisedEndpoints(includeLoopback, ipv4Only, scheme, httpsPort) {
|
|
237
|
+
const allow = this.config.allowedAddresses;
|
|
238
|
+
const interfaces = applyAllowlist(this.enumerate(), allow);
|
|
239
|
+
await this.reconcileMeshEndpoint();
|
|
240
|
+
const httpState = require_lan_http_bind.readLanHttpState();
|
|
241
|
+
return buildEndpoints(interfaces, httpsPort, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
|
|
242
|
+
enabled: this.config.httpEnabled !== false,
|
|
243
|
+
listening: httpState.listening,
|
|
244
|
+
httpPort: this.resolvedHttpPort(),
|
|
245
|
+
httpsPort,
|
|
246
|
+
localHostname: this.config.localHostname || null
|
|
247
|
+
});
|
|
248
|
+
}
|
|
113
249
|
async onInitialize() {
|
|
114
250
|
if (!this.config.bootSeeded) {
|
|
115
251
|
const seed = autoSeedAllowlist(this.enumerate());
|
|
@@ -119,6 +255,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
119
255
|
});
|
|
120
256
|
this.ctx.logger.info("local-network: first-boot auto-seed", { meta: { addresses: seed } });
|
|
121
257
|
}
|
|
258
|
+
await this.persistReconciledAllowlist();
|
|
122
259
|
const provider = {
|
|
123
260
|
list: async () => ({
|
|
124
261
|
interfaces: this.enumerate(),
|
|
@@ -126,27 +263,16 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
126
263
|
}),
|
|
127
264
|
getPreferred: async () => pickPreferred(applyAllowlist(this.enumerate(), this.config.allowedAddresses)),
|
|
128
265
|
getConnectionEndpoints: async (input) => {
|
|
129
|
-
const includeLoopback = input.includeLoopback ??
|
|
130
|
-
const ipv4Only = input.ipv4Only ?? false;
|
|
266
|
+
const includeLoopback = input.includeLoopback ?? false;
|
|
131
267
|
const scheme = input.scheme ?? "http";
|
|
132
|
-
const allow = this.config.allowedAddresses;
|
|
133
|
-
const interfaces = applyAllowlist(this.enumerate(), allow);
|
|
134
268
|
const localPort = await this.resolveAdvertisedListenPort(input.port ?? null);
|
|
135
269
|
if (input.port !== void 0 && input.port !== localPort.port) this.ctx.logger.info("local-network: caller port hint overridden", { meta: {
|
|
136
270
|
hint: input.port,
|
|
137
271
|
advertised: localPort.port,
|
|
138
272
|
source: localPort.source
|
|
139
273
|
} });
|
|
140
|
-
await this.reconcileMeshEndpoint();
|
|
141
|
-
const httpState = require_lan_http_bind.readLanHttpState();
|
|
142
274
|
return {
|
|
143
|
-
endpoints:
|
|
144
|
-
enabled: this.config.httpEnabled !== false,
|
|
145
|
-
listening: httpState.listening,
|
|
146
|
-
httpPort: this.resolvedHttpPort(),
|
|
147
|
-
httpsPort: localPort.port,
|
|
148
|
-
localHostname: this.config.localHostname || null
|
|
149
|
-
}),
|
|
275
|
+
endpoints: await this.buildAdvertisedEndpoints(includeLoopback, input.ipv4Only ?? false, scheme, localPort.port),
|
|
150
276
|
localPort
|
|
151
277
|
};
|
|
152
278
|
},
|
|
@@ -162,6 +288,16 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
162
288
|
resolved: this.resolveNotificationEndpoint()
|
|
163
289
|
};
|
|
164
290
|
},
|
|
291
|
+
getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
|
|
292
|
+
setViewerEndpoints: async ({ baseUrls }) => {
|
|
293
|
+
const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
|
|
294
|
+
await this.updateGlobalSettings({ viewerBaseUrls: cleaned });
|
|
295
|
+
this.ctx.logger.info("local-network: viewer endpoints set", { meta: {
|
|
296
|
+
count: cleaned.length,
|
|
297
|
+
auto: cleaned.length === 0
|
|
298
|
+
} });
|
|
299
|
+
return this.snapshotViewerEndpoints();
|
|
300
|
+
},
|
|
165
301
|
getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),
|
|
166
302
|
resetAllowlistToBestMatch: async () => {
|
|
167
303
|
const seed = autoSeedAllowlist(this.enumerate());
|
|
@@ -213,6 +349,10 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
213
349
|
this.lastSnapshotKey = snapshotKey(this.enumerate());
|
|
214
350
|
this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS);
|
|
215
351
|
this.pollTimer.unref?.();
|
|
352
|
+
this.allowlistTimer = setInterval(() => {
|
|
353
|
+
this.persistReconciledAllowlist();
|
|
354
|
+
}, ALLOWLIST_RECONCILE_INTERVAL_MS);
|
|
355
|
+
this.allowlistTimer.unref?.();
|
|
216
356
|
this.ctx.eventBus?.subscribe({ category: require_dist.EventCategory.NetworkTunnelStarted }, (event) => {
|
|
217
357
|
const data = event.data ?? {};
|
|
218
358
|
if (typeof data.url === "string") try {
|
|
@@ -288,6 +428,10 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
288
428
|
clearInterval(this.pollTimer);
|
|
289
429
|
this.pollTimer = null;
|
|
290
430
|
}
|
|
431
|
+
if (this.allowlistTimer) {
|
|
432
|
+
clearInterval(this.allowlistTimer);
|
|
433
|
+
this.allowlistTimer = null;
|
|
434
|
+
}
|
|
291
435
|
}
|
|
292
436
|
/**
|
|
293
437
|
* Other hub addons (e.g. cloudflare-tunnel) signal the active public
|
|
@@ -421,7 +565,26 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
421
565
|
}
|
|
422
566
|
}
|
|
423
567
|
enumerate() {
|
|
424
|
-
return enumerateOsInterfaces(node_os.networkInterfaces());
|
|
568
|
+
return enumerateOsInterfaces(node_os.networkInterfaces(), readLinuxIpv6Flags());
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Replace rotated IPv6 and drop addresses that no longer exist, without
|
|
572
|
+
* clearing operator extras that are still live (docker/vpn opt-in).
|
|
573
|
+
*/
|
|
574
|
+
async persistReconciledAllowlist(interfaces = this.enumerate()) {
|
|
575
|
+
if (this.allowlistReconcilePending) return;
|
|
576
|
+
this.allowlistReconcilePending = true;
|
|
577
|
+
try {
|
|
578
|
+
const next = reconcileAllowlist(this.config.allowedAddresses, interfaces);
|
|
579
|
+
if (sameAddressSet(this.config.allowedAddresses, next)) return;
|
|
580
|
+
await this.updateGlobalSettings({
|
|
581
|
+
allowedAddresses: next,
|
|
582
|
+
bootSeeded: true
|
|
583
|
+
});
|
|
584
|
+
this.ctx.logger.info("local-network: allowlist reconciled", { meta: { addresses: next } });
|
|
585
|
+
} finally {
|
|
586
|
+
this.allowlistReconcilePending = false;
|
|
587
|
+
}
|
|
425
588
|
}
|
|
426
589
|
detectChanges() {
|
|
427
590
|
const interfaces = this.enumerate();
|
|
@@ -432,6 +595,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
432
595
|
key
|
|
433
596
|
} });
|
|
434
597
|
this.lastSnapshotKey = key;
|
|
598
|
+
this.persistReconciledAllowlist(interfaces);
|
|
435
599
|
this.ctx.eventBus?.emit({
|
|
436
600
|
id: `local-network-changed-${Date.now()}`,
|
|
437
601
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -480,7 +644,7 @@ function validPort(value) {
|
|
|
480
644
|
if (value === null) return null;
|
|
481
645
|
return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
|
|
482
646
|
}
|
|
483
|
-
function enumerateOsInterfaces(ifaces) {
|
|
647
|
+
function enumerateOsInterfaces(ifaces, ipv6Flags) {
|
|
484
648
|
const raw = [];
|
|
485
649
|
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
486
650
|
if (!addrs) continue;
|
|
@@ -500,8 +664,12 @@ function enumerateOsInterfaces(ifaces) {
|
|
|
500
664
|
const classified = raw.map((r) => {
|
|
501
665
|
const kind = classifyKind(r.name, r.address, r.internal);
|
|
502
666
|
const reachableKind = kind === "lan" || kind === "wifi";
|
|
503
|
-
const
|
|
504
|
-
const
|
|
667
|
+
const hex = r.family === "IPv6" ? ipv6ToHex32(r.address) : null;
|
|
668
|
+
const flags = hex !== null ? ipv6Flags?.get(hex) : void 0;
|
|
669
|
+
const rangeOk = isPlausibleAutoSeed(r.address, r.family);
|
|
670
|
+
const stableV6 = r.family !== "IPv6" || shouldAdvertiseIpv6(r.address, flags);
|
|
671
|
+
const plausible = !r.internal && reachableKind && rangeOk && stableV6;
|
|
672
|
+
const plausibleReason = plausible ? "" : r.family === "IPv6" && rangeOk && !stableV6 ? "IPv6 temporary / privacy address — rotates and is not advertised." : explainNonPlausible({
|
|
505
673
|
kind,
|
|
506
674
|
family: r.family,
|
|
507
675
|
address: r.address,
|
|
@@ -530,13 +698,14 @@ function enumerateOsInterfaces(ifaces) {
|
|
|
530
698
|
/**
|
|
531
699
|
* Filter the interface list by the operator's allowlist. Empty
|
|
532
700
|
* allowlist = no-op (every interface passes); otherwise only entries
|
|
533
|
-
* whose `address` matches an allowlist entry remain.
|
|
534
|
-
*
|
|
701
|
+
* whose `address` matches an allowlist entry remain. IPv6 matches the
|
|
702
|
+
* allowlisted /64 so a rotated privacy address does not hide the live
|
|
703
|
+
* GUA. Loopback is not an advertised client route and is not forced
|
|
704
|
+
* through.
|
|
535
705
|
*/
|
|
536
706
|
function applyAllowlist(interfaces, allowed) {
|
|
537
707
|
if (allowed.length === 0) return interfaces;
|
|
538
|
-
|
|
539
|
-
return interfaces.filter((i) => i.kind === "loopback" || set.has(i.address));
|
|
708
|
+
return interfaces.filter((i) => ipv6AllowlistMatches(i.address, allowed) || allowed.includes(i.address));
|
|
540
709
|
}
|
|
541
710
|
/**
|
|
542
711
|
* Rank interfaces and pick the auto-selected outbound one. See the
|
|
@@ -595,6 +764,13 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
595
764
|
if (iface.address.startsWith("169.254.")) continue;
|
|
596
765
|
emit(iface, "lan-ipv4", `${formatKind(iface.kind)} — ${iface.name}`, `${lanScheme}://${iface.address}:${lanPort}`, 10 + priority++);
|
|
597
766
|
}
|
|
767
|
+
if (!ipv4Only) for (const iface of interfaces) {
|
|
768
|
+
if (iface.internal || iface.family !== "IPv6") continue;
|
|
769
|
+
if (iface.kind === "loopback") continue;
|
|
770
|
+
if (iface.address.toLowerCase().startsWith("fe80:")) continue;
|
|
771
|
+
if (!iface.plausible) continue;
|
|
772
|
+
emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, 10 + priority++);
|
|
773
|
+
}
|
|
598
774
|
if (publicHostname) out.push({
|
|
599
775
|
label: "Public tunnel",
|
|
600
776
|
baseUrl: `https://${publicHostname}`,
|
|
@@ -613,15 +789,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
613
789
|
plausibleReason: "",
|
|
614
790
|
priority: 150
|
|
615
791
|
});
|
|
616
|
-
if (!ipv4Only) {
|
|
617
|
-
let v6prio = 200;
|
|
618
|
-
for (const iface of interfaces) {
|
|
619
|
-
if (iface.internal || iface.family !== "IPv6") continue;
|
|
620
|
-
if (iface.kind === "loopback") continue;
|
|
621
|
-
if (iface.address.startsWith("fe80:")) continue;
|
|
622
|
-
emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, v6prio++);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
792
|
if (includeLoopback) out.push({
|
|
626
793
|
label: "Loopback",
|
|
627
794
|
baseUrl: `${scheme}://127.0.0.1:${port}`,
|
|
@@ -633,22 +800,39 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
633
800
|
});
|
|
634
801
|
return out.toSorted((a, b) => a.priority - b.priority);
|
|
635
802
|
}
|
|
803
|
+
/** AUTO viewer race: LAN IPv4 + the public tunnel. Never IPv6, mesh, or loopback. */
|
|
804
|
+
function isDefaultViewerEndpoint(endpoint) {
|
|
805
|
+
if (endpoint.kind === "lan-ipv4") return true;
|
|
806
|
+
return endpoint.kind === "public" && endpoint.interfaceKind === "public";
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* The URLs the SDK should race for API / viewer access.
|
|
810
|
+
*
|
|
811
|
+
* Empty `viewerBaseUrls` = AUTO (IPv4 + public tunnel). A non-empty list is
|
|
812
|
+
* the operator's pin; if none of those URLs are in the current palette,
|
|
813
|
+
* fall back to AUTO rather than racing an empty set.
|
|
814
|
+
*/
|
|
815
|
+
function selectViewerEndpoints(endpoints, viewerBaseUrls) {
|
|
816
|
+
const auto = endpoints.filter(isDefaultViewerEndpoint);
|
|
817
|
+
if (viewerBaseUrls.length === 0) return auto;
|
|
818
|
+
const wanted = new Set(viewerBaseUrls);
|
|
819
|
+
const explicit = endpoints.filter((e) => wanted.has(e.baseUrl));
|
|
820
|
+
return explicit.length > 0 ? explicit : auto;
|
|
821
|
+
}
|
|
636
822
|
/**
|
|
637
823
|
* First-boot heuristic: which addresses should the allowlist start
|
|
638
824
|
* with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
|
|
639
825
|
*
|
|
640
826
|
* - **IPv4**: skip link-local (`169.254.*`), keep the rest.
|
|
641
827
|
* - **IPv6**: skip link-local (`fe80::*`), unspecified, and
|
|
642
|
-
* multicast. Keep ULAs (`fc00::/7`
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* prune them from the Network Addresses tab if the rotating IPs
|
|
646
|
-
* become a nuisance.
|
|
828
|
+
* multicast. Keep ULAs (`fc00::/7`) and Global Unicast (`2000::/3`).
|
|
829
|
+
* Privacy-extension temporary addresses are skipped when Linux flags
|
|
830
|
+
* mark them non-permanent (`plausible === false`).
|
|
647
831
|
*
|
|
648
832
|
* Skips docker/vpn/loopback/other entirely — those stay opt-in.
|
|
649
833
|
*/
|
|
650
834
|
function autoSeedAllowlist(interfaces) {
|
|
651
|
-
return [...new Set(interfaces.filter((i) => !i.internal && (i.kind === "lan" || i.kind === "wifi") && isPlausibleAutoSeed(i.address, i.family)).map((i) => i.address))];
|
|
835
|
+
return [...new Set(interfaces.filter((i) => !i.internal && (i.kind === "lan" || i.kind === "wifi") && isPlausibleAutoSeed(i.address, i.family) && (i.family !== "IPv6" || i.plausible)).map((i) => i.address))];
|
|
652
836
|
}
|
|
653
837
|
/**
|
|
654
838
|
* Per-interface tooltip text surfaced on the "Unlikely usable" badge.
|
|
@@ -737,7 +921,33 @@ function formatKind(kind) {
|
|
|
737
921
|
case "other": return "Other";
|
|
738
922
|
}
|
|
739
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Rewrite a persisted allowlist against the live interface set:
|
|
926
|
+
* current auto-seed (LAN/Wi-Fi, stable IPv6) plus still-live operator
|
|
927
|
+
* extras (docker/vpn). Empty stays empty — that is "unrestricted".
|
|
928
|
+
*/
|
|
929
|
+
function reconcileAllowlist(current, interfaces) {
|
|
930
|
+
if (current.length === 0) return [];
|
|
931
|
+
const live = new Set(interfaces.map((i) => i.address));
|
|
932
|
+
const seed = autoSeedAllowlist(interfaces);
|
|
933
|
+
const seedSet = new Set(seed);
|
|
934
|
+
const extras = current.filter((a) => live.has(a) && !seedSet.has(a) && !a.includes(":"));
|
|
935
|
+
return [...new Set([...seed, ...extras])];
|
|
936
|
+
}
|
|
937
|
+
function sameAddressSet(a, b) {
|
|
938
|
+
if (a.length !== b.length) return false;
|
|
939
|
+
const set = new Set(a);
|
|
940
|
+
return b.every((addr) => set.has(addr));
|
|
941
|
+
}
|
|
942
|
+
function readLinuxIpv6Flags() {
|
|
943
|
+
try {
|
|
944
|
+
return parseProcIfInet6((0, node_fs.readFileSync)("/proc/net/if_inet6", "utf8"));
|
|
945
|
+
} catch {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
740
949
|
//#endregion
|
|
950
|
+
exports.ALLOWLIST_RECONCILE_INTERVAL_MS = ALLOWLIST_RECONCILE_INTERVAL_MS;
|
|
741
951
|
exports.LocalNetworkAddon = LocalNetworkAddon;
|
|
742
952
|
exports.applyAllowlist = applyAllowlist;
|
|
743
953
|
exports.autoSeedAllowlist = autoSeedAllowlist;
|
|
@@ -745,7 +955,13 @@ exports.buildEndpoints = buildEndpoints;
|
|
|
745
955
|
exports.classifyKind = classifyKind;
|
|
746
956
|
exports.enumerateOsInterfaces = enumerateOsInterfaces;
|
|
747
957
|
exports.explainNonPlausible = explainNonPlausible;
|
|
958
|
+
exports.ipv6Prefix64Key = ipv6Prefix64Key;
|
|
959
|
+
exports.isDefaultViewerEndpoint = isDefaultViewerEndpoint;
|
|
748
960
|
exports.isPlausibleAutoSeed = isPlausibleAutoSeed;
|
|
961
|
+
exports.parseProcIfInet6 = parseProcIfInet6;
|
|
749
962
|
exports.pickPreferred = pickPreferred;
|
|
750
963
|
exports.prefixLen = prefixLen;
|
|
964
|
+
exports.reconcileAllowlist = reconcileAllowlist;
|
|
751
965
|
exports.resolveAdvertisedPort = resolveAdvertisedPort;
|
|
966
|
+
exports.selectViewerEndpoints = selectViewerEndpoints;
|
|
967
|
+
exports.shouldAdvertiseIpv6 = shouldAdvertiseIpv6;
|