@decentnetwork/lan 0.1.22 → 0.1.24

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
@@ -2,6 +2,7 @@
2
2
  * Manages Carrier peer identity and connections
3
3
  * Wraps @decentnetwork/peer SDK
4
4
  */
5
+ import { Peer } from "@decentnetwork/peer";
5
6
  import { EventEmitter } from "events";
6
7
  import { PacketSession } from "./packet-session.js";
7
8
  import type { PeerIdentity, RemotePeer } from "./types.js";
@@ -62,6 +63,16 @@ export declare class PeerManager extends EventEmitter {
62
63
  getHexPubkey(): string;
63
64
  getAddress(): string;
64
65
  getFriends(): RemotePeer[];
66
+ /**
67
+ * Snapshot of the underlying net_crypto session — UDP-direct vs
68
+ * TCP-relay vs not-yet-established. Surfaces what's behind a
69
+ * "friend.status === online" so an operator can see whether
70
+ * latency is going via a fast direct UDP path or via a TCP relay.
71
+ * Returns null if the SDK has no session record for the pubkey
72
+ * yet (i.e. handshake hasn't happened). See peer.ts for the
73
+ * Status shape.
74
+ */
75
+ getSessionStatus(pubkey: string): ReturnType<Peer["sessionStatus"]> | null;
65
76
  /**
66
77
  * Check if a specific friend is currently online (direct UDP path established).
67
78
  * Returns false for unknown pubkeys.
@@ -183,6 +183,20 @@ export class PeerManager extends EventEmitter {
183
183
  };
184
184
  });
185
185
  }
186
+ /**
187
+ * Snapshot of the underlying net_crypto session — UDP-direct vs
188
+ * TCP-relay vs not-yet-established. Surfaces what's behind a
189
+ * "friend.status === online" so an operator can see whether
190
+ * latency is going via a fast direct UDP path or via a TCP relay.
191
+ * Returns null if the SDK has no session record for the pubkey
192
+ * yet (i.e. handshake hasn't happened). See peer.ts for the
193
+ * Status shape.
194
+ */
195
+ getSessionStatus(pubkey) {
196
+ if (!this.peer)
197
+ return null;
198
+ return this.peer.sessionStatus(pubkey);
199
+ }
186
200
  /**
187
201
  * Check if a specific friend is currently online (direct UDP path established).
188
202
  * Returns false for unknown pubkeys.
@@ -1282,7 +1282,32 @@ export async function cmdDoraAutofriend(args) {
1282
1282
  * the writes fail with EACCES.
1283
1283
  */
1284
1284
  export async function cmdServiceInstall(args) {
1285
- const dir = args.configDir || ConfigLoader.defaultConfigDir();
1285
+ // Detect sudo's $HOME=/root trap. When `service install` is run via
1286
+ // sudo and no --config-dir is provided, derive the home dir from
1287
+ // SUDO_USER instead of os.homedir() — otherwise the unit's
1288
+ // ExecStart points at /root/.agentnet which is empty and the daemon
1289
+ // immediately crash-loops. Falls back to defaultConfigDir() when
1290
+ // SUDO_USER isn't set (non-sudo invocation).
1291
+ let dir;
1292
+ if (args.configDir) {
1293
+ dir = args.configDir;
1294
+ }
1295
+ else if (process.env.SUDO_USER && process.env.SUDO_USER !== "root") {
1296
+ const { execSync } = await import("child_process");
1297
+ try {
1298
+ const sudoHome = execSync(`getent passwd ${process.env.SUDO_USER} | cut -d: -f6`, { encoding: "utf-8" }).trim();
1299
+ dir = sudoHome
1300
+ ? `${sudoHome}/.agentnet`
1301
+ : ConfigLoader.defaultConfigDir();
1302
+ console.log(`[service install] sudo detected — using ${dir} (override with --config-dir)`);
1303
+ }
1304
+ catch {
1305
+ dir = ConfigLoader.defaultConfigDir();
1306
+ }
1307
+ }
1308
+ else {
1309
+ dir = ConfigLoader.defaultConfigDir();
1310
+ }
1286
1311
  const { spawnSync, execSync } = await import("child_process");
1287
1312
  if (process.platform === "linux") {
1288
1313
  const unitPath = "/etc/systemd/system/agentnet.service";
@@ -202,6 +202,13 @@ export class DaemonServer {
202
202
  status: f.status,
203
203
  address: f.address,
204
204
  acceptedAt: f.acceptedAt,
205
+ // Transport state of the underlying net_crypto session.
206
+ // Surface this so operators can tell "online via direct UDP
207
+ // (~80ms RTT)" from "online via TCP relay (~500ms+ RTT)" —
208
+ // the difference matters a lot and `status: online` hides it.
209
+ // Null when no session record exists yet (handshake hasn't
210
+ // happened).
211
+ session: this.peerManager?.getSessionStatus(f.pubkey) ?? null,
205
212
  })),
206
213
  ipam: ipamPeers,
207
214
  };
@@ -61,6 +61,20 @@ export declare class DoraIntegration {
61
61
  * us, and auto-IP allocation is lost. bootstrap() re-derives its
62
62
  * own myUserid so we don't need to thread it through.
63
63
  */
64
+ /**
65
+ * Re-send the friend-request to each configured dora userid that's
66
+ * still in "requested" state. The public express relay can drop
67
+ * friend-requests transiently (HTTP 500s observed in the wild) AND
68
+ * DHT onion discovery can miss freshly-restarted dora announces;
69
+ * either path failing strands a daemon with `status: requested`
70
+ * forever. The Carrier SDK doesn't retry the friend-request on
71
+ * its own, so we kick it again here whenever bootstrap retry runs.
72
+ *
73
+ * We read the address back from peer.friends() — `sendFriendRequest`
74
+ * stored it in the friend record when the original (failed) request
75
+ * went out, so we don't need it threaded through config.
76
+ */
77
+ private resendStuckFriendRequests;
64
78
  private scheduleBootstrapRetry;
65
79
  /**
66
80
  * Wrap a single register call with up to 3 retries spaced 2s apart.
@@ -185,12 +185,48 @@ export class DoraIntegration {
185
185
  * us, and auto-IP allocation is lost. bootstrap() re-derives its
186
186
  * own myUserid so we don't need to thread it through.
187
187
  */
188
+ /**
189
+ * Re-send the friend-request to each configured dora userid that's
190
+ * still in "requested" state. The public express relay can drop
191
+ * friend-requests transiently (HTTP 500s observed in the wild) AND
192
+ * DHT onion discovery can miss freshly-restarted dora announces;
193
+ * either path failing strands a daemon with `status: requested`
194
+ * forever. The Carrier SDK doesn't retry the friend-request on
195
+ * its own, so we kick it again here whenever bootstrap retry runs.
196
+ *
197
+ * We read the address back from peer.friends() — `sendFriendRequest`
198
+ * stored it in the friend record when the original (failed) request
199
+ * went out, so we don't need it threaded through config.
200
+ */
201
+ resendStuckFriendRequests() {
202
+ const userids = this.opts.config.userids ?? [];
203
+ const peer = this.opts.peerManager;
204
+ const friends = peer.getFriends();
205
+ for (const id of userids) {
206
+ const f = friends.find((x) => x.userid === id || x.pubkey === id);
207
+ if (!f)
208
+ continue;
209
+ if (f.status !== "requested")
210
+ continue;
211
+ if (!f.address)
212
+ continue;
213
+ this.logger.info(`Re-sending friend-request to dora ${id.slice(0, 16)}... (status was 'requested')`);
214
+ peer
215
+ .sendFriendRequest(f.address, "decentlan: dora retry")
216
+ .catch((err) => {
217
+ this.logger.debug(`friend-request retry failed for ${id.slice(0, 16)}: ${err instanceof Error ? err.message : err}`);
218
+ });
219
+ }
220
+ }
188
221
  scheduleBootstrapRetry() {
189
222
  if (this.retryBootstrapTimer || this.registered)
190
223
  return;
191
224
  const tick = () => {
192
225
  if (this.registered)
193
226
  return;
227
+ // Re-send the friend-request first — if it was lost to express
228
+ // 500s or DHT misses, this is what unblocks the retry.
229
+ this.resendStuckFriendRequests();
194
230
  this.logger.debug("Retrying dora bootstrap…");
195
231
  // Don't await — let it run async and clear the timer if it
196
232
  // succeeds, otherwise leave the timer running.
package/docs/INSTALL.md CHANGED
@@ -35,25 +35,56 @@ specific TUN helper binaries (we ship `linux-amd64`, `linux-arm64`,
35
35
  agentnet --help
36
36
  ```
37
37
 
38
- ## First-time setup
38
+ ## First-time setup — three commands
39
39
 
40
40
  ```bash
41
- # 1. Generate this machine's Carrier identity + default config
42
- agentnet init --name my-laptop
41
+ sudo npm install -g @decentnetwork/lan
42
+ agentnet init --name my-machine
43
+ sudo agentnet service install --config-dir $HOME/.agentnet
44
+ ```
45
+
46
+ That's it. `agentnet init` already points at the public dora and sends
47
+ the one-time friend-request; `service install` writes a systemd unit
48
+ (Linux) or LaunchDaemon (macOS) that runs the daemon as root with
49
+ auto-restart and logs to `/var/log/agentnet.log`.
43
50
 
44
- # 2. Point at the dora name server. Pass the FULL ADDRESS (whoever
45
- # runs dora publishes it). This one command friends dora,
46
- # derives its userid, and writes the config — no separate
47
- # friend-request step needed.
48
- agentnet dora enable --address <dora's Carrier address>
51
+ Wait ~60–90s, then verify:
49
52
 
50
- # 3. Start the daemon (needs sudo for the TUN device)
51
- sudo $(which agentnet) up --real-tun
53
+ ```bash
54
+ agentnet ipam list # should show 6+ peers
55
+ agentnet diag | grep tun # your allocated 10.86.1.x
56
+ ```
57
+
58
+ > ### If `ipam list` shows only yourself after 2 minutes
59
+ > The friend-request to dora didn't land — usually because the public
60
+ > express relay is having a bad day and Vultr/cloud DHT discovery
61
+ > sometimes misses freshly-restarted dora announces. Two quick
62
+ > recovery steps:
63
+ >
64
+ > 1. Re-send the friend-request directly through your live daemon:
65
+ > ```bash
66
+ > agentnet friend-request Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd
67
+ > ```
68
+ > Then wait 60s and re-check `agentnet ipam list`.
69
+ >
70
+ > 2. If that still doesn't work, the **dora operator** can pre-friend
71
+ > you so you don't depend on the public delivery path: give them
72
+ > your userid (the line `agentnet identity show` prints) and ask
73
+ > them to run a one-shot add on their dora data-dir, then they
74
+ > restart dora. Your next refresh lands instantly.
75
+
76
+ ## Pointing at a private dora (skip if you're joining the public network)
77
+
78
+ The public dora is baked into `agentnet init` defaults. To override:
79
+
80
+ ```bash
81
+ agentnet dora enable --address <YOUR private dora's full Carrier address>
82
+ sudo systemctl restart agentnet # or 'launchctl unload + load' on macOS
52
83
  ```
53
84
 
54
- Three commands, no "userid vs address" gymnastics. The daemon will
55
- register with dora, pull the roster, and auto-friend every other
56
- peer in the network.
85
+ `agentnet dora enable` replaces the default userid, sends a fresh
86
+ friend-request, and updates the config — one command, no "userid vs
87
+ address" gymnastics.
57
88
 
58
89
  The daemon will:
59
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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",