@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/bootstrap-authority.d.ts +2 -0
- package/dist/bootstrap-authority.js +47 -0
- package/dist/bootstrap-tls.d.ts +14 -0
- package/dist/bootstrap-tls.js +69 -0
- package/dist/db-owner-fencing.d.ts +10 -0
- package/dist/db-owner-fencing.js +44 -0
- package/dist/derp-relay.d.ts +75 -0
- package/dist/derp-relay.js +311 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +100 -0
- package/dist/nat.d.ts +84 -0
- package/dist/nat.js +397 -0
- package/dist/network-state-store.d.ts +46 -0
- package/dist/network-state-store.js +248 -0
- package/dist/network.d.ts +590 -0
- package/dist/network.js +3391 -0
- package/dist/peer-discovery.d.ts +133 -0
- package/dist/peer-discovery.js +486 -0
- package/dist/resilient-tunnel.d.ts +70 -0
- package/dist/resilient-tunnel.js +389 -0
- package/dist/route-ownership.d.ts +23 -0
- package/dist/route-ownership.js +79 -0
- package/dist/tunnel.d.ts +141 -0
- package/dist/tunnel.js +474 -0
- package/index.js +52 -52
- package/npm/darwin-arm64/package.json +1 -1
- package/npm/darwin-x64/package.json +1 -1
- package/npm/linux-arm64-gnu/package.json +1 -1
- package/npm/linux-x64-gnu/package.json +1 -1
- package/npm/win32-x64-msvc/package.json +1 -1
- package/package.json +13 -20
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
|