@aria-cli/wireguard 1.0.38 → 1.0.40

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.
@@ -0,0 +1,389 @@
1
+ "use strict";
2
+ /**
3
+ * ResilientTunnel — wraps SecureTunnel with dead peer detection, auto-reconnection,
4
+ * and outbound message queuing.
5
+ *
6
+ * State machine:
7
+ * CONNECTING → HANDSHAKING → CONNECTED → DISCONNECTED → RECONNECTING → HANDSHAKING → CONNECTED
8
+ * ↓
9
+ * DEAD (max retries exhausted)
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ResilientTunnel = void 0;
13
+ const node_events_1 = require("node:events");
14
+ const tunnel_js_1 = require("./tunnel.js");
15
+ class ResilientTunnel extends node_events_1.EventEmitter {
16
+ options;
17
+ tunnel = null;
18
+ _state = "connecting";
19
+ queue = [];
20
+ queueBytes = 0;
21
+ reconnectAttempts = 0;
22
+ reconnectTimer = null;
23
+ deadPeerTimer = null;
24
+ lastPacketAt = 0;
25
+ reconnections = 0;
26
+ awaitingReplayReadiness = false;
27
+ stopped = false;
28
+ static MAX_QUEUE_SIZE = 1000;
29
+ static MAX_QUEUE_BYTES = 1_048_576; // 1MB
30
+ static MAX_RECONNECT_ATTEMPTS = 10;
31
+ // Safety-net timeout: fires only if neither peerActivity events nor the
32
+ // WG state machine's own REJECT_AFTER_TIME (180s) detect the dead peer.
33
+ // Must be longer than REJECT_AFTER_TIME to avoid premature disconnection
34
+ // when keepalives can't reach us (e.g. stale endpoint from STUN/WAN IP).
35
+ static DEAD_PEER_TIMEOUT_MS = 300_000; // 5 minutes
36
+ static MAX_BACKOFF_MS = 60_000;
37
+ static BASE_BACKOFF_MS = 1_000;
38
+ constructor(options) {
39
+ super();
40
+ this.options = options;
41
+ }
42
+ /** Start the resilient tunnel — creates underlying SecureTunnel and waits for remote proof before CONNECTED */
43
+ async start() {
44
+ if (this.stopped)
45
+ throw new Error("Tunnel has been stopped");
46
+ this.setState("connecting");
47
+ const port = await this.createAndStartTunnel();
48
+ if (this._state === "connecting") {
49
+ this.setState("handshaking");
50
+ this.requestSessionProof();
51
+ }
52
+ return port;
53
+ }
54
+ /** Send plaintext through the tunnel. Queues if not yet connected or reconnecting, throws if dead. */
55
+ sendPlaintext(data) {
56
+ if (this._state === "dead") {
57
+ throw new Error("Tunnel is dead — max reconnection attempts exhausted");
58
+ }
59
+ if (this._state === "connecting" ||
60
+ this._state === "handshaking" ||
61
+ this._state === "reconnecting" ||
62
+ this._state === "disconnected") {
63
+ this.enqueue(data);
64
+ // Don't call requestSessionProof() here — the initial handshake was
65
+ // already triggered by start(). Re-probing while a handshake is in
66
+ // progress risks calling encrypt() after the native tunnel processed
67
+ // a peer's init (state=None), which starts a NEW handshake that
68
+ // overwrites the pending initiator state in Handshake::previous.
69
+ // boringtun internally queues data sent via encrypt() during handshake.
70
+ return;
71
+ }
72
+ if (this._state !== "connected" || !this.tunnel) {
73
+ throw new Error(`Cannot send in state: ${this._state}`);
74
+ }
75
+ const activeTunnel = this.tunnel;
76
+ try {
77
+ activeTunnel.sendPlaintext(data);
78
+ }
79
+ catch (error) {
80
+ if (!this.stopped) {
81
+ this.enqueue(data);
82
+ }
83
+ throw error;
84
+ }
85
+ // If the send synchronously triggered a disconnect, preserve the payload
86
+ // for replay on the next reconnect instead of dropping the in-flight frame.
87
+ if (!this.stopped && this._state !== "connected") {
88
+ this.enqueue(data);
89
+ return;
90
+ }
91
+ if (!this.stopped && this.tunnel !== activeTunnel) {
92
+ this.enqueue(data);
93
+ }
94
+ }
95
+ /** Stop the tunnel permanently */
96
+ stop() {
97
+ this.stopped = true;
98
+ this.clearTimers();
99
+ if (this.tunnel) {
100
+ this.tunnel.removeAllListeners();
101
+ this.tunnel.stop();
102
+ this.tunnel = null;
103
+ }
104
+ this.queue = [];
105
+ this.queueBytes = 0;
106
+ }
107
+ /** Whether the tunnel is actively connected */
108
+ get isActive() {
109
+ return this._state === "connected";
110
+ }
111
+ /** Current tunnel state */
112
+ getState() {
113
+ return this._state;
114
+ }
115
+ /** Stats including reconnection count and queue depth */
116
+ getStats() {
117
+ return {
118
+ state: this._state,
119
+ reconnections: this.reconnections,
120
+ reconnectAttempts: this.reconnectAttempts,
121
+ queueDepth: this.queue.length,
122
+ queueBytes: this.queueBytes,
123
+ };
124
+ }
125
+ /** Get the underlying SecureTunnel (for event wiring, e.g. "plaintext") */
126
+ getInnerTunnel() {
127
+ return this.tunnel;
128
+ }
129
+ // ── Internal ─────────────────────────────────────────────────────
130
+ setState(newState) {
131
+ const prev = this._state;
132
+ if (prev === newState)
133
+ return;
134
+ this._state = newState;
135
+ this.emit("stateChange", newState, prev);
136
+ }
137
+ requestSessionProof() {
138
+ if (this.stopped || !this.tunnel) {
139
+ return;
140
+ }
141
+ if (this._state !== "handshaking" &&
142
+ !(this._state === "reconnecting" && this.awaitingReplayReadiness)) {
143
+ return;
144
+ }
145
+ try {
146
+ // Empty payload is a no-op at the application layer but still forces
147
+ // WireGuard to emit handshake traffic. This primes the route without
148
+ // falsely promoting the tunnel to CONNECTED.
149
+ this.tunnel.sendPlaintext(Buffer.alloc(0));
150
+ }
151
+ catch {
152
+ // Best-effort only: later outbound queueing or reconnect backoff can retry.
153
+ }
154
+ }
155
+ async createAndStartTunnel() {
156
+ if (this.tunnel) {
157
+ this.tunnel.removeAllListeners();
158
+ this.tunnel.stop();
159
+ this.tunnel = null;
160
+ }
161
+ const tunnel = new tunnel_js_1.SecureTunnel(this.options);
162
+ this.tunnel = tunnel;
163
+ // Wire events from inner tunnel
164
+ tunnel.on("plaintext", (data) => {
165
+ this.lastPacketAt = Date.now();
166
+ this.resetDeadPeerTimer();
167
+ this.promoteReconnectReady();
168
+ this.emit("plaintext", data);
169
+ });
170
+ tunnel.on("handshake", () => {
171
+ this.lastPacketAt = Date.now();
172
+ this.resetDeadPeerTimer();
173
+ this.promoteInitialReady();
174
+ this.promoteReconnectReady();
175
+ this.emit("handshake");
176
+ });
177
+ // WG keepalive responses produce write_to_tunnel with empty data.
178
+ // Without this, the dead-peer timer fires after 75s of no real
179
+ // plaintext, even though the peer is alive and exchanging keepalives.
180
+ tunnel.on("peerActivity", () => {
181
+ this.lastPacketAt = Date.now();
182
+ this.resetDeadPeerTimer();
183
+ });
184
+ tunnel.on("error", (err) => {
185
+ this.emit("error", err);
186
+ if (!this.stopped &&
187
+ (this._state === "handshaking" ||
188
+ this._state === "connected" ||
189
+ this._state === "reconnecting")) {
190
+ this.handleDisconnect();
191
+ }
192
+ });
193
+ tunnel.on("close", () => {
194
+ if (!this.stopped &&
195
+ (this._state === "handshaking" ||
196
+ this._state === "connected" ||
197
+ this._state === "reconnecting")) {
198
+ this.handleDisconnect();
199
+ }
200
+ });
201
+ try {
202
+ const port = await tunnel.start();
203
+ return port;
204
+ }
205
+ catch (err) {
206
+ // CRITICAL: Clean up listeners on failure to prevent accumulation
207
+ // after N failed reconnections (N×4 orphaned listeners → OOM)
208
+ tunnel.removeAllListeners();
209
+ tunnel.stop();
210
+ this.tunnel = null;
211
+ throw err;
212
+ }
213
+ }
214
+ disposeCurrentTunnel() {
215
+ if (!this.tunnel) {
216
+ return;
217
+ }
218
+ this.tunnel.removeAllListeners();
219
+ this.tunnel.stop();
220
+ this.tunnel = null;
221
+ }
222
+ handleDisconnect() {
223
+ this.awaitingReplayReadiness = false;
224
+ this.clearDeadPeerTimer();
225
+ this.disposeCurrentTunnel();
226
+ this.setState("disconnected");
227
+ this.attemptReconnect();
228
+ }
229
+ promoteInitialReady() {
230
+ if (this.stopped)
231
+ return;
232
+ if (this._state !== "connecting" && this._state !== "handshaking") {
233
+ return;
234
+ }
235
+ this.setState("connected");
236
+ this.resetDeadPeerTimer();
237
+ this.flushQueue();
238
+ }
239
+ promoteReconnectReady() {
240
+ if (!this.awaitingReplayReadiness || this.stopped) {
241
+ return;
242
+ }
243
+ this.finalizeReconnect();
244
+ }
245
+ finalizeReconnect() {
246
+ this.awaitingReplayReadiness = false;
247
+ this.reconnectAttempts = 0;
248
+ this.reconnections++;
249
+ this.setState("connected");
250
+ this.resetDeadPeerTimer();
251
+ this.flushQueue();
252
+ this.emit("reconnected");
253
+ }
254
+ attemptReconnect() {
255
+ if (this.stopped)
256
+ return;
257
+ if (this.reconnectTimer)
258
+ return;
259
+ if (this.reconnectAttempts >= ResilientTunnel.MAX_RECONNECT_ATTEMPTS) {
260
+ this.setState("dead");
261
+ this.emit("dead");
262
+ return;
263
+ }
264
+ this.setState("reconnecting");
265
+ const backoff = Math.min(ResilientTunnel.BASE_BACKOFF_MS * Math.pow(2, this.reconnectAttempts), ResilientTunnel.MAX_BACKOFF_MS);
266
+ this.reconnectAttempts++;
267
+ this.reconnectTimer = setTimeout(async () => {
268
+ this.reconnectTimer = null;
269
+ if (this.stopped)
270
+ return;
271
+ try {
272
+ this.awaitingReplayReadiness = true;
273
+ await this.createAndStartTunnel();
274
+ // NOTE: Do NOT reset reconnectAttempts here. createAndStartTunnel()
275
+ // succeeding only means the UDP socket bound — the handshake has NOT
276
+ // completed yet. Reset happens in finalizeReconnect() when the peer
277
+ // proves liveness. Without this, an unreachable peer causes an infinite
278
+ // reconnect loop: start succeeds → error fires → counter resets → repeat.
279
+ if (this._state === "connected") {
280
+ return;
281
+ }
282
+ this.requestSessionProof();
283
+ }
284
+ catch {
285
+ this.awaitingReplayReadiness = false;
286
+ // Reconnect failed — try again
287
+ this.attemptReconnect();
288
+ }
289
+ }, backoff);
290
+ }
291
+ resetDeadPeerTimer() {
292
+ this.clearDeadPeerTimer();
293
+ if (this.stopped || this._state !== "connected")
294
+ return;
295
+ this.deadPeerTimer = setTimeout(() => {
296
+ if (this._state === "connected" && !this.stopped) {
297
+ this.handleDisconnect();
298
+ }
299
+ }, ResilientTunnel.DEAD_PEER_TIMEOUT_MS);
300
+ }
301
+ clearDeadPeerTimer() {
302
+ if (this.deadPeerTimer) {
303
+ clearTimeout(this.deadPeerTimer);
304
+ this.deadPeerTimer = null;
305
+ }
306
+ }
307
+ clearTimers() {
308
+ this.clearDeadPeerTimer();
309
+ if (this.reconnectTimer) {
310
+ clearTimeout(this.reconnectTimer);
311
+ this.reconnectTimer = null;
312
+ }
313
+ }
314
+ enqueue(data) {
315
+ // Drop expired messages first
316
+ this.purgeExpired();
317
+ // Check capacity
318
+ if (this.queue.length >= ResilientTunnel.MAX_QUEUE_SIZE) {
319
+ process.stderr.write(`[ResilientTunnel] dropping message, queue overflow (maxSize: ${this.queue.length}/${ResilientTunnel.MAX_QUEUE_SIZE})\n`);
320
+ this.emit("queueOverflow", { reason: "maxSize", dropped: 1 });
321
+ return;
322
+ }
323
+ if (this.queueBytes + data.length > ResilientTunnel.MAX_QUEUE_BYTES) {
324
+ process.stderr.write(`[ResilientTunnel] dropping message, queue overflow (maxBytes: ${this.queueBytes + data.length}/${ResilientTunnel.MAX_QUEUE_BYTES})\n`);
325
+ this.emit("queueOverflow", { reason: "maxBytes", dropped: 1 });
326
+ return;
327
+ }
328
+ this.queue.push({ data, enqueuedAt: Date.now(), sendAttempts: 0 });
329
+ this.queueBytes += data.length;
330
+ }
331
+ purgeExpired() {
332
+ const now = Date.now();
333
+ const before = this.queue.length;
334
+ this.queue = this.queue.filter((msg) => {
335
+ if (msg.ttl !== undefined && now - msg.enqueuedAt > msg.ttl) {
336
+ this.queueBytes -= msg.data.length;
337
+ return false;
338
+ }
339
+ return true;
340
+ });
341
+ const expired = before - this.queue.length;
342
+ if (expired > 0) {
343
+ this.emit("messagesExpired", { count: expired });
344
+ }
345
+ }
346
+ flushQueue() {
347
+ if (!this.tunnel || this._state !== "connected")
348
+ return;
349
+ // Purge expired before flushing
350
+ this.purgeExpired();
351
+ const toSend = this.queue.splice(0);
352
+ this.queueBytes = 0;
353
+ const MAX_SEND_ATTEMPTS = 3;
354
+ const requeueFrom = (startIndex, currentMessage) => {
355
+ const replayQueue = [];
356
+ if (currentMessage) {
357
+ currentMessage.sendAttempts++;
358
+ if (currentMessage.sendAttempts < MAX_SEND_ATTEMPTS) {
359
+ replayQueue.push(currentMessage);
360
+ }
361
+ }
362
+ replayQueue.push(...toSend.slice(startIndex + (currentMessage ? 1 : 0)));
363
+ if (replayQueue.length > 0) {
364
+ this.queue.unshift(...replayQueue);
365
+ this.queueBytes = replayQueue.reduce((sum, message) => sum + message.data.length, this.queueBytes);
366
+ this.emit("queueFlushPartialFailure", {
367
+ failed: replayQueue.length,
368
+ total: toSend.length,
369
+ });
370
+ }
371
+ };
372
+ for (let index = 0; index < toSend.length; index += 1) {
373
+ const msg = toSend[index];
374
+ try {
375
+ this.tunnel.sendPlaintext(msg.data);
376
+ }
377
+ catch {
378
+ requeueFrom(index, msg);
379
+ return;
380
+ }
381
+ if (this._state !== "connected") {
382
+ requeueFrom(index, msg);
383
+ return;
384
+ }
385
+ }
386
+ }
387
+ }
388
+ exports.ResilientTunnel = ResilientTunnel;
389
+ //# sourceMappingURL=resilient-tunnel.js.map
@@ -0,0 +1,23 @@
1
+ export type DirectRouteClaimStatus = "active" | "pending" | "pending_tunnel" | "pending_verification" | "revoked";
2
+ export interface DirectRouteClaim {
3
+ publicKey: string;
4
+ nodeId?: string | null;
5
+ status: DirectRouteClaimStatus;
6
+ endpointHost?: string | null;
7
+ endpointPort?: number | null;
8
+ endpointRevision?: number | null;
9
+ createdAt: number;
10
+ updatedAt?: number | null;
11
+ }
12
+ export interface DirectRouteOwnershipDecision {
13
+ routeKey: string | null;
14
+ ownership: "current" | "superseded";
15
+ ownerPublicKey: string;
16
+ supersededByPublicKey?: string;
17
+ }
18
+ export declare function getDirectRouteKey(input: {
19
+ endpointHost?: string | null;
20
+ endpointPort?: number | null;
21
+ }): string | null;
22
+ export declare function resolveDirectRouteOwnership(claims: readonly DirectRouteClaim[]): Map<string, DirectRouteOwnershipDecision>;
23
+ //# sourceMappingURL=route-ownership.d.ts.map
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDirectRouteKey = getDirectRouteKey;
4
+ exports.resolveDirectRouteOwnership = resolveDirectRouteOwnership;
5
+ const network_runtime_1 = require("@aria-cli/tools/network-runtime");
6
+ const STATUS_PRECEDENCE = {
7
+ pending_verification: 5,
8
+ pending_tunnel: 4,
9
+ active: 3,
10
+ pending: 2,
11
+ revoked: 1,
12
+ };
13
+ function getDirectRouteKey(input) {
14
+ const { endpointHost: host, endpointPort: port } = (0, network_runtime_1.canonicalizeAuthoritativeDirectEndpoint)(input);
15
+ if (!host || typeof port !== "number" || !Number.isFinite(port)) {
16
+ return null;
17
+ }
18
+ return `${host}:${port}`;
19
+ }
20
+ function compareRouteClaims(a, b) {
21
+ const precedenceDelta = STATUS_PRECEDENCE[b.status] - STATUS_PRECEDENCE[a.status];
22
+ if (precedenceDelta !== 0) {
23
+ return precedenceDelta;
24
+ }
25
+ const aEndpointRevision = a.endpointRevision ?? 0;
26
+ const bEndpointRevision = b.endpointRevision ?? 0;
27
+ if (aEndpointRevision !== bEndpointRevision) {
28
+ return bEndpointRevision - aEndpointRevision;
29
+ }
30
+ const aUpdatedAt = a.updatedAt ?? a.createdAt;
31
+ const bUpdatedAt = b.updatedAt ?? b.createdAt;
32
+ if (aUpdatedAt !== bUpdatedAt) {
33
+ return bUpdatedAt - aUpdatedAt;
34
+ }
35
+ if (a.createdAt !== b.createdAt) {
36
+ return b.createdAt - a.createdAt;
37
+ }
38
+ return a.publicKey.localeCompare(b.publicKey);
39
+ }
40
+ function resolveDirectRouteOwnership(claims) {
41
+ const decisions = new Map();
42
+ const byRouteKey = new Map();
43
+ for (const claim of claims) {
44
+ const routeKey = getDirectRouteKey(claim);
45
+ if (!routeKey) {
46
+ decisions.set(claim.publicKey, {
47
+ routeKey: null,
48
+ ownership: "current",
49
+ ownerPublicKey: claim.publicKey,
50
+ });
51
+ continue;
52
+ }
53
+ const bucket = byRouteKey.get(routeKey) ?? [];
54
+ bucket.push(claim);
55
+ byRouteKey.set(routeKey, bucket);
56
+ }
57
+ for (const [routeKey, bucket] of byRouteKey) {
58
+ const sorted = [...bucket].sort(compareRouteClaims);
59
+ const owner = sorted[0];
60
+ if (!owner) {
61
+ continue;
62
+ }
63
+ decisions.set(owner.publicKey, {
64
+ routeKey,
65
+ ownership: "current",
66
+ ownerPublicKey: owner.publicKey,
67
+ });
68
+ for (const superseded of sorted.slice(1)) {
69
+ decisions.set(superseded.publicKey, {
70
+ routeKey,
71
+ ownership: "superseded",
72
+ ownerPublicKey: owner.publicKey,
73
+ supersededByPublicKey: owner.publicKey,
74
+ });
75
+ }
76
+ }
77
+ return decisions;
78
+ }
79
+ //# sourceMappingURL=route-ownership.js.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * SecureTunnel — TypeScript transport layer wrapping the boringtun native addon.
3
+ *
4
+ * Manages the UDP socket, port forwarding (plaintext ↔ encrypted), and the
5
+ * 250ms timer loop that drives WireGuard's internal state machine. The native
6
+ * addon handles all cryptographic operations; this module handles I/O.
7
+ *
8
+ * Usage:
9
+ * const tunnel = new SecureTunnel({ privateKey, peerPublicKey, listenPort });
10
+ * await tunnel.start();
11
+ * tunnel.sendPlaintext(Buffer.from("hello"));
12
+ * tunnel.on("plaintext", (data) => console.log("received:", data));
13
+ * tunnel.stop();
14
+ */
15
+ import * as dgram from "node:dgram";
16
+ import { EventEmitter } from "node:events";
17
+ export interface PacketHandlingDisposition {
18
+ handled: boolean;
19
+ peerPublicKey: string;
20
+ outcome: "no_tunnel" | "done" | "write_to_network" | "write_to_tunnel" | "decrypt_error" | "decrypt_exception";
21
+ errorDetail?: string;
22
+ }
23
+ /**
24
+ * External shared UDP socket interface.
25
+ * When provided, SecureTunnel uses this socket instead of creating its own.
26
+ * This enables the single-port WireGuard architecture where NetworkManager
27
+ * owns ONE socket for ALL peers (like the real WireGuard kernel module).
28
+ */
29
+ export interface ExternalSocket {
30
+ /** Send encrypted data to a peer through the shared socket */
31
+ send(data: Buffer, port: number, host: string, cb?: (err?: Error) => void): void;
32
+ /** The bound port of the shared socket (returned from start()) */
33
+ port: number;
34
+ /**
35
+ * Subscribe to incoming packets on the shared socket.
36
+ * The handler receives ALL packets — the tunnel must check if each packet
37
+ * belongs to it (via decrypt) and report whether it consumed the packet.
38
+ * Returns an unsubscribe function.
39
+ */
40
+ onPacket(handler: (msg: Buffer, rinfo: dgram.RemoteInfo) => PacketHandlingDisposition): () => void;
41
+ }
42
+ export interface SecureTunnelOptions {
43
+ /** Base64-encoded X25519 private key */
44
+ privateKey: string;
45
+ /** Base64-encoded X25519 peer public key */
46
+ peerPublicKey: string;
47
+ /** Optional preshared key (base64) */
48
+ presharedKey?: string;
49
+ /** Persistent keepalive interval in seconds (0 = disabled, default: 25) */
50
+ keepalive?: number;
51
+ /** Local UDP port for WireGuard protocol (default: random high port) */
52
+ listenPort?: number;
53
+ /** Peer's endpoint host */
54
+ peerHost?: string;
55
+ /** Peer's endpoint port */
56
+ peerPort?: number;
57
+ /** Tunnel-internal source IPv4 address as 32-bit integer (default: 10.0.0.1 = 0x0a000001) */
58
+ tunnelSrcIp?: number;
59
+ /** Tunnel-internal destination IPv4 address as 32-bit integer (default: 10.0.0.2 = 0x0a000002) */
60
+ tunnelDstIp?: number;
61
+ /**
62
+ * External shared UDP socket. When provided, the tunnel uses this socket
63
+ * instead of creating its own. This is the single-port WireGuard model
64
+ * where NetworkManager owns ONE socket for ALL peers.
65
+ */
66
+ externalSocket?: ExternalSocket;
67
+ }
68
+ export interface TunnelStats {
69
+ /** Bytes sent through the tunnel */
70
+ bytesSent: number;
71
+ /** Bytes received through the tunnel */
72
+ bytesReceived: number;
73
+ /** Number of successful handshakes */
74
+ handshakes: number;
75
+ /** Timestamp of last handshake (epoch ms) */
76
+ lastHandshake: number | null;
77
+ /** Whether the tunnel is currently active */
78
+ active: boolean;
79
+ }
80
+ /**
81
+ * Encrypted UDP tunnel backed by boringtun.
82
+ *
83
+ * Events:
84
+ * - "plaintext": decrypted data received from peer
85
+ * - "handshake": WireGuard handshake completed
86
+ * - "error": tunnel or socket error
87
+ * - "close": tunnel stopped
88
+ */
89
+ /**
90
+ * Wrap arbitrary data in a minimal valid IPv4 packet for boringtun layer-3 tunnel.
91
+ *
92
+ * WireGuard is a layer-3 VPN — boringtun validates that decrypted plaintext is a
93
+ * valid IPv4 or IPv6 packet before returning it. Raw bytes are rejected with
94
+ * "InvalidPacket". This helper constructs a minimal 20-byte IPv4 header wrapping
95
+ * the given payload.
96
+ */
97
+ declare function wrapIpv4(payload: Buffer, srcIp?: number, dstIp?: number): Buffer;
98
+ /**
99
+ * Extract payload from an IPv4 packet by reading the IHL field.
100
+ * Optionally validates that the source IP matches the expected peer IP.
101
+ */
102
+ declare function unwrapIpv4(packet: Buffer, expectedSourceIp?: number): Buffer;
103
+ export { wrapIpv4, unwrapIpv4 };
104
+ export declare class SecureTunnel extends EventEmitter {
105
+ private options;
106
+ private socket;
107
+ private tickTimer;
108
+ private tunnel;
109
+ private stats;
110
+ /** Tracks whether the first successful decrypt has occurred (handshake completed) */
111
+ private handshakeCompleted;
112
+ private peerHost;
113
+ private peerPort;
114
+ /** Unsubscribe function for external socket mode */
115
+ private externalSocketUnsub;
116
+ constructor(options: SecureTunnelOptions);
117
+ /** Start the tunnel — binds UDP socket, creates WireGuard tunnel, starts timer */
118
+ start(): Promise<number>;
119
+ /**
120
+ * Handle an incoming encrypted packet. Called either by our own socket
121
+ * or by the shared socket dispatcher (in shared socket mode).
122
+ *
123
+ * Returns true if this tunnel consumed the packet, false otherwise.
124
+ */
125
+ handleIncomingPacket(msg: Buffer, rinfo: dgram.RemoteInfo): boolean;
126
+ private classifyIncomingPacket;
127
+ /** Whether the tunnel is currently active */
128
+ get isActive(): boolean;
129
+ /** Send plaintext data through the encrypted tunnel */
130
+ sendPlaintext(data: Buffer): void;
131
+ /** Set or update the peer endpoint */
132
+ setPeerEndpoint(host: string, port: number): void;
133
+ /** Get tunnel statistics */
134
+ getStats(): TunnelStats;
135
+ private markAuthenticatedSessionProof;
136
+ /** Stop the tunnel — closes socket (or unsubscribes from shared), stops timer */
137
+ stop(): void;
138
+ /** Handle a WireGuard tunnel result */
139
+ private handleResult;
140
+ }
141
+ //# sourceMappingURL=tunnel.d.ts.map