@aria-cli/wireguard 1.0.38 → 1.0.39

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/index.js ADDED
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * @aria/wireguard — WireGuard native addon for ARIA secure networking.
4
+ *
5
+ * Wraps Cloudflare's boringtun (BSD-3) via napi-rs for userspace
6
+ * encrypted tunnels. Three hot-path functions: encrypt, decrypt, tick.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.DerpRelay = exports.PeerDiscoveryService = exports.ensureSecureNetwork = exports.decodeInviteToken = exports.createInviteToken = exports.generateSigningKeypair = exports.generateKeyPair = exports.PeerRegistry = exports.NetworkManager = exports.detectNatType = exports.discoverEndpoint = exports.StunClient = exports.ResilientTunnel = exports.SecureTunnel = void 0;
45
+ exports.assertNativeAddonAvailable = assertNativeAddonAvailable;
46
+ exports.createTunnel = createTunnel;
47
+ exports.generateKeypair = generateKeypair;
48
+ const path = __importStar(require("node:path"));
49
+ /** Lazy-loaded native addon */
50
+ let _native = null;
51
+ function loadNative() {
52
+ if (_native)
53
+ return _native;
54
+ try {
55
+ // The package-root napi loader resolves the platform-specific native addon.
56
+ // The dist/ runtime must not depend on a copied dist/wireguard.node shim.
57
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
58
+ _native = require("../index.js");
59
+ return _native;
60
+ }
61
+ catch (err) {
62
+ const reason = err instanceof Error ? err.message : String(err);
63
+ throw new Error(`@aria/wireguard: Failed to load native addon via ${path.join(__dirname, "../index.js")} (${process.platform}-${process.arch}). ${reason}`);
64
+ }
65
+ }
66
+ /** Validate that the native addon can be loaded in the current runtime. */
67
+ function assertNativeAddonAvailable() {
68
+ loadNative();
69
+ }
70
+ /** Create a new WireGuard tunnel */
71
+ function createTunnel(options) {
72
+ const native = loadNative();
73
+ return new native.WireGuardTunnel(options.privateKey, options.peerPublicKey, options.presharedKey ?? null, options.keepalive ?? 0, options.index ?? null);
74
+ }
75
+ /** Generate a new X25519 keypair for WireGuard */
76
+ function generateKeypair() {
77
+ const native = loadNative();
78
+ return native.generateKeypair();
79
+ }
80
+ var tunnel_js_1 = require("./tunnel.js");
81
+ Object.defineProperty(exports, "SecureTunnel", { enumerable: true, get: function () { return tunnel_js_1.SecureTunnel; } });
82
+ var resilient_tunnel_js_1 = require("./resilient-tunnel.js");
83
+ Object.defineProperty(exports, "ResilientTunnel", { enumerable: true, get: function () { return resilient_tunnel_js_1.ResilientTunnel; } });
84
+ var nat_js_1 = require("./nat.js");
85
+ Object.defineProperty(exports, "StunClient", { enumerable: true, get: function () { return nat_js_1.StunClient; } });
86
+ Object.defineProperty(exports, "discoverEndpoint", { enumerable: true, get: function () { return nat_js_1.discoverEndpoint; } });
87
+ Object.defineProperty(exports, "detectNatType", { enumerable: true, get: function () { return nat_js_1.detectNatType; } });
88
+ var network_js_1 = require("./network.js");
89
+ Object.defineProperty(exports, "NetworkManager", { enumerable: true, get: function () { return network_js_1.NetworkManager; } });
90
+ Object.defineProperty(exports, "PeerRegistry", { enumerable: true, get: function () { return network_js_1.PeerRegistry; } });
91
+ Object.defineProperty(exports, "generateKeyPair", { enumerable: true, get: function () { return network_js_1.generateKeyPair; } });
92
+ Object.defineProperty(exports, "generateSigningKeypair", { enumerable: true, get: function () { return network_js_1.generateSigningKeypair; } });
93
+ Object.defineProperty(exports, "createInviteToken", { enumerable: true, get: function () { return network_js_1.createInviteToken; } });
94
+ Object.defineProperty(exports, "decodeInviteToken", { enumerable: true, get: function () { return network_js_1.decodeInviteToken; } });
95
+ Object.defineProperty(exports, "ensureSecureNetwork", { enumerable: true, get: function () { return network_js_1.ensureSecureNetwork; } });
96
+ var peer_discovery_js_1 = require("./peer-discovery.js");
97
+ Object.defineProperty(exports, "PeerDiscoveryService", { enumerable: true, get: function () { return peer_discovery_js_1.PeerDiscoveryService; } });
98
+ var derp_relay_js_1 = require("./derp-relay.js");
99
+ Object.defineProperty(exports, "DerpRelay", { enumerable: true, get: function () { return derp_relay_js_1.DerpRelay; } });
100
+ //# sourceMappingURL=index.js.map
package/dist/nat.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * STUN client + NAT traversal for ARIA secure networking.
3
+ *
4
+ * Discovers external IP:port via STUN (RFC 5389), enabling WireGuard
5
+ * peers to find each other through NAT. Falls back to a UDP relay
6
+ * when symmetric NAT prevents direct connectivity.
7
+ *
8
+ * No external dependencies — implements STUN binding request/response
9
+ * directly using node:dgram.
10
+ */
11
+ import * as dgram from "node:dgram";
12
+ /** Result of STUN endpoint discovery */
13
+ export interface StunResult {
14
+ /** External (public) IP address */
15
+ address: string;
16
+ /** External (public) port */
17
+ port: number;
18
+ /** STUN server used for discovery */
19
+ server: string;
20
+ }
21
+ /**
22
+ * Discover our external endpoint by sending a STUN Binding Request.
23
+ *
24
+ * Tries multiple STUN servers in parallel, returns the first successful response.
25
+ * Timeout: 3 seconds per server, 5 second total.
26
+ */
27
+ /**
28
+ * Discover external endpoint via STUN.
29
+ *
30
+ * @param server STUN server hostname:port (or undefined to try defaults)
31
+ * @param timeoutMs Timeout in milliseconds
32
+ * @param existingSocket Optional: use this socket for STUN instead of creating
33
+ * ephemeral ones. This is critical for the shared-socket WG model — STUN must
34
+ * discover the NAT mapping of the ACTUAL listening socket, not a throwaway one.
35
+ * When provided, the socket is NOT closed after discovery.
36
+ */
37
+ export declare function discoverEndpoint(server?: string, timeoutMs?: number, existingSocket?: dgram.Socket): Promise<StunResult>;
38
+ /**
39
+ * NAT type classification.
40
+ *
41
+ * Detected by comparing mapped ports from two different STUN servers:
42
+ * - Same port from both servers → Full Cone or Restricted Cone (direct tunnel works)
43
+ * - Different port from each server → Symmetric NAT (needs relay)
44
+ */
45
+ export type NatType = "full_cone" | "restricted" | "symmetric" | "unknown";
46
+ /**
47
+ * Detect NAT type by querying two STUN servers and comparing mapped ports.
48
+ *
49
+ * Symmetric NAT allocates a new external port for each destination,
50
+ * so mapped ports will differ across STUN servers. Cone NATs reuse
51
+ * the same external port, so mapped ports will match.
52
+ *
53
+ * Returns "unknown" if fewer than 2 STUN servers respond.
54
+ */
55
+ export declare function detectNatType(servers?: string[], timeoutMs?: number): Promise<{
56
+ natType: NatType;
57
+ results: StunResult[];
58
+ }>;
59
+ /**
60
+ * STUN client for periodic endpoint discovery.
61
+ *
62
+ * Use for ongoing NAT traversal — discovers and tracks endpoint changes.
63
+ */
64
+ export declare class StunClient {
65
+ private servers;
66
+ private pollIntervalMs;
67
+ private interval;
68
+ private lastResult;
69
+ private _natType;
70
+ /** Number of consecutive discovery failures (reset to 0 on success) */
71
+ consecutiveFailures: number;
72
+ constructor(servers?: string[], pollIntervalMs?: number);
73
+ /** Get the detected NAT type (null if not yet detected) */
74
+ getNatType(): NatType | null;
75
+ /** Get the last discovered endpoint */
76
+ getEndpoint(): StunResult | null;
77
+ /** Discover endpoint once — uses first configured server (or defaults) */
78
+ discover(): Promise<StunResult>;
79
+ /** Start periodic endpoint discovery. Detects NAT type on first call. */
80
+ start(onUpdate?: (result: StunResult) => void): void;
81
+ /** Stop periodic discovery */
82
+ stop(): void;
83
+ }
84
+ //# sourceMappingURL=nat.d.ts.map
package/dist/nat.js ADDED
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+ /**
3
+ * STUN client + NAT traversal for ARIA secure networking.
4
+ *
5
+ * Discovers external IP:port via STUN (RFC 5389), enabling WireGuard
6
+ * peers to find each other through NAT. Falls back to a UDP relay
7
+ * when symmetric NAT prevents direct connectivity.
8
+ *
9
+ * No external dependencies — implements STUN binding request/response
10
+ * directly using node:dgram.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.StunClient = void 0;
47
+ exports.discoverEndpoint = discoverEndpoint;
48
+ exports.detectNatType = detectNatType;
49
+ const dgram = __importStar(require("node:dgram"));
50
+ const crypto = __importStar(require("node:crypto"));
51
+ const os = __importStar(require("node:os"));
52
+ // STUN message types (RFC 5389)
53
+ const STUN_BINDING_REQUEST = 0x0001;
54
+ const STUN_BINDING_RESPONSE = 0x0101;
55
+ const STUN_MAGIC_COOKIE = 0x2112a442;
56
+ const STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020;
57
+ const STUN_ATTR_MAPPED_ADDRESS = 0x0001;
58
+ /**
59
+ * Detect whether the route to a STUN server exits through a tunnel/VPN
60
+ * interface. If so, STUN results reflect the tunnel exit IP — not the
61
+ * machine's real public IP — and should be discarded.
62
+ *
63
+ * Detection: uses a connected UDP socket to discover the OS-selected source
64
+ * address, then checks if that address belongs to an interface with a
65
+ * zero MAC address (00:00:00:00:00:00). Tunnel/VPN interfaces (WireGuard,
66
+ * OpenVPN, Tailscale, IPSec, ZeroTier, etc.) have no hardware address.
67
+ * Physical interfaces (Ethernet, WiFi) always have a real MAC.
68
+ */
69
+ function routeExitsThroughTunnel(host, port) {
70
+ return new Promise((resolve) => {
71
+ const probe = dgram.createSocket("udp4");
72
+ const timer = setTimeout(() => {
73
+ probe.close();
74
+ resolve(false);
75
+ }, 2000);
76
+ probe.connect(port, host, () => {
77
+ clearTimeout(timer);
78
+ try {
79
+ const sourceAddr = probe.address().address;
80
+ const ifaces = os.networkInterfaces();
81
+ for (const addrs of Object.values(ifaces)) {
82
+ for (const a of addrs ?? []) {
83
+ if (a.address === sourceAddr && a.mac === "00:00:00:00:00:00") {
84
+ probe.close();
85
+ resolve(true);
86
+ return;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ catch {
92
+ // ignore
93
+ }
94
+ probe.close();
95
+ resolve(false);
96
+ });
97
+ probe.on("error", () => {
98
+ clearTimeout(timer);
99
+ probe.close();
100
+ resolve(false);
101
+ });
102
+ });
103
+ }
104
+ // Well-known public STUN servers (no authentication required)
105
+ const DEFAULT_STUN_SERVERS = [
106
+ "stun.l.google.com:19302",
107
+ "stun1.l.google.com:19302",
108
+ "stun.cloudflare.com:3478",
109
+ ];
110
+ /**
111
+ * Build a STUN Binding Request message (RFC 5389).
112
+ * Header: type (2) + length (2) + magic cookie (4) + transaction ID (12) = 20 bytes.
113
+ */
114
+ function buildBindingRequest() {
115
+ const msg = Buffer.alloc(20);
116
+ msg.writeUInt16BE(STUN_BINDING_REQUEST, 0); // Message type
117
+ msg.writeUInt16BE(0, 2); // Message length (no attributes)
118
+ msg.writeUInt32BE(STUN_MAGIC_COOKIE, 4); // Magic cookie
119
+ const txId = crypto.randomBytes(12);
120
+ txId.copy(msg, 8); // Transaction ID
121
+ return { message: msg, transactionId: txId };
122
+ }
123
+ /**
124
+ * Parse a STUN Binding Response to extract the mapped address.
125
+ * Handles both XOR-MAPPED-ADDRESS (preferred) and MAPPED-ADDRESS (fallback).
126
+ */
127
+ function parseBindingResponse(msg, expectedTxId) {
128
+ if (msg.length < 20)
129
+ return null;
130
+ const msgType = msg.readUInt16BE(0);
131
+ if (msgType !== STUN_BINDING_RESPONSE)
132
+ return null;
133
+ // Verify magic cookie
134
+ if (msg.readUInt32BE(4) !== STUN_MAGIC_COOKIE)
135
+ return null;
136
+ // Verify transaction ID
137
+ if (!msg.subarray(8, 20).equals(expectedTxId))
138
+ return null;
139
+ const attrLength = msg.readUInt16BE(2);
140
+ let offset = 20;
141
+ const end = 20 + attrLength;
142
+ let mappedAddress = null;
143
+ while (offset + 4 <= end) {
144
+ const attrType = msg.readUInt16BE(offset);
145
+ const attrLen = msg.readUInt16BE(offset + 2);
146
+ const attrStart = offset + 4;
147
+ if (attrType === STUN_ATTR_XOR_MAPPED_ADDRESS && attrLen >= 8) {
148
+ const family = msg.readUInt8(attrStart + 1);
149
+ if (family === 0x01) {
150
+ // IPv4
151
+ const xPort = msg.readUInt16BE(attrStart + 2) ^ (STUN_MAGIC_COOKIE >> 16);
152
+ const xAddr = msg.readUInt32BE(attrStart + 4) ^ STUN_MAGIC_COOKIE;
153
+ const a = (xAddr >> 24) & 0xff;
154
+ const b = (xAddr >> 16) & 0xff;
155
+ const c = (xAddr >> 8) & 0xff;
156
+ const d = xAddr & 0xff;
157
+ return { address: `${a}.${b}.${c}.${d}`, port: xPort };
158
+ }
159
+ }
160
+ if (attrType === STUN_ATTR_MAPPED_ADDRESS && attrLen >= 8) {
161
+ const family = msg.readUInt8(attrStart + 1);
162
+ if (family === 0x01) {
163
+ // IPv4 (not XORed)
164
+ const port = msg.readUInt16BE(attrStart + 2);
165
+ const addr = msg.readUInt32BE(attrStart + 4);
166
+ const a = (addr >> 24) & 0xff;
167
+ const b = (addr >> 16) & 0xff;
168
+ const c = (addr >> 8) & 0xff;
169
+ const d = addr & 0xff;
170
+ mappedAddress = { address: `${a}.${b}.${c}.${d}`, port };
171
+ }
172
+ }
173
+ // Attribute value is padded to 4-byte boundary
174
+ offset = attrStart + Math.ceil(attrLen / 4) * 4;
175
+ }
176
+ return mappedAddress;
177
+ }
178
+ /**
179
+ * Discover our external endpoint by sending a STUN Binding Request.
180
+ *
181
+ * Tries multiple STUN servers in parallel, returns the first successful response.
182
+ * Timeout: 3 seconds per server, 5 second total.
183
+ */
184
+ /**
185
+ * Discover external endpoint via STUN.
186
+ *
187
+ * @param server STUN server hostname:port (or undefined to try defaults)
188
+ * @param timeoutMs Timeout in milliseconds
189
+ * @param existingSocket Optional: use this socket for STUN instead of creating
190
+ * ephemeral ones. This is critical for the shared-socket WG model — STUN must
191
+ * discover the NAT mapping of the ACTUAL listening socket, not a throwaway one.
192
+ * When provided, the socket is NOT closed after discovery.
193
+ */
194
+ async function discoverEndpoint(server, timeoutMs = 5000, existingSocket) {
195
+ const servers = server ? [server] : DEFAULT_STUN_SERVERS;
196
+ // Pre-check: if the route to the STUN server exits through a tunnel
197
+ // interface (VPN), the result will be the VPN exit IP — not our real
198
+ // public IP. Reject early to prevent advertising a wrong endpoint.
199
+ const [firstHost, firstPortStr] = servers[0].split(":");
200
+ const isTunnel = await routeExitsThroughTunnel(firstHost, parseInt(firstPortStr ?? "3478", 10));
201
+ if (isTunnel) {
202
+ throw new Error("STUN route exits through a tunnel interface — result would be unreliable");
203
+ }
204
+ return new Promise((resolve, reject) => {
205
+ let resolved = false;
206
+ const ownedSockets = []; // sockets WE created (will be closed)
207
+ const timer = setTimeout(() => {
208
+ if (!resolved) {
209
+ resolved = true;
210
+ ownedSockets.forEach((s) => {
211
+ try {
212
+ s.close();
213
+ }
214
+ catch {
215
+ // ignore
216
+ }
217
+ });
218
+ reject(new Error(`STUN discovery timed out after ${timeoutMs}ms`));
219
+ }
220
+ }, timeoutMs);
221
+ for (const srv of servers) {
222
+ const [host, portStr] = srv.split(":");
223
+ const port = parseInt(portStr ?? "3478", 10);
224
+ // Use existing socket if provided (shared socket model), else create ephemeral
225
+ const socket = existingSocket ?? dgram.createSocket("udp4");
226
+ if (!existingSocket)
227
+ ownedSockets.push(socket);
228
+ const { message, transactionId } = buildBindingRequest();
229
+ const handler = (msg) => {
230
+ if (resolved)
231
+ return;
232
+ const result = parseBindingResponse(msg, transactionId);
233
+ if (result) {
234
+ resolved = true;
235
+ clearTimeout(timer);
236
+ // Remove our handler from the existing socket (don't close it)
237
+ if (existingSocket) {
238
+ existingSocket.off("message", handler);
239
+ }
240
+ ownedSockets.forEach((s) => {
241
+ try {
242
+ s.close();
243
+ }
244
+ catch {
245
+ // ignore
246
+ }
247
+ });
248
+ resolve({ ...result, server: srv });
249
+ }
250
+ };
251
+ socket.on("message", handler);
252
+ socket.on("error", () => {
253
+ // Individual socket errors are non-fatal — other servers may succeed
254
+ if (!existingSocket) {
255
+ try {
256
+ socket.close();
257
+ }
258
+ catch {
259
+ /* already closed */
260
+ }
261
+ }
262
+ });
263
+ socket.send(message, port, host);
264
+ }
265
+ });
266
+ }
267
+ /**
268
+ * Detect NAT type by querying two STUN servers and comparing mapped ports.
269
+ *
270
+ * Symmetric NAT allocates a new external port for each destination,
271
+ * so mapped ports will differ across STUN servers. Cone NATs reuse
272
+ * the same external port, so mapped ports will match.
273
+ *
274
+ * Returns "unknown" if fewer than 2 STUN servers respond.
275
+ */
276
+ async function detectNatType(servers = DEFAULT_STUN_SERVERS, timeoutMs = 5000) {
277
+ const results = [];
278
+ // Query each server individually (sequential to avoid port reuse confusion)
279
+ for (const server of servers.slice(0, 3)) {
280
+ try {
281
+ const result = await discoverEndpoint(server, timeoutMs);
282
+ results.push(result);
283
+ if (results.length >= 2)
284
+ break; // Only need 2 for comparison
285
+ }
286
+ catch {
287
+ // Server unreachable — try next
288
+ }
289
+ }
290
+ if (results.length < 2) {
291
+ return { natType: "unknown", results };
292
+ }
293
+ // Compare mapped ports: if different → symmetric NAT
294
+ const port1 = results[0].port;
295
+ const port2 = results[1].port;
296
+ if (port1 !== port2) {
297
+ return { natType: "symmetric", results };
298
+ }
299
+ // Same port → cone or restricted (both allow direct connections)
300
+ // We can't distinguish full cone from restricted cone with STUN alone
301
+ // (would need a STUN server that sends from a different IP), but both work for us.
302
+ return { natType: "full_cone", results };
303
+ }
304
+ /**
305
+ * STUN client for periodic endpoint discovery.
306
+ *
307
+ * Use for ongoing NAT traversal — discovers and tracks endpoint changes.
308
+ */
309
+ class StunClient {
310
+ servers;
311
+ pollIntervalMs;
312
+ interval = null;
313
+ lastResult = null;
314
+ _natType = null;
315
+ /** Number of consecutive discovery failures (reset to 0 on success) */
316
+ consecutiveFailures = 0;
317
+ constructor(servers = DEFAULT_STUN_SERVERS, pollIntervalMs = 60_000) {
318
+ this.servers = servers;
319
+ this.pollIntervalMs = pollIntervalMs;
320
+ }
321
+ /** Get the detected NAT type (null if not yet detected) */
322
+ getNatType() {
323
+ return this._natType;
324
+ }
325
+ /** Get the last discovered endpoint */
326
+ getEndpoint() {
327
+ return this.lastResult;
328
+ }
329
+ /** Discover endpoint once — uses first configured server (or defaults) */
330
+ async discover() {
331
+ const result = await discoverEndpoint(this.servers[0], 5000);
332
+ this.lastResult = result;
333
+ return result;
334
+ }
335
+ /** Start periodic endpoint discovery. Detects NAT type on first call. */
336
+ start(onUpdate) {
337
+ if (this.interval)
338
+ return;
339
+ // Check once if we're behind a VPN. If so, STUN will always return
340
+ // the wrong IP — skip the entire poll loop instead of spamming errors.
341
+ void (async () => {
342
+ const [firstHost, firstPortStr] = this.servers[0].split(":");
343
+ const isTunnel = await routeExitsThroughTunnel(firstHost, parseInt(firstPortStr ?? "3478", 10));
344
+ if (isTunnel) {
345
+ if (typeof process !== "undefined" && process.stderr) {
346
+ process.stderr.write("[wireguard] STUN skipped — route exits through VPN/tunnel interface\n");
347
+ }
348
+ return; // Don't start the poll loop
349
+ }
350
+ // Initial discovery + NAT type detection
351
+ try {
352
+ if (!this._natType) {
353
+ const detection = await detectNatType(this.servers);
354
+ this._natType = detection.natType;
355
+ if (detection.results.length > 0) {
356
+ this.lastResult = detection.results[0];
357
+ this.consecutiveFailures = 0;
358
+ onUpdate?.(detection.results[0]);
359
+ }
360
+ }
361
+ if (!this.lastResult) {
362
+ const r = await this.discover();
363
+ this.consecutiveFailures = 0;
364
+ onUpdate?.(r);
365
+ }
366
+ }
367
+ catch {
368
+ this.consecutiveFailures++;
369
+ }
370
+ // Start periodic refresh only after confirming STUN works
371
+ this.interval = setInterval(async () => {
372
+ try {
373
+ const result = await this.discover();
374
+ this.consecutiveFailures = 0;
375
+ onUpdate?.(result);
376
+ }
377
+ catch {
378
+ this.consecutiveFailures++;
379
+ if (this.consecutiveFailures >= 3) {
380
+ if (typeof process !== "undefined" && process.stderr) {
381
+ process.stderr.write(`[wireguard] STUN discovery failed ${this.consecutiveFailures} consecutive times\n`);
382
+ }
383
+ }
384
+ }
385
+ }, this.pollIntervalMs);
386
+ })();
387
+ }
388
+ /** Stop periodic discovery */
389
+ stop() {
390
+ if (this.interval) {
391
+ clearInterval(this.interval);
392
+ this.interval = null;
393
+ }
394
+ }
395
+ }
396
+ exports.StunClient = StunClient;
397
+ //# sourceMappingURL=nat.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * NetworkStateStore — canonical, machine-scoped persistence for network control-plane state.
3
+ *
4
+ * All network-adjacent durable state (peers, signing keys, revocations, nonces, trust)
5
+ * lives in ONE canonical store at ARIA_HOME/network/state.db, independent of any
6
+ * arion-specific Memoria DB. This eliminates the split-brain where peer state could
7
+ * drift across multiple DB paths.
8
+ *
9
+ * PeerRegistry, RevocationStore, and other network stores all use the Database
10
+ * returned by getDatabase().
11
+ */
12
+ import type Database from "better-sqlite3";
13
+ export interface NetworkStateStoreOptions {
14
+ /** Base ARIA home directory (e.g., ~/.aria) */
15
+ ariaHome: string;
16
+ /** Override DB path (for testing) */
17
+ dbPath?: string;
18
+ }
19
+ export declare class NetworkStateStore {
20
+ private readonly options;
21
+ private db;
22
+ private readonly dbPath;
23
+ constructor(options: NetworkStateStoreOptions);
24
+ /** Canonical path to the network state DB */
25
+ get path(): string;
26
+ /** Whether the store has been opened */
27
+ get isOpen(): boolean;
28
+ /** Open the database, creating schema if needed */
29
+ open(): Database.Database;
30
+ private reconcilePeerTableSchema;
31
+ /** Get the underlying Database handle (opens if needed) */
32
+ getDatabase(): Database.Database;
33
+ /**
34
+ * Claim the owner epoch for this runtime on the shared network state DB.
35
+ * Must be called after open() and before any durable writes.
36
+ * Throws StaleOwnerError if a newer generation already owns the DB.
37
+ */
38
+ claimOwnerEpoch(generation: number): void;
39
+ /** Close the database */
40
+ close(): void;
41
+ }
42
+ /**
43
+ * Resolve the canonical network state DB path for a given ARIA home.
44
+ */
45
+ export declare function canonicalNetworkStatePath(ariaHome: string): string;
46
+ //# sourceMappingURL=network-state-store.d.ts.map