@decentnetwork/lan 0.1.44 → 0.1.46
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/acl/audit.d.ts +14 -1
- package/dist/acl/audit.js +60 -3
- package/dist/cli/commands.d.ts +28 -0
- package/dist/cli/commands.js +85 -7
- package/dist/daemon/server.js +13 -6
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/acl/audit.d.ts
CHANGED
|
@@ -8,7 +8,13 @@ export declare class AuditLog {
|
|
|
8
8
|
private logger;
|
|
9
9
|
private buffer;
|
|
10
10
|
private bufferLimit;
|
|
11
|
-
|
|
11
|
+
private maxBytes;
|
|
12
|
+
private keepFiles;
|
|
13
|
+
private curBytes;
|
|
14
|
+
constructor(filePath: string, opts?: {
|
|
15
|
+
maxBytes?: number;
|
|
16
|
+
keepFiles?: number;
|
|
17
|
+
});
|
|
12
18
|
/**
|
|
13
19
|
* Log an access attempt (allowed or denied)
|
|
14
20
|
*/
|
|
@@ -67,4 +73,11 @@ export declare class AuditLog {
|
|
|
67
73
|
*/
|
|
68
74
|
readSince(timestamp: number): AuditEntry[];
|
|
69
75
|
private write;
|
|
76
|
+
/**
|
|
77
|
+
* Size-based rotation: audit.log -> audit.log.1 -> ... -> audit.log.N,
|
|
78
|
+
* dropping the oldest. Bounds total on-disk audit data to
|
|
79
|
+
* maxBytes * (keepFiles + 1). Best-effort — a failure here must never
|
|
80
|
+
* break the daemon, so errors are logged and writing continues.
|
|
81
|
+
*/
|
|
82
|
+
private rotate;
|
|
70
83
|
}
|
package/dist/acl/audit.js
CHANGED
|
@@ -2,20 +2,40 @@
|
|
|
2
2
|
* Audit logger for ACL decisions and connection events
|
|
3
3
|
* Append-only JSONL file format
|
|
4
4
|
*/
|
|
5
|
-
import { appendFileSync, readFileSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync, statSync, renameSync, rmSync, } from "fs";
|
|
6
6
|
import { dirname } from "path";
|
|
7
7
|
import { Logger } from "../utils/logger.js";
|
|
8
|
+
// Defaults chosen so a busy exit (proxy_open/proxy_close per CONNECT — can
|
|
9
|
+
// be hundreds/sec for HLS video) can never fill the disk: at most
|
|
10
|
+
// maxBytes * (keepFiles + 1) on disk (~200 MB by default), oldest dropped.
|
|
11
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024; // rotate when the live file passes 50 MB
|
|
12
|
+
const DEFAULT_KEEP_FILES = 3; // audit.log.1 .. audit.log.3
|
|
8
13
|
export class AuditLog {
|
|
9
14
|
filePath;
|
|
10
15
|
logger;
|
|
11
16
|
buffer = [];
|
|
12
17
|
bufferLimit = 100; // Keep last N entries in memory for fast reads
|
|
13
|
-
|
|
18
|
+
maxBytes;
|
|
19
|
+
keepFiles;
|
|
20
|
+
curBytes = 0; // running size of the live file (avoids stat() per write)
|
|
21
|
+
constructor(filePath, opts) {
|
|
14
22
|
this.filePath = filePath;
|
|
15
23
|
this.logger = new Logger({ prefix: "AuditLog" });
|
|
24
|
+
this.maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
25
|
+
this.keepFiles = opts?.keepFiles ?? DEFAULT_KEEP_FILES;
|
|
16
26
|
// Ensure parent dir exists
|
|
17
27
|
const dir = dirname(filePath);
|
|
18
28
|
mkdirSync(dir, { recursive: true });
|
|
29
|
+
// Seed the byte counter from any existing file so a daemon that
|
|
30
|
+
// restarts onto an already-large log rotates promptly instead of
|
|
31
|
+
// letting it grow further.
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(this.filePath))
|
|
34
|
+
this.curBytes = statSync(this.filePath).size;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
this.curBytes = 0;
|
|
38
|
+
}
|
|
19
39
|
}
|
|
20
40
|
/**
|
|
21
41
|
* Log an access attempt (allowed or denied)
|
|
@@ -134,11 +154,48 @@ export class AuditLog {
|
|
|
134
154
|
this.buffer = this.buffer.slice(-this.bufferLimit);
|
|
135
155
|
}
|
|
136
156
|
// Append to file (JSONL)
|
|
157
|
+
const line = JSON.stringify(entry) + "\n";
|
|
137
158
|
try {
|
|
138
|
-
|
|
159
|
+
// Rotate BEFORE the write that would push us over the cap, so the
|
|
160
|
+
// live file never exceeds maxBytes by more than one line.
|
|
161
|
+
if (this.curBytes + Buffer.byteLength(line) > this.maxBytes) {
|
|
162
|
+
this.rotate();
|
|
163
|
+
}
|
|
164
|
+
appendFileSync(this.filePath, line, "utf-8");
|
|
165
|
+
this.curBytes += Buffer.byteLength(line);
|
|
139
166
|
}
|
|
140
167
|
catch (error) {
|
|
141
168
|
this.logger.error(`Failed to write audit entry: ${error}`);
|
|
142
169
|
}
|
|
143
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Size-based rotation: audit.log -> audit.log.1 -> ... -> audit.log.N,
|
|
173
|
+
* dropping the oldest. Bounds total on-disk audit data to
|
|
174
|
+
* maxBytes * (keepFiles + 1). Best-effort — a failure here must never
|
|
175
|
+
* break the daemon, so errors are logged and writing continues.
|
|
176
|
+
*/
|
|
177
|
+
rotate() {
|
|
178
|
+
try {
|
|
179
|
+
// Drop the oldest, then shift each backup up by one.
|
|
180
|
+
const oldest = `${this.filePath}.${this.keepFiles}`;
|
|
181
|
+
if (existsSync(oldest))
|
|
182
|
+
rmSync(oldest, { force: true });
|
|
183
|
+
for (let i = this.keepFiles - 1; i >= 1; i--) {
|
|
184
|
+
const src = `${this.filePath}.${i}`;
|
|
185
|
+
if (existsSync(src))
|
|
186
|
+
renameSync(src, `${this.filePath}.${i + 1}`);
|
|
187
|
+
}
|
|
188
|
+
if (existsSync(this.filePath))
|
|
189
|
+
renameSync(this.filePath, `${this.filePath}.1`);
|
|
190
|
+
this.curBytes = 0;
|
|
191
|
+
this.logger.info(`Rotated audit log (cap ${Math.round(this.maxBytes / 1048576)} MB, keep ${this.keepFiles})`);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
// Couldn't rotate (permissions, races). Don't let the file grow
|
|
195
|
+
// unbounded: reset the counter so we retry on the next write, but
|
|
196
|
+
// keep the daemon running.
|
|
197
|
+
this.logger.error(`Audit log rotation failed: ${error}`);
|
|
198
|
+
this.curBytes = 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
144
201
|
}
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -81,6 +81,34 @@ export declare function cmdFriendRequest(args: {
|
|
|
81
81
|
configDir?: string;
|
|
82
82
|
waitMs?: number;
|
|
83
83
|
}): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Send friend requests to MANY addresses in a single Carrier session.
|
|
86
|
+
*
|
|
87
|
+
* The expensive parts of a standalone friend-request — start, joinNetwork,
|
|
88
|
+
* announceSelf, and the relay-delivery wait — are PER-SESSION, not
|
|
89
|
+
* per-recipient. Sending N requests by calling cmdFriendRequest N times
|
|
90
|
+
* pays them N times (~25s each). This opens the peer once, joins/announces
|
|
91
|
+
* once, fires all requests, waits once, and stops once — so friending 4
|
|
92
|
+
* doras costs about the same as friending 1 (~25s instead of ~100s).
|
|
93
|
+
*
|
|
94
|
+
* Used by `agentnet init` to friend all federated default doras. Returns
|
|
95
|
+
* per-address ok/error so the caller can report which landed. If the
|
|
96
|
+
* daemon is already running, each request is routed via IPC (no peer).
|
|
97
|
+
*/
|
|
98
|
+
export declare function cmdFriendRequestMany(args: {
|
|
99
|
+
targets: {
|
|
100
|
+
address: string;
|
|
101
|
+
label: string;
|
|
102
|
+
}[];
|
|
103
|
+
hello?: string;
|
|
104
|
+
configDir?: string;
|
|
105
|
+
waitMs?: number;
|
|
106
|
+
}): Promise<{
|
|
107
|
+
label: string;
|
|
108
|
+
address: string;
|
|
109
|
+
ok: boolean;
|
|
110
|
+
error?: string;
|
|
111
|
+
}[]>;
|
|
84
112
|
/**
|
|
85
113
|
* Accept a pending friend request.
|
|
86
114
|
* Run while daemon is DOWN — opens a temporary peer, accepts, exits.
|
package/dist/cli/commands.js
CHANGED
|
@@ -139,21 +139,27 @@ export async function cmdInit(args) {
|
|
|
139
139
|
// them gives redundancy: any one staying up is enough to get an IP.
|
|
140
140
|
const configuredUserids = new Set(config.dora?.userids ?? []);
|
|
141
141
|
const dorasToFriend = DEFAULT_DORAS.filter((d) => configuredUserids.has(d.userid));
|
|
142
|
-
|
|
143
|
-
console.log(`\nFriending ${
|
|
142
|
+
if (dorasToFriend.length > 0) {
|
|
143
|
+
console.log(`\nFriending ${dorasToFriend.length} default dora${dorasToFriend.length > 1 ? "s" : ""} (${dorasToFriend
|
|
144
|
+
.map((d) => d.name)
|
|
145
|
+
.join(", ")}) in one Carrier session so the daemon can join the shared network on first start.`);
|
|
144
146
|
try {
|
|
145
|
-
await
|
|
146
|
-
address:
|
|
147
|
+
const results = await cmdFriendRequestMany({
|
|
148
|
+
targets: dorasToFriend.map((d) => ({ address: d.address, label: d.name })),
|
|
147
149
|
hello: `decentlan init (${nodeName})`,
|
|
148
150
|
waitMs: 8000,
|
|
149
151
|
configDir: dir,
|
|
150
152
|
});
|
|
151
|
-
|
|
153
|
+
const failed = results.filter((r) => !r.ok);
|
|
154
|
+
for (const f of failed) {
|
|
155
|
+
console.warn(` ${f.label} failed: ${f.error} — re-run later: agentnet dora enable --address ${f.address}`);
|
|
156
|
+
}
|
|
157
|
+
console.log(` Friended ${results.length - failed.length}/${results.length} doras. Auto-accept on their side takes it from here (you only need one to get an IP).`);
|
|
152
158
|
}
|
|
153
159
|
catch (err) {
|
|
154
160
|
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
-
console.warn(` Friend-
|
|
156
|
-
console.warn(` Re-run later: agentnet
|
|
161
|
+
console.warn(` Friend-requests failed: ${msg}`);
|
|
162
|
+
console.warn(` Re-run later with: agentnet up --real-tun (the daemon retries joining a dora on start).`);
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
165
|
console.log(`\nNext: sudo agentnet service install # or 'agentnet up --real-tun' to run in foreground`);
|
|
@@ -476,6 +482,78 @@ export async function cmdFriendRequest(args) {
|
|
|
476
482
|
console.log(`pick it up on next start (and still auto-accept). Only run 'agentnet friend-accept --pubkey ${myPubkey}'`);
|
|
477
483
|
console.log(`manually if their daemon is down AND they've disabled autoAccept.`);
|
|
478
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Send friend requests to MANY addresses in a single Carrier session.
|
|
487
|
+
*
|
|
488
|
+
* The expensive parts of a standalone friend-request — start, joinNetwork,
|
|
489
|
+
* announceSelf, and the relay-delivery wait — are PER-SESSION, not
|
|
490
|
+
* per-recipient. Sending N requests by calling cmdFriendRequest N times
|
|
491
|
+
* pays them N times (~25s each). This opens the peer once, joins/announces
|
|
492
|
+
* once, fires all requests, waits once, and stops once — so friending 4
|
|
493
|
+
* doras costs about the same as friending 1 (~25s instead of ~100s).
|
|
494
|
+
*
|
|
495
|
+
* Used by `agentnet init` to friend all federated default doras. Returns
|
|
496
|
+
* per-address ok/error so the caller can report which landed. If the
|
|
497
|
+
* daemon is already running, each request is routed via IPC (no peer).
|
|
498
|
+
*/
|
|
499
|
+
export async function cmdFriendRequestMany(args) {
|
|
500
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
501
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
502
|
+
const results = [];
|
|
503
|
+
// Daemon up → route each via IPC (no second peer, no announce cost).
|
|
504
|
+
const livePid = daemonPid(config);
|
|
505
|
+
if (livePid !== null) {
|
|
506
|
+
console.log(`Daemon is running (pid ${livePid}); routing friend-requests via IPC...`);
|
|
507
|
+
for (const t of args.targets) {
|
|
508
|
+
const res = await ipcCall(config, { op: "friend-request", address: t.address, hello: args.hello });
|
|
509
|
+
results.push({ label: t.label, address: t.address, ok: res.ok, error: res.ok ? undefined : res.error });
|
|
510
|
+
console.log(res.ok ? ` ✓ ${t.label}` : ` ✗ ${t.label}: ${res.error}`);
|
|
511
|
+
}
|
|
512
|
+
return results;
|
|
513
|
+
}
|
|
514
|
+
// Daemon down → one standalone peer session for ALL requests.
|
|
515
|
+
const { Peer } = await import("@decentnetwork/peer");
|
|
516
|
+
const keyFile = resolve(config.carrier.dataDir, "keypair.json");
|
|
517
|
+
console.log(`Opening peer with identity at ${keyFile}...`);
|
|
518
|
+
const peer = await Peer.create({
|
|
519
|
+
keyFile,
|
|
520
|
+
compatibilityMode: "legacy",
|
|
521
|
+
bootstrapNodes: config.carrier.bootstrapNodes,
|
|
522
|
+
expressNodes: config.carrier.expressNodes,
|
|
523
|
+
});
|
|
524
|
+
await peer.start();
|
|
525
|
+
console.log(`My address: ${peer.address()}`);
|
|
526
|
+
console.log(`My pubkey: ${peer.pubkey()}`);
|
|
527
|
+
console.log(`Joining Carrier network...`);
|
|
528
|
+
const joinResult = await peer.joinNetwork();
|
|
529
|
+
console.log(`Joined via ${joinResult.respondingNode.host}:${joinResult.respondingNode.port}`);
|
|
530
|
+
console.log(`Announcing self (15s)...`);
|
|
531
|
+
await peer.announceSelf(15000).catch((err) => {
|
|
532
|
+
console.warn(`Self-announce failed: ${err.message}`);
|
|
533
|
+
});
|
|
534
|
+
// Fire all requests on the one announced session.
|
|
535
|
+
for (const t of args.targets) {
|
|
536
|
+
try {
|
|
537
|
+
console.log(`Sending friend request to ${t.label} (${t.address.slice(0, 16)}...)...`);
|
|
538
|
+
await peer.sendFriendRequest(t.address, args.hello || "Decent AgentNet friend request");
|
|
539
|
+
results.push({ label: t.label, address: t.address, ok: true });
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
543
|
+
console.warn(` Failed to send to ${t.label}: ${msg}`);
|
|
544
|
+
results.push({ label: t.label, address: t.address, ok: false, error: msg });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Single shared wait for relay delivery of all requests above.
|
|
548
|
+
const waitMs = args.waitMs ?? 8000;
|
|
549
|
+
console.log(`Waiting ${waitMs}ms for relay delivery...`);
|
|
550
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
551
|
+
const myUserid = peer.userid();
|
|
552
|
+
await peer.stop();
|
|
553
|
+
console.log(`\nFriend requests sent. Recipients running with autoAccept (the default) have already`);
|
|
554
|
+
console.log(`accepted — they can confirm with 'agentnet diag' and look for userid ${myUserid}.`);
|
|
555
|
+
return results;
|
|
556
|
+
}
|
|
479
557
|
/**
|
|
480
558
|
* Accept a pending friend request.
|
|
481
559
|
* Run while daemon is DOWN — opens a temporary peer, accepts, exits.
|
package/dist/daemon/server.js
CHANGED
|
@@ -551,12 +551,19 @@ export class DaemonServer {
|
|
|
551
551
|
allowHosts: this.config.proxy.allowHosts ?? [],
|
|
552
552
|
allowConnectPorts: this.config.proxy.allowConnectPorts,
|
|
553
553
|
resolvePeerName: (srcIp) => this.ipam.resolveIp(srcIp)?.name,
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
554
|
+
// Per-tunnel audit is opt-in: on a busy exit it floods the
|
|
555
|
+
// log (the 13 GB audit.log bug). Off by default; ACL and
|
|
556
|
+
// connect events are still always audited.
|
|
557
|
+
onTunnelOpen: this.config.proxy.auditTunnels
|
|
558
|
+
? ({ src, srcName, target }) => {
|
|
559
|
+
this.auditLog.logProxyOpen({ srcIp: src, srcName, target });
|
|
560
|
+
}
|
|
561
|
+
: undefined,
|
|
562
|
+
onTunnelClose: this.config.proxy.auditTunnels
|
|
563
|
+
? ({ src, srcName, target, bytesTransferred }) => {
|
|
564
|
+
this.auditLog.logProxyClose({ srcIp: src, srcName, target, bytesTransferred });
|
|
565
|
+
}
|
|
566
|
+
: undefined,
|
|
560
567
|
});
|
|
561
568
|
await this.connectProxy.start();
|
|
562
569
|
this.logger.info(`Proxy listening on ${tunIp}:${this.config.proxy.port}`);
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
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",
|