@decentnetwork/lan 0.1.44 → 0.1.45

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.
@@ -8,7 +8,13 @@ export declare class AuditLog {
8
8
  private logger;
9
9
  private buffer;
10
10
  private bufferLimit;
11
- constructor(filePath: string);
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
- constructor(filePath) {
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
- appendFileSync(this.filePath, JSON.stringify(entry) + "\n", "utf-8");
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
  }
@@ -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
- onTunnelOpen: ({ src, srcName, target }) => {
555
- this.auditLog.logProxyOpen({ srcIp: src, srcName, target });
556
- },
557
- onTunnelClose: ({ src, srcName, target, bytesTransferred }) => {
558
- this.auditLog.logProxyClose({ srcIp: src, srcName, target, bytesTransferred });
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
@@ -129,6 +129,7 @@ export interface ProxyConfig {
129
129
  port: number;
130
130
  allowHosts?: string[];
131
131
  allowConnectPorts?: number[];
132
+ auditTunnels?: boolean;
132
133
  }
133
134
  export interface FriendsConfig {
134
135
  /** When true (default), the running daemon auto-accepts incoming
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
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",