@decentnetwork/lan 0.1.7 → 0.1.9

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
@@ -108,11 +108,13 @@ export class ConfigLoader {
108
108
  enabled: false,
109
109
  userids: [],
110
110
  refreshIntervalMs: 60_000,
111
- // Default: do NOT auto-friend roster peers. Dora is a naming
112
- // / IP service, not a trust statement. Operators opt in with
113
- // `agentnet dora autofriend all` (closed labs) or
114
- // `agentnet dora autofriend allow <name|userid>` (whitelist).
115
- autoFriend: "none",
111
+ // Default: auto-friend every peer in the dora roster. Dora
112
+ // membership IS the trust statement joining a dora means
113
+ // "I want to be on this network", and a single-tenant lab is
114
+ // the common case. Operators on a multi-tenant / public
115
+ // dora opt out with `agentnet dora autofriend none` or
116
+ // whitelist via `agentnet dora autofriend allow <peer>...`.
117
+ autoFriend: "all",
116
118
  },
117
119
  };
118
120
  }
@@ -191,6 +191,48 @@ export class DaemonServer {
191
191
  ipam: this.ipam,
192
192
  nodeName: this.config.node.name,
193
193
  preferredIp: this.config.network.ip,
194
+ // Fires when dora's background retry eventually succeeds
195
+ // AFTER the initial bootstrap already returned the fallback
196
+ // IP. At that point the TUN is up on the fallback (e.g.
197
+ // 10.86.1.10) but our record in dora's roster says we're
198
+ // at 10.86.1.15 — other peers route to .15, our kernel
199
+ // doesn't recognize it as local, packets drop. Rebuild
200
+ // the TUN at the new IP so traffic actually reaches us.
201
+ onAllocatedIpChanged: async (newIp) => {
202
+ if (!this.tunDevice || !this.routeManager)
203
+ return;
204
+ const currentIp = this.tunDevice.getConfig().ip;
205
+ if (currentIp === newIp)
206
+ return;
207
+ this.logger.info(`Reconfiguring TUN: dora-allocated IP changed ${currentIp} -> ${newIp}`);
208
+ const ifname = this.tunDevice.getInterface();
209
+ try {
210
+ await this.routeManager.cleanup(ifname, this.config.network.subnet, currentIp);
211
+ await this.tunDevice.close();
212
+ this.tunDevice = new TunDevice({
213
+ config: {
214
+ name: this.config.network.interface,
215
+ ip: newIp,
216
+ subnet: this.config.network.subnet,
217
+ },
218
+ mockMode: this.useMockTun,
219
+ });
220
+ await this.tunDevice.open();
221
+ if (!this.useMockTun) {
222
+ await this.routeManager.configureTun({
223
+ ...this.tunDevice.getConfig(),
224
+ name: this.tunDevice.getInterface(),
225
+ });
226
+ }
227
+ // Re-attach the packet-router's TUN listener — the
228
+ // listener was bound to the old TunDevice instance.
229
+ this.packetRouter?.swapTunDevice(this.tunDevice);
230
+ this.logger.info(`TUN now at ${newIp}`);
231
+ }
232
+ catch (err) {
233
+ this.logger.error(`Failed to reconfigure TUN to ${newIp}: ${err instanceof Error ? err.message : err}`);
234
+ }
235
+ },
194
236
  });
195
237
  const doraIp = await this.doraIntegration.bootstrap();
196
238
  if (doraIp && doraIp !== tunIp) {
@@ -23,6 +23,15 @@ export interface DoraIntegrationOptions {
23
23
  /** Preferred virtual IP from local config — sent as `requestedIp` so a
24
24
  * restart keeps the same address when possible. */
25
25
  preferredIp?: string;
26
+ /**
27
+ * Fires when dora register completes AFTER the initial bootstrap
28
+ * already returned (i.e. the retry path). At that moment the TUN
29
+ * is already configured at the fallback IP, but our actual record
30
+ * in dora's roster has a different IP. The daemon needs to
31
+ * reconfigure the TUN, or the peer is reachable at neither IP
32
+ * (TUN listens on fallback, peers' IPAM says our allocated IP).
33
+ */
34
+ onAllocatedIpChanged?: (newIp: string) => Promise<void> | void;
26
35
  }
27
36
  export declare class DoraIntegration {
28
37
  private opts;
@@ -194,10 +194,24 @@ export class DoraIntegration {
194
194
  this.logger.debug("Retrying dora bootstrap…");
195
195
  // Don't await — let it run async and clear the timer if it
196
196
  // succeeds, otherwise leave the timer running.
197
- void this.bootstrap().then(() => {
197
+ void this.bootstrap().then(async () => {
198
198
  if (this.registered && this.retryBootstrapTimer) {
199
199
  clearInterval(this.retryBootstrapTimer);
200
200
  this.retryBootstrapTimer = undefined;
201
+ // Notify the daemon so it can reconfigure the TUN — the
202
+ // TUN is already up at the fallback IP at this point, but
203
+ // dora gave us a different IP and our record in the roster
204
+ // reflects that. Without rebuild, other peers route to our
205
+ // dora IP and our kernel drops the packet because the TUN
206
+ // listens on the fallback.
207
+ if (this.allocatedIp && this.opts.onAllocatedIpChanged) {
208
+ try {
209
+ await this.opts.onAllocatedIpChanged(this.allocatedIp);
210
+ }
211
+ catch (err) {
212
+ this.logger.warn(`onAllocatedIpChanged callback failed: ${err instanceof Error ? err.message : err}`);
213
+ }
214
+ }
201
215
  }
202
216
  });
203
217
  };
@@ -318,14 +332,16 @@ export class DoraIntegration {
318
332
  this.friendRequested.add(entry.userid);
319
333
  return;
320
334
  }
321
- // Policy gate. Dora is a name service, not a trust statement.
322
- // Default ("none") means we DON'T auto-friend roster peers
323
- // an operator on a multi-tenant dora isn't asking to be
324
- // mutually-friended with everyone else just because they share
325
- // a name service. "all" reproduces the old behavior (closed
326
- // labs). Otherwise the policy is a whitelist of names or
327
- // userids; only matches are friended.
328
- const policy = this.opts.config.autoFriend ?? "none";
335
+ // Policy gate. The undefined default is "all" because a peer
336
+ // that just joined a dora explicitly said "I want to be on
337
+ // this network" that's the trust statement. Without "all"
338
+ // as the legacy-config default, new peers can never join an
339
+ // existing mesh: their roster fills up but everyone else's
340
+ // legacy configs (lacking autoFriend) silently refuse to
341
+ // friend them back. Operators on a public / multi-tenant dora
342
+ // can opt out with `agentnet dora autofriend none` or supply
343
+ // a whitelist via `agentnet dora autofriend allow <peer>...`.
344
+ const policy = this.opts.config.autoFriend ?? "all";
329
345
  if (!this.policyAllows(entry, policy)) {
330
346
  // Mark as "seen" so we don't re-evaluate the policy every
331
347
  // 60s for entries we deliberately skip — but DON'T mark as
@@ -30,6 +30,16 @@ export declare class PacketRouter extends EventEmitter {
30
30
  constructor(opts: PacketRouterOptions);
31
31
  start(): Promise<void>;
32
32
  stop(): Promise<void>;
33
+ /**
34
+ * Swap the active TUN device. Used when dora's background retry
35
+ * succeeds with a different IP than the daemon's initial fallback
36
+ * — the daemon tears down the old TUN, brings up a fresh one at
37
+ * the new IP, and calls this to re-wire the packet read loop +
38
+ * the packet event handler.
39
+ *
40
+ * Caller is responsible for opening the new TUN before calling.
41
+ */
42
+ swapTunDevice(newDevice: TunDevice): Promise<void>;
33
43
  /**
34
44
  * Periodically send a 1-byte ping through each connected packet session
35
45
  * so the Carrier crypto session doesn't idle out. The receiver's IpParser
@@ -83,6 +83,38 @@ export class PacketRouter extends EventEmitter {
83
83
  this.sessionManager.closeAll();
84
84
  this.logger.info("Packet router stopped");
85
85
  }
86
+ /**
87
+ * Swap the active TUN device. Used when dora's background retry
88
+ * succeeds with a different IP than the daemon's initial fallback
89
+ * — the daemon tears down the old TUN, brings up a fresh one at
90
+ * the new IP, and calls this to re-wire the packet read loop +
91
+ * the packet event handler.
92
+ *
93
+ * Caller is responsible for opening the new TUN before calling.
94
+ */
95
+ async swapTunDevice(newDevice) {
96
+ if (this.tunDevice === newDevice)
97
+ return;
98
+ this.logger.info("Swapping TUN device");
99
+ // Old device is already torn down by the caller (or about to be).
100
+ this.tunDevice = newDevice;
101
+ if (!this.isRunning)
102
+ return;
103
+ this.tunDevice.on("packet", (packet) => {
104
+ this.handleOutgoingPacket(packet).catch((err) => {
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ if (msg.includes("offline") ||
107
+ msg.includes("no transport") ||
108
+ msg.includes("Handshake timeout")) {
109
+ this.logger.debug(`Outgoing packet dropped: ${msg}`);
110
+ }
111
+ else {
112
+ this.logger.error("Outgoing packet error:", err);
113
+ }
114
+ });
115
+ });
116
+ await this.tunDevice.startReadLoop();
117
+ }
86
118
  /**
87
119
  * Periodically send a 1-byte ping through each connected packet session
88
120
  * so the Carrier crypto session doesn't idle out. The receiver's IpParser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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",