@decentnetwork/lan 0.1.11 → 0.1.13
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/carrier/peer-manager.d.ts +7 -0
- package/dist/carrier/peer-manager.js +33 -19
- package/dist/router/packet-router.d.ts +25 -0
- package/dist/router/packet-router.js +104 -26
- package/dist/router/session-manager.d.ts +8 -0
- package/dist/router/session-manager.js +22 -7
- package/dist/router/types.d.ts +19 -0
- package/package.json +1 -1
|
@@ -65,6 +65,13 @@ export declare class PeerManager extends EventEmitter {
|
|
|
65
65
|
/**
|
|
66
66
|
* Check if a specific friend is currently online (direct UDP path established).
|
|
67
67
|
* Returns false for unknown pubkeys.
|
|
68
|
+
*
|
|
69
|
+
* Matches on BOTH `pubkey` and `userid` because the SDK's FriendRecord
|
|
70
|
+
* is inconsistent across code paths: outbound friend requests set
|
|
71
|
+
* `pubkey: friendId` (the base58 userid), inbound friend requests do
|
|
72
|
+
* the same, but older persisted entries (pre-1.7.x) only had `pubkey`
|
|
73
|
+
* and the comparison failed silently when callers pass a userid.
|
|
74
|
+
* Same belt-and-braces as `isFriend()` above.
|
|
68
75
|
*/
|
|
69
76
|
isFriendOnline(pubkey: string): boolean;
|
|
70
77
|
/**
|
|
@@ -169,11 +169,20 @@ export class PeerManager extends EventEmitter {
|
|
|
169
169
|
/**
|
|
170
170
|
* Check if a specific friend is currently online (direct UDP path established).
|
|
171
171
|
* Returns false for unknown pubkeys.
|
|
172
|
+
*
|
|
173
|
+
* Matches on BOTH `pubkey` and `userid` because the SDK's FriendRecord
|
|
174
|
+
* is inconsistent across code paths: outbound friend requests set
|
|
175
|
+
* `pubkey: friendId` (the base58 userid), inbound friend requests do
|
|
176
|
+
* the same, but older persisted entries (pre-1.7.x) only had `pubkey`
|
|
177
|
+
* and the comparison failed silently when callers pass a userid.
|
|
178
|
+
* Same belt-and-braces as `isFriend()` above.
|
|
172
179
|
*/
|
|
173
180
|
isFriendOnline(pubkey) {
|
|
174
181
|
if (!this.peer)
|
|
175
182
|
return false;
|
|
176
|
-
const friend = this.peer
|
|
183
|
+
const friend = this.peer
|
|
184
|
+
.friends()
|
|
185
|
+
.find((f) => f.pubkey === pubkey || f.userid === pubkey);
|
|
177
186
|
return friend?.status === "online";
|
|
178
187
|
}
|
|
179
188
|
/**
|
|
@@ -312,25 +321,30 @@ export class PeerManager extends EventEmitter {
|
|
|
312
321
|
};
|
|
313
322
|
this.emit("friend-connection", evt);
|
|
314
323
|
this.logger.info(`Friend ${event.pubkey} ${event.status}`);
|
|
315
|
-
//
|
|
316
|
-
// online after we already had a session — e.g. they restarted
|
|
317
|
-
// their daemon), the Carrier crypto session under us has
|
|
318
|
-
// rotated. Our cached PacketSession is bound to the previous
|
|
319
|
-
// session's frame-sequence state. Sending DATA frames through
|
|
320
|
-
// it produces ciphertext the new peer can't decrypt; the
|
|
321
|
-
// packets land in their PeerManager.onText but no session
|
|
322
|
-
// matches and they're dropped silently. Result: 100% loss
|
|
323
|
-
// that looks like "the link is up but pings time out."
|
|
324
|
+
// Drop our cached PacketSession ONLY on disconnect.
|
|
324
325
|
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
326
|
+
// History: we used to also close on `connected` events under the
|
|
327
|
+
// theory that the Carrier crypto session had "rotated" and our
|
|
328
|
+
// session was bound to old state. That theory was wrong —
|
|
329
|
+
// PacketSession holds only `sessionId` (decentlan-layer demux ID)
|
|
330
|
+
// and `isActive`; all crypto state lives inside the SDK and is
|
|
331
|
+
// read fresh on every sendText. Worse, the SDK fires `connected`
|
|
332
|
+
// multiple times during initial session-up (once when a TCP relay
|
|
333
|
+
// route appears, again when UDP holepunching lands). Each one
|
|
334
|
+
// destroyed our just-created PacketSession mid-handshake,
|
|
335
|
+
// killing the HANDSHAKE_ACK before it could fire — symptom:
|
|
336
|
+
// activeSessions=0 forever, even though friend status shows
|
|
337
|
+
// "online". The restart-case the old code worried about is
|
|
338
|
+
// already covered: a fresh HANDSHAKE_REQ from the remote enters
|
|
339
|
+
// the onText path below and triggers session replacement when
|
|
340
|
+
// the sessionId differs.
|
|
341
|
+
if (event.status !== "connected") {
|
|
342
|
+
const existing = this.sessions.get(event.pubkey);
|
|
343
|
+
if (existing) {
|
|
344
|
+
this.logger.debug(`Closing packet session to ${event.pubkey} on friend-disconnect`);
|
|
345
|
+
existing.close();
|
|
346
|
+
this.sessions.delete(event.pubkey);
|
|
347
|
+
}
|
|
334
348
|
}
|
|
335
349
|
});
|
|
336
350
|
// Text messages contain our framed packets
|
|
@@ -20,6 +20,7 @@ export interface PacketRouterOptions {
|
|
|
20
20
|
export declare class PacketRouter extends EventEmitter {
|
|
21
21
|
private tunDevice;
|
|
22
22
|
private peerManager;
|
|
23
|
+
private ipam;
|
|
23
24
|
private acl;
|
|
24
25
|
private sessionManager;
|
|
25
26
|
private logger;
|
|
@@ -27,6 +28,13 @@ export declare class PacketRouter extends EventEmitter {
|
|
|
27
28
|
private listenedSessions;
|
|
28
29
|
private keepaliveTimer?;
|
|
29
30
|
private stats;
|
|
31
|
+
/** Per-destination drop reasons. Exposed via getStats() for `agentnet diag`. */
|
|
32
|
+
private dropsByDst;
|
|
33
|
+
/** (dstIp,reason) tuples we've already logged at info level — avoids
|
|
34
|
+
* spamming the journal when a single broken destination receives
|
|
35
|
+
* thousands of packets. The first hit per tuple is logged loudly so
|
|
36
|
+
* the operator notices in `journalctl`; subsequent hits stay at debug. */
|
|
37
|
+
private loggedDropTuples;
|
|
30
38
|
constructor(opts: PacketRouterOptions);
|
|
31
39
|
start(): Promise<void>;
|
|
32
40
|
stop(): Promise<void>;
|
|
@@ -47,6 +55,23 @@ export declare class PacketRouter extends EventEmitter {
|
|
|
47
55
|
*/
|
|
48
56
|
private sendKeepalivesToActiveSessions;
|
|
49
57
|
getStats(): ForwardingStats;
|
|
58
|
+
/**
|
|
59
|
+
* Add (or correct) a (carrierId, virtualIp) mapping in IPAM based on
|
|
60
|
+
* an authenticated inbound packet. Idempotent and cheap — does
|
|
61
|
+
* nothing when the mapping already matches.
|
|
62
|
+
*
|
|
63
|
+
* The IP can change for a single peer (dora re-allocation, daemon
|
|
64
|
+
* restart with a different fallback IP) so we always overwrite on
|
|
65
|
+
* mismatch rather than first-write-wins.
|
|
66
|
+
*/
|
|
67
|
+
private learnPeerMapping;
|
|
68
|
+
/**
|
|
69
|
+
* Record a drop and bump per-destination diagnostics. Logs the FIRST
|
|
70
|
+
* occurrence of each (dstIp, reason) at info level so the failure mode
|
|
71
|
+
* shows up in `journalctl` without needing AGENTNET_LOG_LEVEL=debug.
|
|
72
|
+
* Subsequent drops are summed into the counter.
|
|
73
|
+
*/
|
|
74
|
+
private recordDrop;
|
|
50
75
|
/**
|
|
51
76
|
* Handle outgoing packet (app -> TUN -> Carrier).
|
|
52
77
|
*/
|
|
@@ -9,9 +9,11 @@ import { EventEmitter } from "events";
|
|
|
9
9
|
import { IpParser } from "./ip-parser.js";
|
|
10
10
|
import { SessionManager } from "./session-manager.js";
|
|
11
11
|
import { Logger } from "../utils/logger.js";
|
|
12
|
+
const SUBNET_PREFIX = "10.86.";
|
|
12
13
|
export class PacketRouter extends EventEmitter {
|
|
13
14
|
tunDevice;
|
|
14
15
|
peerManager;
|
|
16
|
+
ipam;
|
|
15
17
|
acl;
|
|
16
18
|
sessionManager;
|
|
17
19
|
logger;
|
|
@@ -25,10 +27,18 @@ export class PacketRouter extends EventEmitter {
|
|
|
25
27
|
packetsDropped: 0,
|
|
26
28
|
activeSessions: 0,
|
|
27
29
|
};
|
|
30
|
+
/** Per-destination drop reasons. Exposed via getStats() for `agentnet diag`. */
|
|
31
|
+
dropsByDst = new Map();
|
|
32
|
+
/** (dstIp,reason) tuples we've already logged at info level — avoids
|
|
33
|
+
* spamming the journal when a single broken destination receives
|
|
34
|
+
* thousands of packets. The first hit per tuple is logged loudly so
|
|
35
|
+
* the operator notices in `journalctl`; subsequent hits stay at debug. */
|
|
36
|
+
loggedDropTuples = new Set();
|
|
28
37
|
constructor(opts) {
|
|
29
38
|
super();
|
|
30
39
|
this.tunDevice = opts.tunDevice;
|
|
31
40
|
this.peerManager = opts.peerManager;
|
|
41
|
+
this.ipam = opts.ipam;
|
|
32
42
|
this.acl = opts.acl;
|
|
33
43
|
this.sessionManager = new SessionManager({
|
|
34
44
|
peerManager: opts.peerManager,
|
|
@@ -137,11 +147,65 @@ export class PacketRouter extends EventEmitter {
|
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
getStats() {
|
|
150
|
+
const dropsByDst = {};
|
|
151
|
+
for (const [dstIp, info] of this.dropsByDst) {
|
|
152
|
+
dropsByDst[dstIp] = info;
|
|
153
|
+
}
|
|
140
154
|
return {
|
|
141
155
|
...this.stats,
|
|
142
156
|
activeSessions: this.sessionManager.getActiveCount(),
|
|
157
|
+
dropsByDst,
|
|
143
158
|
};
|
|
144
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Add (or correct) a (carrierId, virtualIp) mapping in IPAM based on
|
|
162
|
+
* an authenticated inbound packet. Idempotent and cheap — does
|
|
163
|
+
* nothing when the mapping already matches.
|
|
164
|
+
*
|
|
165
|
+
* The IP can change for a single peer (dora re-allocation, daemon
|
|
166
|
+
* restart with a different fallback IP) so we always overwrite on
|
|
167
|
+
* mismatch rather than first-write-wins.
|
|
168
|
+
*/
|
|
169
|
+
learnPeerMapping(srcPubkey, srcIp) {
|
|
170
|
+
const existing = this.ipam.resolveCarrierId(srcPubkey);
|
|
171
|
+
if (existing && existing.virtualIp === srcIp)
|
|
172
|
+
return;
|
|
173
|
+
// Skip our own pubkey — we already own our IPAM entry.
|
|
174
|
+
if (srcPubkey === this.peerManager.getPubkey())
|
|
175
|
+
return;
|
|
176
|
+
const name = existing?.name ?? `peer-${srcPubkey.slice(0, 8)}`;
|
|
177
|
+
this.ipam.assignPeer({
|
|
178
|
+
name,
|
|
179
|
+
carrierId: srcPubkey,
|
|
180
|
+
virtualIp: srcIp,
|
|
181
|
+
services: existing?.services ?? [],
|
|
182
|
+
});
|
|
183
|
+
this.logger.info(existing
|
|
184
|
+
? `IPAM updated from inbound: ${name} ${existing.virtualIp} -> ${srcIp}`
|
|
185
|
+
: `IPAM learned from inbound: ${name} (${srcPubkey.slice(0, 12)}...) -> ${srcIp}`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Record a drop and bump per-destination diagnostics. Logs the FIRST
|
|
189
|
+
* occurrence of each (dstIp, reason) at info level so the failure mode
|
|
190
|
+
* shows up in `journalctl` without needing AGENTNET_LOG_LEVEL=debug.
|
|
191
|
+
* Subsequent drops are summed into the counter.
|
|
192
|
+
*/
|
|
193
|
+
recordDrop(dstIp, reason, errorMsg) {
|
|
194
|
+
this.stats.packetsDropped++;
|
|
195
|
+
const existing = this.dropsByDst.get(dstIp);
|
|
196
|
+
const next = {
|
|
197
|
+
count: (existing?.count ?? 0) + 1,
|
|
198
|
+
lastReason: reason,
|
|
199
|
+
lastError: errorMsg ? errorMsg.slice(0, 200) : undefined,
|
|
200
|
+
lastAt: Date.now(),
|
|
201
|
+
};
|
|
202
|
+
this.dropsByDst.set(dstIp, next);
|
|
203
|
+
const tuple = `${dstIp}|${reason}`;
|
|
204
|
+
if (!this.loggedDropTuples.has(tuple)) {
|
|
205
|
+
this.loggedDropTuples.add(tuple);
|
|
206
|
+
this.logger.info(`Drop ${dstIp}: ${reason}${errorMsg ? ` — ${errorMsg.slice(0, 120)}` : ""} (further drops with this reason silenced)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
145
209
|
/**
|
|
146
210
|
* Handle outgoing packet (app -> TUN -> Carrier).
|
|
147
211
|
*/
|
|
@@ -150,8 +214,7 @@ export class PacketRouter extends EventEmitter {
|
|
|
150
214
|
return;
|
|
151
215
|
const parsed = IpParser.parse(packet);
|
|
152
216
|
if (!parsed) {
|
|
153
|
-
this.
|
|
154
|
-
this.logger.debug("Dropped invalid IP packet");
|
|
217
|
+
this.recordDrop("unknown", "invalid-ip");
|
|
155
218
|
return;
|
|
156
219
|
}
|
|
157
220
|
// Get our own pubkey for ACL (we are the source for outbound)
|
|
@@ -159,7 +222,7 @@ export class PacketRouter extends EventEmitter {
|
|
|
159
222
|
const proto = IpParser.protoName(parsed.protocol);
|
|
160
223
|
// Only forward TCP/UDP packets that have ports
|
|
161
224
|
if (parsed.dstPort === undefined && proto !== "icmp") {
|
|
162
|
-
this.
|
|
225
|
+
this.recordDrop(parsed.dstIp, "no-port");
|
|
163
226
|
return;
|
|
164
227
|
}
|
|
165
228
|
// ACL check (outbound)
|
|
@@ -178,11 +241,23 @@ export class PacketRouter extends EventEmitter {
|
|
|
178
241
|
return;
|
|
179
242
|
}
|
|
180
243
|
}
|
|
181
|
-
// Get session for destination
|
|
182
|
-
|
|
244
|
+
// Get session for destination. getOrOpenSession returns null for two
|
|
245
|
+
// distinct reasons; we want them distinguished in diag so the operator
|
|
246
|
+
// can tell "I forgot to add this peer to IPAM" from "the friend session
|
|
247
|
+
// hasn't completed yet." sessionManager.classify() looks up the IPAM
|
|
248
|
+
// state without triggering a handshake.
|
|
249
|
+
let session;
|
|
250
|
+
try {
|
|
251
|
+
session = await this.sessionManager.getOrOpenSession(parsed.dstIp);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
this.recordDrop(parsed.dstIp, "handshake-failed", msg);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
183
258
|
if (!session) {
|
|
184
|
-
this.
|
|
185
|
-
this.
|
|
259
|
+
const reason = this.sessionManager.classifyMissingSession(parsed.dstIp);
|
|
260
|
+
this.recordDrop(parsed.dstIp, reason);
|
|
186
261
|
return;
|
|
187
262
|
}
|
|
188
263
|
// Send packet
|
|
@@ -192,16 +267,8 @@ export class PacketRouter extends EventEmitter {
|
|
|
192
267
|
this.logger.debug(`Forwarded ${proto} ${parsed.srcIp}:${parsed.srcPort || "-"} -> ${parsed.dstIp}:${parsed.dstPort || "-"} (${packet.length} bytes)`);
|
|
193
268
|
}
|
|
194
269
|
catch (err) {
|
|
195
|
-
// Friend going offline mid-send is normal during connection flapping;
|
|
196
|
-
// log at debug rather than warn to avoid spamming the operator.
|
|
197
270
|
const msg = err instanceof Error ? err.message : String(err);
|
|
198
|
-
this.
|
|
199
|
-
if (msg.includes("offline") || msg.includes("no transport")) {
|
|
200
|
-
this.logger.debug(`Drop ${parsed.dstIp}: ${msg}`);
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
this.logger.warn(`Failed to forward to ${parsed.dstIp}: ${msg}`);
|
|
204
|
-
}
|
|
271
|
+
this.recordDrop(parsed.dstIp, "send-failed", msg);
|
|
205
272
|
}
|
|
206
273
|
}
|
|
207
274
|
/**
|
|
@@ -216,6 +283,25 @@ export class PacketRouter extends EventEmitter {
|
|
|
216
283
|
this.logger.debug("Dropped invalid incoming IP packet");
|
|
217
284
|
return;
|
|
218
285
|
}
|
|
286
|
+
// Auto-learn (srcPubkey -> srcIp) into IPAM. Without this, ubuntu
|
|
287
|
+
// can receive cn's ICMP fine but the reply path fails: ubuntu's
|
|
288
|
+
// outbound handleOutgoingPacket calls ipam.resolveIp("10.86.1.15")
|
|
289
|
+
// which returns null when ubuntu's dora roster sync is broken
|
|
290
|
+
// (dora friend offline, no other source for cn's virtualIp).
|
|
291
|
+
//
|
|
292
|
+
// We trust the (srcPubkey, srcIp) pairing for two reasons:
|
|
293
|
+
// 1. srcPubkey is Carrier-authenticated — only the real cn can
|
|
294
|
+
// open a PacketSession against ubuntu under cn's userid.
|
|
295
|
+
// 2. The only damage a lying sender could cause is shadowing
|
|
296
|
+
// their OWN reply routing — the IP is the destination for
|
|
297
|
+
// packets WE send back to THEM, no third party affected.
|
|
298
|
+
//
|
|
299
|
+
// Scope to the agentnet subnet (10.86.0.0/16) so a buggy peer
|
|
300
|
+
// leaking RFC1918 traffic through the TUN can't pollute our IPAM
|
|
301
|
+
// with 192.168.x.x entries.
|
|
302
|
+
if (parsed.srcIp.startsWith(SUBNET_PREFIX)) {
|
|
303
|
+
this.learnPeerMapping(srcPubkey, parsed.srcIp);
|
|
304
|
+
}
|
|
219
305
|
const proto = IpParser.protoName(parsed.protocol);
|
|
220
306
|
// ACL check (inbound)
|
|
221
307
|
if (parsed.dstPort !== undefined && (proto === "tcp" || proto === "udp")) {
|
|
@@ -240,7 +326,7 @@ export class PacketRouter extends EventEmitter {
|
|
|
240
326
|
// between TUN close and full session teardown produces a "TUN device
|
|
241
327
|
// not open" warning. Treat that case as a silent drop.
|
|
242
328
|
if (!this.isRunning || !this.tunDevice.isActive()) {
|
|
243
|
-
this.
|
|
329
|
+
this.recordDrop(parsed.dstIp, "tun-down");
|
|
244
330
|
return;
|
|
245
331
|
}
|
|
246
332
|
try {
|
|
@@ -249,16 +335,8 @@ export class PacketRouter extends EventEmitter {
|
|
|
249
335
|
this.logger.debug(`Received ${proto} ${parsed.srcIp}:${parsed.srcPort || "-"} -> ${parsed.dstIp}:${parsed.dstPort || "-"} (${packet.length} bytes)`);
|
|
250
336
|
}
|
|
251
337
|
catch (err) {
|
|
252
|
-
this.stats.packetsDropped++;
|
|
253
338
|
const msg = err instanceof Error ? err.message : String(err);
|
|
254
|
-
|
|
255
|
-
// Late packet during shutdown — already covered by the guard
|
|
256
|
-
// above on most paths but the TUN can also close mid-write.
|
|
257
|
-
this.logger.debug(`Drop late inbound packet: ${msg}`);
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
this.logger.warn(`Failed to write to TUN:`, err);
|
|
261
|
-
}
|
|
339
|
+
this.recordDrop(parsed.dstIp, "tun-write-failed", msg);
|
|
262
340
|
}
|
|
263
341
|
}
|
|
264
342
|
setupCarrierHandlers() {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { PacketSession } from "../carrier/packet-session.js";
|
|
6
6
|
import type { PeerManager } from "../carrier/peer-manager.js";
|
|
7
7
|
import type { Ipam } from "../ipam/ipam.js";
|
|
8
|
+
import type { DropReason } from "./types.js";
|
|
8
9
|
export declare class SessionManager {
|
|
9
10
|
private peerManager;
|
|
10
11
|
private ipam;
|
|
@@ -25,6 +26,13 @@ export declare class SessionManager {
|
|
|
25
26
|
* Get session by destination IP (no handshake)
|
|
26
27
|
*/
|
|
27
28
|
getSession(dstIp: string): PacketSession | null;
|
|
29
|
+
/**
|
|
30
|
+
* Classify why getOrOpenSession() returned null for a given dstIp. Used
|
|
31
|
+
* by PacketRouter to record an accurate drop reason in diag without
|
|
32
|
+
* having to plumb a (session | string-reason) union through the hot
|
|
33
|
+
* path.
|
|
34
|
+
*/
|
|
35
|
+
classifyMissingSession(dstIp: string): DropReason;
|
|
28
36
|
/**
|
|
29
37
|
* Register a session that was opened by remote peer (responder side).
|
|
30
38
|
* Called when PeerManager auto-creates session for inbound HANDSHAKE_REQ.
|
|
@@ -13,16 +13,16 @@ export class SessionManager {
|
|
|
13
13
|
this.peerManager = opts.peerManager;
|
|
14
14
|
this.ipam = opts.ipam;
|
|
15
15
|
this.logger = new Logger({ prefix: "SessionManager" });
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// window between the close() call and our re-keying race
|
|
21
|
-
// could return stale data. Belt-and-braces.
|
|
16
|
+
// Mirror PeerManager's policy: drop the IP→session cache on
|
|
17
|
+
// disconnect only. `connected` events fire repeatedly during
|
|
18
|
+
// initial session-up (TCP relay route, then UDP holepunch) and
|
|
19
|
+
// dropping on each one races with the handshake we just started.
|
|
22
20
|
this.peerManager.on("friend-connection", (evt) => {
|
|
21
|
+
if (evt.status === "connected")
|
|
22
|
+
return;
|
|
23
23
|
const record = this.ipam.resolveCarrierId(evt.pubkey);
|
|
24
24
|
if (record && this.sessions.has(record.virtualIp)) {
|
|
25
|
-
this.logger.debug(`Dropping ${record.name} (${record.virtualIp}) session on friend
|
|
25
|
+
this.logger.debug(`Dropping ${record.name} (${record.virtualIp}) session on friend-disconnect`);
|
|
26
26
|
this.sessions.delete(record.virtualIp);
|
|
27
27
|
}
|
|
28
28
|
});
|
|
@@ -74,6 +74,21 @@ export class SessionManager {
|
|
|
74
74
|
getSession(dstIp) {
|
|
75
75
|
return this.sessions.get(dstIp) || null;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Classify why getOrOpenSession() returned null for a given dstIp. Used
|
|
79
|
+
* by PacketRouter to record an accurate drop reason in diag without
|
|
80
|
+
* having to plumb a (session | string-reason) union through the hot
|
|
81
|
+
* path.
|
|
82
|
+
*/
|
|
83
|
+
classifyMissingSession(dstIp) {
|
|
84
|
+
const record = this.ipam.resolveIp(dstIp);
|
|
85
|
+
if (!record)
|
|
86
|
+
return "no-ipam-record";
|
|
87
|
+
if (!this.peerManager.isFriendOnline(record.carrierId)) {
|
|
88
|
+
return "friend-offline";
|
|
89
|
+
}
|
|
90
|
+
return "handshake-failed";
|
|
91
|
+
}
|
|
77
92
|
/**
|
|
78
93
|
* Register a session that was opened by remote peer (responder side).
|
|
79
94
|
* Called when PeerManager auto-creates session for inbound HANDSHAKE_REQ.
|
package/dist/router/types.d.ts
CHANGED
|
@@ -12,10 +12,29 @@ export interface ParsedPacket {
|
|
|
12
12
|
export declare const IP_PROTO_TCP = 6;
|
|
13
13
|
export declare const IP_PROTO_UDP = 17;
|
|
14
14
|
export declare const IP_PROTO_ICMP = 1;
|
|
15
|
+
/**
|
|
16
|
+
* Why a single outbound packet was dropped. Tracked per-destination so
|
|
17
|
+
* `agentnet diag` can answer the operator's first question — "why
|
|
18
|
+
* isn't my ping working?" — without forcing them to crank up the
|
|
19
|
+
* log level.
|
|
20
|
+
*/
|
|
21
|
+
export type DropReason = "invalid-ip" | "no-port" | "no-ipam-record" | "friend-offline" | "handshake-failed" | "send-failed" | "tun-write-failed" | "tun-down";
|
|
22
|
+
export interface DstDropInfo {
|
|
23
|
+
/** Total drops to this destination. */
|
|
24
|
+
count: number;
|
|
25
|
+
/** Most recent reason. */
|
|
26
|
+
lastReason: DropReason;
|
|
27
|
+
/** Truncated error message from the most recent drop, if any. */
|
|
28
|
+
lastError?: string;
|
|
29
|
+
/** Timestamp (ms since epoch) of the most recent drop. */
|
|
30
|
+
lastAt: number;
|
|
31
|
+
}
|
|
15
32
|
export interface ForwardingStats {
|
|
16
33
|
packetsForwarded: number;
|
|
17
34
|
packetsReceived: number;
|
|
18
35
|
packetsDenied: number;
|
|
19
36
|
packetsDropped: number;
|
|
20
37
|
activeSessions: number;
|
|
38
|
+
/** Per-destination drop diagnostics. Keyed by virtual IP. */
|
|
39
|
+
dropsByDst?: Record<string, DstDropInfo>;
|
|
21
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|