@camstack/system 1.2.116 → 1.2.117
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/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 +28 -14
- package/dist/builtins/local-network/local-network.addon.js +195 -25
- package/dist/builtins/local-network/local-network.addon.mjs +191 -26
- 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-BpzbLPel.mjs → dist-D4Vmj8iw.mjs} +16 -8
- package/dist/{dist-Cri60Jd0.js → dist-DMEGlqg6.js} +16 -8
- package/dist/http/lan-http-bind.d.ts +12 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.mjs +3 -3
- 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--aM_vGF9.mjs} +1 -1
- package/dist/{retired-settings-keys-BVAxT5XL.js → retired-settings-keys-DET4YkAU.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-DMEGlqg6.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. */
|
|
@@ -119,6 +229,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
119
229
|
});
|
|
120
230
|
this.ctx.logger.info("local-network: first-boot auto-seed", { meta: { addresses: seed } });
|
|
121
231
|
}
|
|
232
|
+
await this.persistReconciledAllowlist();
|
|
122
233
|
const provider = {
|
|
123
234
|
list: async () => ({
|
|
124
235
|
interfaces: this.enumerate(),
|
|
@@ -213,6 +324,10 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
213
324
|
this.lastSnapshotKey = snapshotKey(this.enumerate());
|
|
214
325
|
this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS);
|
|
215
326
|
this.pollTimer.unref?.();
|
|
327
|
+
this.allowlistTimer = setInterval(() => {
|
|
328
|
+
this.persistReconciledAllowlist();
|
|
329
|
+
}, ALLOWLIST_RECONCILE_INTERVAL_MS);
|
|
330
|
+
this.allowlistTimer.unref?.();
|
|
216
331
|
this.ctx.eventBus?.subscribe({ category: require_dist.EventCategory.NetworkTunnelStarted }, (event) => {
|
|
217
332
|
const data = event.data ?? {};
|
|
218
333
|
if (typeof data.url === "string") try {
|
|
@@ -288,6 +403,10 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
288
403
|
clearInterval(this.pollTimer);
|
|
289
404
|
this.pollTimer = null;
|
|
290
405
|
}
|
|
406
|
+
if (this.allowlistTimer) {
|
|
407
|
+
clearInterval(this.allowlistTimer);
|
|
408
|
+
this.allowlistTimer = null;
|
|
409
|
+
}
|
|
291
410
|
}
|
|
292
411
|
/**
|
|
293
412
|
* Other hub addons (e.g. cloudflare-tunnel) signal the active public
|
|
@@ -421,7 +540,26 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
421
540
|
}
|
|
422
541
|
}
|
|
423
542
|
enumerate() {
|
|
424
|
-
return enumerateOsInterfaces(node_os.networkInterfaces());
|
|
543
|
+
return enumerateOsInterfaces(node_os.networkInterfaces(), readLinuxIpv6Flags());
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Replace rotated IPv6 and drop addresses that no longer exist, without
|
|
547
|
+
* clearing operator extras that are still live (docker/vpn opt-in).
|
|
548
|
+
*/
|
|
549
|
+
async persistReconciledAllowlist(interfaces = this.enumerate()) {
|
|
550
|
+
if (this.allowlistReconcilePending) return;
|
|
551
|
+
this.allowlistReconcilePending = true;
|
|
552
|
+
try {
|
|
553
|
+
const next = reconcileAllowlist(this.config.allowedAddresses, interfaces);
|
|
554
|
+
if (sameAddressSet(this.config.allowedAddresses, next)) return;
|
|
555
|
+
await this.updateGlobalSettings({
|
|
556
|
+
allowedAddresses: next,
|
|
557
|
+
bootSeeded: true
|
|
558
|
+
});
|
|
559
|
+
this.ctx.logger.info("local-network: allowlist reconciled", { meta: { addresses: next } });
|
|
560
|
+
} finally {
|
|
561
|
+
this.allowlistReconcilePending = false;
|
|
562
|
+
}
|
|
425
563
|
}
|
|
426
564
|
detectChanges() {
|
|
427
565
|
const interfaces = this.enumerate();
|
|
@@ -432,6 +570,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
432
570
|
key
|
|
433
571
|
} });
|
|
434
572
|
this.lastSnapshotKey = key;
|
|
573
|
+
this.persistReconciledAllowlist(interfaces);
|
|
435
574
|
this.ctx.eventBus?.emit({
|
|
436
575
|
id: `local-network-changed-${Date.now()}`,
|
|
437
576
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -480,7 +619,7 @@ function validPort(value) {
|
|
|
480
619
|
if (value === null) return null;
|
|
481
620
|
return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
|
|
482
621
|
}
|
|
483
|
-
function enumerateOsInterfaces(ifaces) {
|
|
622
|
+
function enumerateOsInterfaces(ifaces, ipv6Flags) {
|
|
484
623
|
const raw = [];
|
|
485
624
|
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
486
625
|
if (!addrs) continue;
|
|
@@ -500,8 +639,12 @@ function enumerateOsInterfaces(ifaces) {
|
|
|
500
639
|
const classified = raw.map((r) => {
|
|
501
640
|
const kind = classifyKind(r.name, r.address, r.internal);
|
|
502
641
|
const reachableKind = kind === "lan" || kind === "wifi";
|
|
503
|
-
const
|
|
504
|
-
const
|
|
642
|
+
const hex = r.family === "IPv6" ? ipv6ToHex32(r.address) : null;
|
|
643
|
+
const flags = hex !== null ? ipv6Flags?.get(hex) : void 0;
|
|
644
|
+
const rangeOk = isPlausibleAutoSeed(r.address, r.family);
|
|
645
|
+
const stableV6 = r.family !== "IPv6" || shouldAdvertiseIpv6(r.address, flags);
|
|
646
|
+
const plausible = !r.internal && reachableKind && rangeOk && stableV6;
|
|
647
|
+
const plausibleReason = plausible ? "" : r.family === "IPv6" && rangeOk && !stableV6 ? "IPv6 temporary / privacy address — rotates and is not advertised." : explainNonPlausible({
|
|
505
648
|
kind,
|
|
506
649
|
family: r.family,
|
|
507
650
|
address: r.address,
|
|
@@ -530,13 +673,14 @@ function enumerateOsInterfaces(ifaces) {
|
|
|
530
673
|
/**
|
|
531
674
|
* Filter the interface list by the operator's allowlist. Empty
|
|
532
675
|
* allowlist = no-op (every interface passes); otherwise only entries
|
|
533
|
-
* whose `address` matches an allowlist entry remain.
|
|
534
|
-
*
|
|
676
|
+
* whose `address` matches an allowlist entry remain. IPv6 matches the
|
|
677
|
+
* allowlisted /64 so a rotated privacy address does not hide the live
|
|
678
|
+
* GUA. Loopback always survives so the SDK keeps `127.0.0.1` as the
|
|
679
|
+
* last-resort fallback.
|
|
535
680
|
*/
|
|
536
681
|
function applyAllowlist(interfaces, allowed) {
|
|
537
682
|
if (allowed.length === 0) return interfaces;
|
|
538
|
-
|
|
539
|
-
return interfaces.filter((i) => i.kind === "loopback" || set.has(i.address));
|
|
683
|
+
return interfaces.filter((i) => i.kind === "loopback" || ipv6AllowlistMatches(i.address, allowed) || allowed.includes(i.address));
|
|
540
684
|
}
|
|
541
685
|
/**
|
|
542
686
|
* Rank interfaces and pick the auto-selected outbound one. See the
|
|
@@ -595,6 +739,13 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
595
739
|
if (iface.address.startsWith("169.254.")) continue;
|
|
596
740
|
emit(iface, "lan-ipv4", `${formatKind(iface.kind)} — ${iface.name}`, `${lanScheme}://${iface.address}:${lanPort}`, 10 + priority++);
|
|
597
741
|
}
|
|
742
|
+
if (!ipv4Only) for (const iface of interfaces) {
|
|
743
|
+
if (iface.internal || iface.family !== "IPv6") continue;
|
|
744
|
+
if (iface.kind === "loopback") continue;
|
|
745
|
+
if (iface.address.toLowerCase().startsWith("fe80:")) continue;
|
|
746
|
+
if (!iface.plausible) continue;
|
|
747
|
+
emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, 10 + priority++);
|
|
748
|
+
}
|
|
598
749
|
if (publicHostname) out.push({
|
|
599
750
|
label: "Public tunnel",
|
|
600
751
|
baseUrl: `https://${publicHostname}`,
|
|
@@ -613,15 +764,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
613
764
|
plausibleReason: "",
|
|
614
765
|
priority: 150
|
|
615
766
|
});
|
|
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
767
|
if (includeLoopback) out.push({
|
|
626
768
|
label: "Loopback",
|
|
627
769
|
baseUrl: `${scheme}://127.0.0.1:${port}`,
|
|
@@ -639,16 +781,14 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
639
781
|
*
|
|
640
782
|
* - **IPv4**: skip link-local (`169.254.*`), keep the rest.
|
|
641
783
|
* - **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.
|
|
784
|
+
* multicast. Keep ULAs (`fc00::/7`) and Global Unicast (`2000::/3`).
|
|
785
|
+
* Privacy-extension temporary addresses are skipped when Linux flags
|
|
786
|
+
* mark them non-permanent (`plausible === false`).
|
|
647
787
|
*
|
|
648
788
|
* Skips docker/vpn/loopback/other entirely — those stay opt-in.
|
|
649
789
|
*/
|
|
650
790
|
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))];
|
|
791
|
+
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
792
|
}
|
|
653
793
|
/**
|
|
654
794
|
* Per-interface tooltip text surfaced on the "Unlikely usable" badge.
|
|
@@ -737,7 +877,33 @@ function formatKind(kind) {
|
|
|
737
877
|
case "other": return "Other";
|
|
738
878
|
}
|
|
739
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Rewrite a persisted allowlist against the live interface set:
|
|
882
|
+
* current auto-seed (LAN/Wi-Fi, stable IPv6) plus still-live operator
|
|
883
|
+
* extras (docker/vpn). Empty stays empty — that is "unrestricted".
|
|
884
|
+
*/
|
|
885
|
+
function reconcileAllowlist(current, interfaces) {
|
|
886
|
+
if (current.length === 0) return [];
|
|
887
|
+
const live = new Set(interfaces.map((i) => i.address));
|
|
888
|
+
const seed = autoSeedAllowlist(interfaces);
|
|
889
|
+
const seedSet = new Set(seed);
|
|
890
|
+
const extras = current.filter((a) => live.has(a) && !seedSet.has(a) && !a.includes(":"));
|
|
891
|
+
return [...new Set([...seed, ...extras])];
|
|
892
|
+
}
|
|
893
|
+
function sameAddressSet(a, b) {
|
|
894
|
+
if (a.length !== b.length) return false;
|
|
895
|
+
const set = new Set(a);
|
|
896
|
+
return b.every((addr) => set.has(addr));
|
|
897
|
+
}
|
|
898
|
+
function readLinuxIpv6Flags() {
|
|
899
|
+
try {
|
|
900
|
+
return parseProcIfInet6((0, node_fs.readFileSync)("/proc/net/if_inet6", "utf8"));
|
|
901
|
+
} catch {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
740
905
|
//#endregion
|
|
906
|
+
exports.ALLOWLIST_RECONCILE_INTERVAL_MS = ALLOWLIST_RECONCILE_INTERVAL_MS;
|
|
741
907
|
exports.LocalNetworkAddon = LocalNetworkAddon;
|
|
742
908
|
exports.applyAllowlist = applyAllowlist;
|
|
743
909
|
exports.autoSeedAllowlist = autoSeedAllowlist;
|
|
@@ -745,7 +911,11 @@ exports.buildEndpoints = buildEndpoints;
|
|
|
745
911
|
exports.classifyKind = classifyKind;
|
|
746
912
|
exports.enumerateOsInterfaces = enumerateOsInterfaces;
|
|
747
913
|
exports.explainNonPlausible = explainNonPlausible;
|
|
914
|
+
exports.ipv6Prefix64Key = ipv6Prefix64Key;
|
|
748
915
|
exports.isPlausibleAutoSeed = isPlausibleAutoSeed;
|
|
916
|
+
exports.parseProcIfInet6 = parseProcIfInet6;
|
|
749
917
|
exports.pickPreferred = pickPreferred;
|
|
750
918
|
exports.prefixLen = prefixLen;
|
|
919
|
+
exports.reconcileAllowlist = reconcileAllowlist;
|
|
751
920
|
exports.resolveAdvertisedPort = resolveAdvertisedPort;
|
|
921
|
+
exports.shouldAdvertiseIpv6 = shouldAdvertiseIpv6;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Gt as EventCategory, Y as localNetworkCapability, yt as BaseAddon } from "../../dist-
|
|
2
|
-
import {
|
|
1
|
+
import { Gt as EventCategory, Y as localNetworkCapability, yt as BaseAddon } from "../../dist-D4Vmj8iw.mjs";
|
|
2
|
+
import { c as readExtraSans, d as writeExtraSans, f as writeTlsMode, g as reissueTlsLeaf, l as readTlsAccessStatus, m as ensureTlsCert, o as readLanHttpState, p as validateUploadedTls, r as applyLanHttp, t as DEFAULT_LAN_HTTP_PORT, u as readTlsMode } from "../../lan-http-bind-jKrj6OjQ.mjs";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import * as os from "node:os";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
6
7
|
import { isIP } from "node:net";
|
|
7
8
|
//#region src/builtins/local-network/local-hostname.ts
|
|
8
9
|
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])$/;
|
|
@@ -38,6 +39,111 @@ function parseLocalHostname(raw) {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
//#endregion
|
|
42
|
+
//#region src/builtins/local-network/ipv6-lan.ts
|
|
43
|
+
/**
|
|
44
|
+
* IPv6 helpers for LAN advertisement: prefix matching (privacy addresses
|
|
45
|
+
* rotate inside a /64), Linux `/proc/net/if_inet6` flags, and the gate
|
|
46
|
+
* that keeps temporary GUA off the connection candidate list.
|
|
47
|
+
*/
|
|
48
|
+
var IFA_F_TEMPORARY = 1;
|
|
49
|
+
var IFA_F_DEPRECATED = 32;
|
|
50
|
+
var IFA_F_TENTATIVE = 64;
|
|
51
|
+
/** First 64 bits, canonical hex groups, or null if the address is not IPv6. */
|
|
52
|
+
function ipv6Prefix64Key(address) {
|
|
53
|
+
const groups = parseIpv6Groups(address);
|
|
54
|
+
if (groups === null) return null;
|
|
55
|
+
return groups.slice(0, 4).map((g) => g.toString(16).padStart(4, "0")).join(":");
|
|
56
|
+
}
|
|
57
|
+
/** 32-char lowercase hex (same encoding as `/proc/net/if_inet6`). */
|
|
58
|
+
function ipv6ToHex32(address) {
|
|
59
|
+
const groups = parseIpv6Groups(address);
|
|
60
|
+
if (groups === null) return null;
|
|
61
|
+
return groups.map((g) => g.toString(16).padStart(4, "0")).join("");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse Linux `if_inet6` into hex32 → flags. Unknown / malformed lines
|
|
65
|
+
* are skipped. Keys match {@link ipv6ToHex32}.
|
|
66
|
+
*/
|
|
67
|
+
function parseProcIfInet6(text) {
|
|
68
|
+
const out = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const line of text.split("\n")) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (trimmed === "") continue;
|
|
72
|
+
const parts = trimmed.split(/\s+/);
|
|
73
|
+
const hex = parts[0]?.toLowerCase();
|
|
74
|
+
const flagsRaw = parts[4];
|
|
75
|
+
if (!hex || hex.length !== 32 || flagsRaw === void 0) continue;
|
|
76
|
+
const flags = Number.parseInt(flagsRaw, 16);
|
|
77
|
+
if (!Number.isFinite(flags)) continue;
|
|
78
|
+
out.set(hex, flags);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether this IPv6 should be auto-seeded / advertised.
|
|
84
|
+
* `flags === undefined` means the platform did not tell us (macOS, tests
|
|
85
|
+
* without a proc snapshot) — keep every non-link-local plausible address.
|
|
86
|
+
*
|
|
87
|
+
* On Linux, drop addresses marked TEMPORARY / DEPRECATED / TENTATIVE, and
|
|
88
|
+
* `flags === 0` which is how this kernel reports RFC 4941 privacy temps
|
|
89
|
+
* (the stable GUA is PERMANENT|NODAD). Do **not** require PERMANENT —
|
|
90
|
+
* stateful DHCPv6 GUA often has only NODAD (0x02).
|
|
91
|
+
*/
|
|
92
|
+
function shouldAdvertiseIpv6(address, flags) {
|
|
93
|
+
const a = address.toLowerCase().split("%")[0] ?? address;
|
|
94
|
+
if (a === "::" || a === "::1") return false;
|
|
95
|
+
if (a.startsWith("fe80:")) return false;
|
|
96
|
+
if (a.startsWith("ff")) return false;
|
|
97
|
+
if (flags === void 0) return true;
|
|
98
|
+
if ((flags & IFA_F_TEMPORARY) !== 0) return false;
|
|
99
|
+
if ((flags & IFA_F_DEPRECATED) !== 0) return false;
|
|
100
|
+
if ((flags & IFA_F_TENTATIVE) !== 0) return false;
|
|
101
|
+
if (flags === 0) return false;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
function ipv6AllowlistMatches(address, allowed) {
|
|
105
|
+
if (allowed.includes(address)) return true;
|
|
106
|
+
const key = ipv6Prefix64Key(address);
|
|
107
|
+
if (key === null) return false;
|
|
108
|
+
for (const entry of allowed) {
|
|
109
|
+
if (!entry.includes(":")) continue;
|
|
110
|
+
if (ipv6Prefix64Key(entry) === key) return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function parseIpv6Groups(address) {
|
|
115
|
+
const raw = address.toLowerCase().split("%")[0];
|
|
116
|
+
if (!raw || raw.includes(".")) return null;
|
|
117
|
+
const sides = raw.split("::");
|
|
118
|
+
if (sides.length > 2) return null;
|
|
119
|
+
const parsePart = (part) => {
|
|
120
|
+
if (part === void 0 || part === "") return [];
|
|
121
|
+
const groups = part.split(":");
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const g of groups) {
|
|
124
|
+
if (g === "") return null;
|
|
125
|
+
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
126
|
+
out.push(Number.parseInt(g, 16));
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
};
|
|
130
|
+
if (sides.length === 1) {
|
|
131
|
+
const groups = parsePart(sides[0]);
|
|
132
|
+
if (groups === null || groups.length !== 8) return null;
|
|
133
|
+
return groups;
|
|
134
|
+
}
|
|
135
|
+
const left = parsePart(sides[0]);
|
|
136
|
+
const right = parsePart(sides[1]);
|
|
137
|
+
if (left === null || right === null) return null;
|
|
138
|
+
const fill = 8 - left.length - right.length;
|
|
139
|
+
if (fill < 0) return null;
|
|
140
|
+
return [
|
|
141
|
+
...left,
|
|
142
|
+
...Array(fill).fill(0),
|
|
143
|
+
...right
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
41
147
|
//#region src/builtins/local-network/local-network.addon.ts
|
|
42
148
|
/**
|
|
43
149
|
* local-network — hub-only system cap implementation.
|
|
@@ -63,6 +169,8 @@ function parseLocalHostname(raw) {
|
|
|
63
169
|
*/
|
|
64
170
|
var MESH_PROVIDER_ADDON_IDS = ["tailscale-client"];
|
|
65
171
|
var POLL_INTERVAL_MS = 3e4;
|
|
172
|
+
/** Drop rotated IPv6 / dead extras from the persisted allowlist. */
|
|
173
|
+
var ALLOWLIST_RECONCILE_INTERVAL_MS = 3600 * 1e3;
|
|
66
174
|
/**
|
|
67
175
|
* Compiled-in boot default — mirrors `CONFIG_DEFAULTS.server.port` in
|
|
68
176
|
* `server/backend/src/boot/boot-config.ts`. Used only when neither the live
|
|
@@ -71,6 +179,8 @@ var POLL_INTERVAL_MS = 3e4;
|
|
|
71
179
|
var DEFAULT_HUB_PORT = 4443;
|
|
72
180
|
var LocalNetworkAddon = class extends BaseAddon {
|
|
73
181
|
pollTimer = null;
|
|
182
|
+
allowlistTimer = null;
|
|
183
|
+
allowlistReconcilePending = false;
|
|
74
184
|
lastSnapshotKey = "";
|
|
75
185
|
/** Optional public hostname tracked from `NetworkTunnelStarted`/
|
|
76
186
|
* `Stopped` events on the bus. */
|
|
@@ -116,6 +226,7 @@ var LocalNetworkAddon = class extends BaseAddon {
|
|
|
116
226
|
});
|
|
117
227
|
this.ctx.logger.info("local-network: first-boot auto-seed", { meta: { addresses: seed } });
|
|
118
228
|
}
|
|
229
|
+
await this.persistReconciledAllowlist();
|
|
119
230
|
const provider = {
|
|
120
231
|
list: async () => ({
|
|
121
232
|
interfaces: this.enumerate(),
|
|
@@ -210,6 +321,10 @@ var LocalNetworkAddon = class extends BaseAddon {
|
|
|
210
321
|
this.lastSnapshotKey = snapshotKey(this.enumerate());
|
|
211
322
|
this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS);
|
|
212
323
|
this.pollTimer.unref?.();
|
|
324
|
+
this.allowlistTimer = setInterval(() => {
|
|
325
|
+
this.persistReconciledAllowlist();
|
|
326
|
+
}, ALLOWLIST_RECONCILE_INTERVAL_MS);
|
|
327
|
+
this.allowlistTimer.unref?.();
|
|
213
328
|
this.ctx.eventBus?.subscribe({ category: EventCategory.NetworkTunnelStarted }, (event) => {
|
|
214
329
|
const data = event.data ?? {};
|
|
215
330
|
if (typeof data.url === "string") try {
|
|
@@ -285,6 +400,10 @@ var LocalNetworkAddon = class extends BaseAddon {
|
|
|
285
400
|
clearInterval(this.pollTimer);
|
|
286
401
|
this.pollTimer = null;
|
|
287
402
|
}
|
|
403
|
+
if (this.allowlistTimer) {
|
|
404
|
+
clearInterval(this.allowlistTimer);
|
|
405
|
+
this.allowlistTimer = null;
|
|
406
|
+
}
|
|
288
407
|
}
|
|
289
408
|
/**
|
|
290
409
|
* Other hub addons (e.g. cloudflare-tunnel) signal the active public
|
|
@@ -418,7 +537,26 @@ var LocalNetworkAddon = class extends BaseAddon {
|
|
|
418
537
|
}
|
|
419
538
|
}
|
|
420
539
|
enumerate() {
|
|
421
|
-
return enumerateOsInterfaces(os.networkInterfaces());
|
|
540
|
+
return enumerateOsInterfaces(os.networkInterfaces(), readLinuxIpv6Flags());
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Replace rotated IPv6 and drop addresses that no longer exist, without
|
|
544
|
+
* clearing operator extras that are still live (docker/vpn opt-in).
|
|
545
|
+
*/
|
|
546
|
+
async persistReconciledAllowlist(interfaces = this.enumerate()) {
|
|
547
|
+
if (this.allowlistReconcilePending) return;
|
|
548
|
+
this.allowlistReconcilePending = true;
|
|
549
|
+
try {
|
|
550
|
+
const next = reconcileAllowlist(this.config.allowedAddresses, interfaces);
|
|
551
|
+
if (sameAddressSet(this.config.allowedAddresses, next)) return;
|
|
552
|
+
await this.updateGlobalSettings({
|
|
553
|
+
allowedAddresses: next,
|
|
554
|
+
bootSeeded: true
|
|
555
|
+
});
|
|
556
|
+
this.ctx.logger.info("local-network: allowlist reconciled", { meta: { addresses: next } });
|
|
557
|
+
} finally {
|
|
558
|
+
this.allowlistReconcilePending = false;
|
|
559
|
+
}
|
|
422
560
|
}
|
|
423
561
|
detectChanges() {
|
|
424
562
|
const interfaces = this.enumerate();
|
|
@@ -429,6 +567,7 @@ var LocalNetworkAddon = class extends BaseAddon {
|
|
|
429
567
|
key
|
|
430
568
|
} });
|
|
431
569
|
this.lastSnapshotKey = key;
|
|
570
|
+
this.persistReconciledAllowlist(interfaces);
|
|
432
571
|
this.ctx.eventBus?.emit({
|
|
433
572
|
id: `local-network-changed-${Date.now()}`,
|
|
434
573
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -477,7 +616,7 @@ function validPort(value) {
|
|
|
477
616
|
if (value === null) return null;
|
|
478
617
|
return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
|
|
479
618
|
}
|
|
480
|
-
function enumerateOsInterfaces(ifaces) {
|
|
619
|
+
function enumerateOsInterfaces(ifaces, ipv6Flags) {
|
|
481
620
|
const raw = [];
|
|
482
621
|
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
483
622
|
if (!addrs) continue;
|
|
@@ -497,8 +636,12 @@ function enumerateOsInterfaces(ifaces) {
|
|
|
497
636
|
const classified = raw.map((r) => {
|
|
498
637
|
const kind = classifyKind(r.name, r.address, r.internal);
|
|
499
638
|
const reachableKind = kind === "lan" || kind === "wifi";
|
|
500
|
-
const
|
|
501
|
-
const
|
|
639
|
+
const hex = r.family === "IPv6" ? ipv6ToHex32(r.address) : null;
|
|
640
|
+
const flags = hex !== null ? ipv6Flags?.get(hex) : void 0;
|
|
641
|
+
const rangeOk = isPlausibleAutoSeed(r.address, r.family);
|
|
642
|
+
const stableV6 = r.family !== "IPv6" || shouldAdvertiseIpv6(r.address, flags);
|
|
643
|
+
const plausible = !r.internal && reachableKind && rangeOk && stableV6;
|
|
644
|
+
const plausibleReason = plausible ? "" : r.family === "IPv6" && rangeOk && !stableV6 ? "IPv6 temporary / privacy address — rotates and is not advertised." : explainNonPlausible({
|
|
502
645
|
kind,
|
|
503
646
|
family: r.family,
|
|
504
647
|
address: r.address,
|
|
@@ -527,13 +670,14 @@ function enumerateOsInterfaces(ifaces) {
|
|
|
527
670
|
/**
|
|
528
671
|
* Filter the interface list by the operator's allowlist. Empty
|
|
529
672
|
* allowlist = no-op (every interface passes); otherwise only entries
|
|
530
|
-
* whose `address` matches an allowlist entry remain.
|
|
531
|
-
*
|
|
673
|
+
* whose `address` matches an allowlist entry remain. IPv6 matches the
|
|
674
|
+
* allowlisted /64 so a rotated privacy address does not hide the live
|
|
675
|
+
* GUA. Loopback always survives so the SDK keeps `127.0.0.1` as the
|
|
676
|
+
* last-resort fallback.
|
|
532
677
|
*/
|
|
533
678
|
function applyAllowlist(interfaces, allowed) {
|
|
534
679
|
if (allowed.length === 0) return interfaces;
|
|
535
|
-
|
|
536
|
-
return interfaces.filter((i) => i.kind === "loopback" || set.has(i.address));
|
|
680
|
+
return interfaces.filter((i) => i.kind === "loopback" || ipv6AllowlistMatches(i.address, allowed) || allowed.includes(i.address));
|
|
537
681
|
}
|
|
538
682
|
/**
|
|
539
683
|
* Rank interfaces and pick the auto-selected outbound one. See the
|
|
@@ -592,6 +736,13 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
592
736
|
if (iface.address.startsWith("169.254.")) continue;
|
|
593
737
|
emit(iface, "lan-ipv4", `${formatKind(iface.kind)} — ${iface.name}`, `${lanScheme}://${iface.address}:${lanPort}`, 10 + priority++);
|
|
594
738
|
}
|
|
739
|
+
if (!ipv4Only) for (const iface of interfaces) {
|
|
740
|
+
if (iface.internal || iface.family !== "IPv6") continue;
|
|
741
|
+
if (iface.kind === "loopback") continue;
|
|
742
|
+
if (iface.address.toLowerCase().startsWith("fe80:")) continue;
|
|
743
|
+
if (!iface.plausible) continue;
|
|
744
|
+
emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, 10 + priority++);
|
|
745
|
+
}
|
|
595
746
|
if (publicHostname) out.push({
|
|
596
747
|
label: "Public tunnel",
|
|
597
748
|
baseUrl: `https://${publicHostname}`,
|
|
@@ -610,15 +761,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
610
761
|
plausibleReason: "",
|
|
611
762
|
priority: 150
|
|
612
763
|
});
|
|
613
|
-
if (!ipv4Only) {
|
|
614
|
-
let v6prio = 200;
|
|
615
|
-
for (const iface of interfaces) {
|
|
616
|
-
if (iface.internal || iface.family !== "IPv6") continue;
|
|
617
|
-
if (iface.kind === "loopback") continue;
|
|
618
|
-
if (iface.address.startsWith("fe80:")) continue;
|
|
619
|
-
emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, v6prio++);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
764
|
if (includeLoopback) out.push({
|
|
623
765
|
label: "Loopback",
|
|
624
766
|
baseUrl: `${scheme}://127.0.0.1:${port}`,
|
|
@@ -636,16 +778,14 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
|
|
|
636
778
|
*
|
|
637
779
|
* - **IPv4**: skip link-local (`169.254.*`), keep the rest.
|
|
638
780
|
* - **IPv6**: skip link-local (`fe80::*`), unspecified, and
|
|
639
|
-
* multicast. Keep ULAs (`fc00::/7`
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* prune them from the Network Addresses tab if the rotating IPs
|
|
643
|
-
* become a nuisance.
|
|
781
|
+
* multicast. Keep ULAs (`fc00::/7`) and Global Unicast (`2000::/3`).
|
|
782
|
+
* Privacy-extension temporary addresses are skipped when Linux flags
|
|
783
|
+
* mark them non-permanent (`plausible === false`).
|
|
644
784
|
*
|
|
645
785
|
* Skips docker/vpn/loopback/other entirely — those stay opt-in.
|
|
646
786
|
*/
|
|
647
787
|
function autoSeedAllowlist(interfaces) {
|
|
648
|
-
return [...new Set(interfaces.filter((i) => !i.internal && (i.kind === "lan" || i.kind === "wifi") && isPlausibleAutoSeed(i.address, i.family)).map((i) => i.address))];
|
|
788
|
+
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))];
|
|
649
789
|
}
|
|
650
790
|
/**
|
|
651
791
|
* Per-interface tooltip text surfaced on the "Unlikely usable" badge.
|
|
@@ -734,5 +874,30 @@ function formatKind(kind) {
|
|
|
734
874
|
case "other": return "Other";
|
|
735
875
|
}
|
|
736
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* Rewrite a persisted allowlist against the live interface set:
|
|
879
|
+
* current auto-seed (LAN/Wi-Fi, stable IPv6) plus still-live operator
|
|
880
|
+
* extras (docker/vpn). Empty stays empty — that is "unrestricted".
|
|
881
|
+
*/
|
|
882
|
+
function reconcileAllowlist(current, interfaces) {
|
|
883
|
+
if (current.length === 0) return [];
|
|
884
|
+
const live = new Set(interfaces.map((i) => i.address));
|
|
885
|
+
const seed = autoSeedAllowlist(interfaces);
|
|
886
|
+
const seedSet = new Set(seed);
|
|
887
|
+
const extras = current.filter((a) => live.has(a) && !seedSet.has(a) && !a.includes(":"));
|
|
888
|
+
return [...new Set([...seed, ...extras])];
|
|
889
|
+
}
|
|
890
|
+
function sameAddressSet(a, b) {
|
|
891
|
+
if (a.length !== b.length) return false;
|
|
892
|
+
const set = new Set(a);
|
|
893
|
+
return b.every((addr) => set.has(addr));
|
|
894
|
+
}
|
|
895
|
+
function readLinuxIpv6Flags() {
|
|
896
|
+
try {
|
|
897
|
+
return parseProcIfInet6(readFileSync("/proc/net/if_inet6", "utf8"));
|
|
898
|
+
} catch {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
737
902
|
//#endregion
|
|
738
|
-
export { LocalNetworkAddon, applyAllowlist, autoSeedAllowlist, buildEndpoints, classifyKind, enumerateOsInterfaces, explainNonPlausible, isPlausibleAutoSeed, pickPreferred, prefixLen, resolveAdvertisedPort };
|
|
903
|
+
export { ALLOWLIST_RECONCILE_INTERVAL_MS, LocalNetworkAddon, applyAllowlist, autoSeedAllowlist, buildEndpoints, classifyKind, enumerateOsInterfaces, explainNonPlausible, ipv6Prefix64Key, isPlausibleAutoSeed, parseProcIfInet6, pickPreferred, prefixLen, reconcileAllowlist, resolveAdvertisedPort, shouldAdvertiseIpv6 };
|