@decentnetwork/lan 0.1.26 → 0.1.28

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.
Binary file
Binary file
Binary file
Binary file
@@ -119,6 +119,20 @@ export declare class PeerManager extends EventEmitter {
119
119
  * Get an existing session for a peer (for routing logic).
120
120
  */
121
121
  getSession(pubkey: string): PacketSession | null;
122
+ /**
123
+ * Snapshot of all current PacketSession entries keyed by their peer
124
+ * userid. Used by PacketRouter to "adopt" sessions that were created
125
+ * BEFORE the router itself was constructed — without this, any
126
+ * HANDSHAKE_REQ that lands during the small window between
127
+ * peerManager.start() and the PacketRouter constructor leaves a
128
+ * session with no "packet" listener attached. Symptom: peer
129
+ * stats show `packetsForwarded=N` outbound but our `packetsReceived=0`,
130
+ * pings flow one way and time out the other.
131
+ */
132
+ getAllSessions(): Array<{
133
+ pubkey: string;
134
+ session: PacketSession;
135
+ }>;
122
136
  /**
123
137
  * Internal: Create and register a session.
124
138
  */
@@ -296,6 +296,19 @@ export class PeerManager extends EventEmitter {
296
296
  getSession(pubkey) {
297
297
  return this.sessions.get(pubkey) || null;
298
298
  }
299
+ /**
300
+ * Snapshot of all current PacketSession entries keyed by their peer
301
+ * userid. Used by PacketRouter to "adopt" sessions that were created
302
+ * BEFORE the router itself was constructed — without this, any
303
+ * HANDSHAKE_REQ that lands during the small window between
304
+ * peerManager.start() and the PacketRouter constructor leaves a
305
+ * session with no "packet" listener attached. Symptom: peer
306
+ * stats show `packetsForwarded=N` outbound but our `packetsReceived=0`,
307
+ * pings flow one way and time out the other.
308
+ */
309
+ getAllSessions() {
310
+ return Array.from(this.sessions, ([pubkey, session]) => ({ pubkey, session }));
311
+ }
299
312
  /**
300
313
  * Internal: Create and register a session.
301
314
  */
@@ -218,9 +218,29 @@ export class DaemonServer {
218
218
  // 6. Optional dora (DHCP-style) registration. When enabled and the
219
219
  // server is reachable, dora hands us a virtual IP and tells us
220
220
  // who else is on the network — eliminating the manual ipam.yaml
221
- // sync between operators. On any failure we fall through to the
222
- // IP already loaded from config + ipam.yaml.
221
+ // sync between operators.
222
+ //
223
+ // Fallback policy when dora is unreachable: DON'T use
224
+ // config.network.ip blindly. `agentnet init` defaults every
225
+ // fresh install to 10.86.1.10, so two new peers that can't
226
+ // reach dora will BOTH claim the same fallback and silently
227
+ // collide — symptom seen in the wild as packets going to the
228
+ // wrong daemon and 100% loss to legitimate peers. Use the
229
+ // deterministic-from-userid IP instead (sha256(userid) → last
230
+ // two octets of 10.86.X.Y). That guarantees every peer gets a
231
+ // unique IP keyed off identity, with no shared default to
232
+ // collide on. If dora comes up later, the
233
+ // onAllocatedIpChanged callback swaps the TUN to dora's value.
223
234
  let tunIp = this.config.network.ip;
235
+ const ownUserid = this.peerManager.getPubkey();
236
+ const deterministicFallback = Ipam.deterministicIpForUserid(ownUserid);
237
+ if (tunIp === "10.86.1.10" || !tunIp) {
238
+ // The init-default fallback IP — every fresh decentlan installs with
239
+ // this. Override with a per-identity deterministic value before
240
+ // dora even gets to try.
241
+ tunIp = deterministicFallback;
242
+ this.logger.info(`Using deterministic fallback IP ${tunIp} (derived from userid; avoids the 10.86.1.10 init-default collision)`);
243
+ }
224
244
  if (this.config.dora?.enabled && (this.config.dora.userids?.length ?? 0) > 0) {
225
245
  // Need to be on the Carrier network before dora can talk to its
226
246
  // server. Wait synchronously here — without joinNetwork the
@@ -234,7 +254,11 @@ export class DaemonServer {
234
254
  peerManager: this.peerManager,
235
255
  ipam: this.ipam,
236
256
  nodeName: this.config.node.name,
237
- preferredIp: this.config.network.ip,
257
+ // Hand dora our deterministic fallback as the requestedIp so
258
+ // a successful register returns the same IP across restarts
259
+ // (avoids the dora-stole-someone-else's-IP race that bit us
260
+ // when ubuntu was momentarily offline during reallocation).
261
+ preferredIp: tunIp,
238
262
  // Fires when dora's background retry eventually succeeds
239
263
  // AFTER the initial bootstrap already returned the fallback
240
264
  // IP. At that point the TUN is up on the fallback (e.g.
@@ -80,5 +80,15 @@ export declare class PacketRouter extends EventEmitter {
80
80
  * Handle incoming packet (Carrier -> peer -> TUN).
81
81
  */
82
82
  private handleIncomingPacket;
83
+ /**
84
+ * Attach the "packet" listener to a single session. Shared by both the
85
+ * session-opened event handler AND the adopt-existing-sessions pass
86
+ * run when PacketRouter starts. The WeakSet check makes calling this
87
+ * multiple times for the same session safe — without it, an early
88
+ * session would get two listeners and every inbound packet would be
89
+ * written to TUN twice (visible as `(DUP!)` ICMP replies on the
90
+ * sender side).
91
+ */
92
+ private attachSessionListener;
83
93
  private setupCarrierHandlers;
84
94
  }
@@ -339,22 +339,51 @@ export class PacketRouter extends EventEmitter {
339
339
  this.recordDrop(parsed.dstIp, "tun-write-failed", msg);
340
340
  }
341
341
  }
342
+ /**
343
+ * Attach the "packet" listener to a single session. Shared by both the
344
+ * session-opened event handler AND the adopt-existing-sessions pass
345
+ * run when PacketRouter starts. The WeakSet check makes calling this
346
+ * multiple times for the same session safe — without it, an early
347
+ * session would get two listeners and every inbound packet would be
348
+ * written to TUN twice (visible as `(DUP!)` ICMP replies on the
349
+ * sender side).
350
+ */
351
+ attachSessionListener(pubkey, session) {
352
+ if (this.listenedSessions.has(session))
353
+ return;
354
+ this.listenedSessions.add(session);
355
+ this.sessionManager.registerInboundSession(pubkey, session);
356
+ session.on("packet", (packet) => {
357
+ this.handleIncomingPacket(pubkey, packet).catch((err) => {
358
+ this.logger.error("Incoming packet error:", err);
359
+ });
360
+ });
361
+ }
342
362
  setupCarrierHandlers() {
343
- // When PeerManager creates a session (inbound or outbound), register packet handler.
344
- // Guard against double-attachment: this event fires from both the responder-side
345
- // handshake-req handler and the initiator-side openPacketSession completion. Without
346
- // the WeakSet check, each peer-arriving packet would be written to TUN twice, which
347
- // shows up as `(DUP!)` ICMP replies on the sender side.
363
+ // Adopt any sessions that already exist by the time PacketRouter is
364
+ // constructed. PeerManager.start() opens Carrier and friends can
365
+ // start handshaking IMMEDIATELY; if Vultr (or any peer) sends a
366
+ // HANDSHAKE_REQ before PacketRouter is built, PeerManager's
367
+ // session-opened event fires with nobody listening the session
368
+ // ends up in PeerManager.sessions but its "packet" callback is
369
+ // never wired, so every inbound DATA frame is silently dropped.
370
+ // Symptom seen in the wild: Vultr stats `packetsForwarded=N`
371
+ // outbound but mac-dev stats `packetsReceived=0`, pings flow one
372
+ // way and time out the other.
373
+ for (const { pubkey, session } of this.peerManager.getAllSessions()) {
374
+ this.attachSessionListener(pubkey, session);
375
+ }
376
+ // When PeerManager creates a session AFTER this point (inbound or
377
+ // outbound), register the packet handler. Guard against double-
378
+ // attachment: this event fires from both the responder-side
379
+ // handshake-req handler and the initiator-side openPacketSession
380
+ // completion. Without the WeakSet check, each peer-arriving packet
381
+ // would be written to TUN twice, which shows up as `(DUP!)` ICMP
382
+ // replies on the sender side.
348
383
  this.peerManager.on("session-opened", ({ pubkey }) => {
349
384
  const session = this.peerManager.getSession(pubkey);
350
385
  if (session && !this.listenedSessions.has(session)) {
351
- this.listenedSessions.add(session);
352
- this.sessionManager.registerInboundSession(pubkey, session);
353
- session.on("packet", (packet) => {
354
- this.handleIncomingPacket(pubkey, packet).catch((err) => {
355
- this.logger.error("Incoming packet error:", err);
356
- });
357
- });
386
+ this.attachSessionListener(pubkey, session);
358
387
  }
359
388
  });
360
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
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",