@decentnetwork/lan 0.1.0

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.
Files changed (79) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +296 -0
  3. package/bin/tun-helper-darwin-amd64 +0 -0
  4. package/bin/tun-helper-darwin-arm64 +0 -0
  5. package/bin/tun-helper-linux-amd64 +0 -0
  6. package/bin/tun-helper-linux-arm64 +0 -0
  7. package/dist/acl/acl-engine.d.ts +43 -0
  8. package/dist/acl/acl-engine.js +189 -0
  9. package/dist/acl/audit.d.ts +70 -0
  10. package/dist/acl/audit.js +144 -0
  11. package/dist/acl/index.d.ts +4 -0
  12. package/dist/acl/index.js +3 -0
  13. package/dist/acl/policy.d.ts +31 -0
  14. package/dist/acl/policy.js +102 -0
  15. package/dist/acl/types.d.ts +18 -0
  16. package/dist/acl/types.js +4 -0
  17. package/dist/carrier/frame.d.ts +18 -0
  18. package/dist/carrier/frame.js +66 -0
  19. package/dist/carrier/index.d.ts +5 -0
  20. package/dist/carrier/index.js +4 -0
  21. package/dist/carrier/packet-session.d.ts +32 -0
  22. package/dist/carrier/packet-session.js +151 -0
  23. package/dist/carrier/peer-manager.d.ts +113 -0
  24. package/dist/carrier/peer-manager.js +392 -0
  25. package/dist/carrier/types.d.ts +10 -0
  26. package/dist/carrier/types.js +11 -0
  27. package/dist/cli/commands.d.ts +223 -0
  28. package/dist/cli/commands.js +932 -0
  29. package/dist/cli/index.d.ts +7 -0
  30. package/dist/cli/index.js +196 -0
  31. package/dist/config/loader.d.ts +10 -0
  32. package/dist/config/loader.js +152 -0
  33. package/dist/daemon/index.d.ts +1 -0
  34. package/dist/daemon/index.js +1 -0
  35. package/dist/daemon/ipc.d.ts +60 -0
  36. package/dist/daemon/ipc.js +144 -0
  37. package/dist/daemon/server.d.ts +63 -0
  38. package/dist/daemon/server.js +510 -0
  39. package/dist/dns/index.d.ts +1 -0
  40. package/dist/dns/index.js +1 -0
  41. package/dist/dns/resolver.d.ts +44 -0
  42. package/dist/dns/resolver.js +82 -0
  43. package/dist/dns/server.d.ts +70 -0
  44. package/dist/dns/server.js +393 -0
  45. package/dist/dora/dora-integration.d.ts +90 -0
  46. package/dist/dora/dora-integration.js +325 -0
  47. package/dist/index.d.ts +13 -0
  48. package/dist/index.js +15 -0
  49. package/dist/ipam/index.d.ts +1 -0
  50. package/dist/ipam/index.js +1 -0
  51. package/dist/ipam/ipam.d.ts +99 -0
  52. package/dist/ipam/ipam.js +254 -0
  53. package/dist/proxy/connect-proxy.d.ts +78 -0
  54. package/dist/proxy/connect-proxy.js +204 -0
  55. package/dist/router/index.d.ts +5 -0
  56. package/dist/router/index.js +4 -0
  57. package/dist/router/ip-parser.d.ts +36 -0
  58. package/dist/router/ip-parser.js +127 -0
  59. package/dist/router/packet-router.d.ts +49 -0
  60. package/dist/router/packet-router.js +251 -0
  61. package/dist/router/session-manager.d.ts +50 -0
  62. package/dist/router/session-manager.js +138 -0
  63. package/dist/router/types.d.ts +21 -0
  64. package/dist/router/types.js +6 -0
  65. package/dist/tun/index.d.ts +3 -0
  66. package/dist/tun/index.js +2 -0
  67. package/dist/tun/route-manager.d.ts +59 -0
  68. package/dist/tun/route-manager.js +353 -0
  69. package/dist/tun/tun-device.d.ts +45 -0
  70. package/dist/tun/tun-device.js +265 -0
  71. package/dist/tun/types.d.ts +28 -0
  72. package/dist/tun/types.js +4 -0
  73. package/dist/types.d.ts +176 -0
  74. package/dist/types.js +4 -0
  75. package/dist/utils/logger.d.ts +20 -0
  76. package/dist/utils/logger.js +43 -0
  77. package/docs/CONFIGURATION.md +197 -0
  78. package/docs/INSTALL.md +145 -0
  79. package/package.json +93 -0
package/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ This package is licensed under the GNU General Public License v3.0 or any
9
+ later version. You may redistribute and/or modify it under the terms of
10
+ the GNU General Public License as published by the Free Software
11
+ Foundation, either version 3 of the License, or (at your option) any
12
+ later version.
13
+
14
+ This program is distributed in the hope that it will be useful, but
15
+ WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
17
+ Public License for more details.
18
+
19
+ The full text of the GPL-3.0-or-later license is available at
20
+ <https://www.gnu.org/licenses/gpl-3.0.html>.
21
+
22
+ This package derives in part from the Elastos.NET.Carrier.Native.SDK,
23
+ which itself derives from the toxcore project — both also licensed under
24
+ GPL-3.0-or-later. The protocol-level parity with those upstreams is
25
+ intentional; the wire format implementations in `compat/` reference the
26
+ toxcore C source by file:line in their top comments.
27
+
28
+ - Elastos.NET.Carrier.Native.SDK:
29
+ https://github.com/elastos/Elastos.NET.Carrier.Native.SDK
30
+ - c-toxcore:
31
+ https://github.com/TokTok/c-toxcore
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # Decent AgentNet
2
+
3
+ > A private network layer for self-hosted AI agents, built on **Elastos Carrier**.
4
+
5
+ **Decent AgentNet** lets machines behind NAT, firewalls, or customer-controlled networks communicate securely with private virtual IPs (`10.86.0.0/16`), without requiring public IPs, port forwarding, or remote desktop access.
6
+
7
+ Perfect for:
8
+ - **Remote agent support** — Service providers accessing customer OpenClaw instances
9
+ - **Agent-to-agent communication** — Trading agents, AI agents, autonomous systems talking over private networks
10
+ - **Private dashboards & APIs** — HTTP endpoints accessible only to approved peers
11
+ - **Secure remote access** — SSH, RDP, and other protocols over encrypted private networks
12
+
13
+ ## Quick Start
14
+
15
+ ### Prerequisites
16
+
17
+ - **Node.js 20+**
18
+ - **Linux or macOS** (Windows support planned)
19
+ - **Carrier identity** (generate with `agentnet init`)
20
+
21
+ ### Installation
22
+
23
+ ```bash
24
+ git clone https://github.com/0xli/decentlan.git
25
+ cd decentlan
26
+ npm install
27
+ npm run build
28
+ ```
29
+
30
+ ### Basic Usage
31
+
32
+ **On Machine A (provider):**
33
+
34
+ ```bash
35
+ # Initialize identity and config
36
+ agentnet init --name provider-macbook
37
+
38
+ # Start daemon
39
+ sudo agentnet up --name provider-macbook
40
+ ```
41
+
42
+ **On Machine B (partner):**
43
+
44
+ ```bash
45
+ agentnet init --name partner-openclaw
46
+ sudo agentnet up --name partner-openclaw
47
+ ```
48
+
49
+ **Grant access from A to B:**
50
+
51
+ ```bash
52
+ # Assign virtual IP
53
+ agentnet ipam assign --peer <B-carrier-id> --ip 10.86.12.34 --name partner-openclaw
54
+
55
+ # Grant access to SSH (port 22) for 1 hour
56
+ agentnet grant --peer <B-carrier-id> --tcp 22 --expires 1h
57
+
58
+ # Grant access to OpenClaw gateway (port 18789)
59
+ agentnet grant --peer <B-carrier-id> --tcp 18789 --expires 24h
60
+ ```
61
+
62
+ **Use the network:**
63
+
64
+ ```bash
65
+ # SSH to partner's machine
66
+ ssh partner@10.86.12.34
67
+
68
+ # Access OpenClaw gateway
69
+ curl http://10.86.12.34:18789/health
70
+
71
+ # Any other TCP service
72
+ # (HTTP, databases, custom APIs, etc.)
73
+ ```
74
+
75
+ **Revoke access:**
76
+
77
+ ```bash
78
+ agentnet revoke --peer <B-carrier-id>
79
+ ```
80
+
81
+ ## Architecture
82
+
83
+ ```
84
+ Your App / Agent → TUN Virtual Interface (10.86.x.x)
85
+ → AgentNet Daemon (Routing, ACL, IPAM)
86
+ → Elastos Carrier (P2P Transport, Encryption, DHT, Relay)
87
+ → Remote Peer's AgentNet Daemon
88
+ → Remote TUN → Remote App / Agent
89
+ ```
90
+
91
+ No modifications to the Carrier protocol. We build an application layer on top of Carrier's encrypted P2P foundation.
92
+
93
+ ## Key Features
94
+
95
+ - **Identity-based** — Uses Carrier addresses (no central account system)
96
+ - **Private by default** — Explicit access grants required (ACL deny-all)
97
+ - **Time-limited access** — Support sessions can expire
98
+ - **Auditable** — All connection attempts logged
99
+ - **NAT-friendly** — Works behind any firewall (relies on Carrier's relay)
100
+ - **No public IP needed** — Pure peer-to-peer over Carrier
101
+
102
+ ## Configuration
103
+
104
+ Main config is at `~/.agentnet/config.yaml`:
105
+
106
+ ```yaml
107
+ node:
108
+ name: my-machine
109
+ namespace: agentnet-main
110
+
111
+ carrier:
112
+ data_dir: ~/.carrier
113
+ bootstrap_nodes:
114
+ - bootstrap1.decent.network
115
+ - bootstrap2.decent.network
116
+
117
+ network:
118
+ interface: agentnet0
119
+ ip: 10.86.1.10
120
+ subnet: 10.86.0.0/16
121
+ dns_domain: agentnet
122
+ dns_port: 5353
123
+ ```
124
+
125
+ Peer mappings are in `~/.agentnet/ipam.yaml`:
126
+
127
+ ```yaml
128
+ peers:
129
+ - name: partner-openclaw
130
+ carrier_id: "8Rkxxx..."
131
+ virtual_ip: 10.86.12.34
132
+ services:
133
+ - name: openclaw
134
+ proto: tcp
135
+ port: 18789
136
+ - name: ssh
137
+ proto: tcp
138
+ port: 22
139
+ ```
140
+
141
+ ACL rules are in `~/.agentnet/policy.yaml`. Audit logs are in `~/.agentnet/audit.log`.
142
+
143
+ ## Command Reference
144
+
145
+ ### Identity & Setup
146
+
147
+ ```bash
148
+ agentnet init # Create ~/.agentnet, generate keys
149
+ agentnet identity show # Display Carrier ID, address, pubkey
150
+ ```
151
+
152
+ ### Peer Management
153
+
154
+ ```bash
155
+ agentnet peers list # List known peers and status
156
+ agentnet ipam assign # Register peer with virtual IP
157
+ --peer <carrier-id>
158
+ --ip <virtual-ip>
159
+ --name <hostname>
160
+
161
+ agentnet resolve <hostname> # Resolve name to virtual IP
162
+ ```
163
+
164
+ ### Access Control
165
+
166
+ ```bash
167
+ agentnet grant # Grant access to peer
168
+ --peer <carrier-id>
169
+ --tcp <port> # or --udp
170
+ --expires <duration> # e.g., "1h", "24h", "7d"
171
+
172
+ agentnet revoke --peer <carrier-id> # Revoke all access
173
+
174
+ agentnet audit log # View audit trail
175
+ --tail <lines>
176
+ --since <time>
177
+ ```
178
+
179
+ ### Daemon Control
180
+
181
+ ```bash
182
+ agentnet up # Start daemon
183
+ --name <node-name>
184
+ --ipam <namespace>
185
+
186
+ agentnet down # Stop daemon
187
+
188
+ agentnet status # Show daemon status
189
+ ```
190
+
191
+ ### OpenClaw Integration
192
+
193
+ ```bash
194
+ agentnet openclaw status --target <name>.agentnet
195
+ agentnet openclaw logs --target <name>.agentnet --follow
196
+ agentnet openclaw diagnose --target <name>.agentnet
197
+ ```
198
+
199
+ ## How It Works
200
+
201
+ 1. **Carrier Friends** — Two nodes must be Carrier friends (manually established using Carrier tooling).
202
+
203
+ 2. **Virtual Network** — Each node runs a TUN interface (`agentnet0`) on the `10.86.0.0/16` subnet.
204
+
205
+ 3. **IP Mapping** — IPAM maps Carrier IDs to virtual IPs (e.g., `8Rkxxx...` → `10.86.12.34`).
206
+
207
+ 4. **Packet Forwarding** — When an app sends a packet to `10.86.12.34`, the daemon:
208
+ - Intercepts it from the TUN interface
209
+ - Looks up the destination peer (Carrier ID)
210
+ - Checks the ACL (is access allowed?)
211
+ - Frames the packet and sends it via Carrier
212
+ - The remote daemon receives it and writes it to its TUN
213
+
214
+ 5. **Access Control** — ACL rules control which peer can access which services/ports and for how long.
215
+
216
+ 6. **Audit Trail** — All access attempts (allowed and denied) are logged.
217
+
218
+ ## Security Model
219
+
220
+ - **Encryption** — All traffic is encrypted by Carrier (NaCl cryptography)
221
+ - **Authentication** — Carrier public key is the identity; friend relationships are the trust boundary
222
+ - **Authorization** — ACL rules grant explicit access; default is deny
223
+ - **Audit** — Connection attempts are logged for compliance and debugging
224
+
225
+ ## Limitations (MVP v0.1)
226
+
227
+ - Linux/macOS only (Windows in future)
228
+ - TCP only (UDP planned in v0.2)
229
+ - No remote desktop protocol support (TBD)
230
+ - No video/media optimization (rely on Carrier relay)
231
+ - Static IPAM (blockchain registry planned for v1.0)
232
+
233
+ ## Development
234
+
235
+ See [CLAUDE.md](./CLAUDE.md) for architecture, directory structure, and contributor guidelines.
236
+
237
+ ### Building from Source
238
+
239
+ ```bash
240
+ npm install
241
+ npm run build
242
+ npm run typecheck # Type-check without emitting
243
+ npm test # Run tests
244
+ npm run dev # Run in dev mode
245
+ ```
246
+
247
+ ### Testing
248
+
249
+ ```bash
250
+ npm test # All tests
251
+ npm test -- --watch # Watch mode
252
+ npm test -- --coverage # Coverage report
253
+ ```
254
+
255
+ ## Troubleshooting
256
+
257
+ ### TUN interface creation fails
258
+
259
+ If you get "Permission denied" when creating the TUN:
260
+
261
+ ```bash
262
+ sudo agentnet up --name my-machine
263
+ ```
264
+
265
+ The daemon needs `CAP_NET_ADMIN` to create the TUN interface.
266
+
267
+ ### Can't reach remote peer
268
+
269
+ 1. Check both daemons are running: `agentnet status`
270
+ 2. Verify you're Carrier friends: `agentnet peers list`
271
+ 3. Check virtual IP is in IPAM: `agentnet resolve <name>`
272
+ 4. Check ACL rules allow the port: `agentnet audit log`
273
+
274
+ ### Performance issues
275
+
276
+ - Carrier relay can add latency. Direct P2P paths are ideal.
277
+ - SSH is responsive even over relay.
278
+ - For low-latency, ensure both nodes have good network connectivity.
279
+
280
+ ## Support & Feedback
281
+
282
+ - **GitHub Issues** — https://github.com/0xli/decentlan/issues
283
+ - **Documentation** — See `docs/` directory
284
+ - **Discord** — (link TBD)
285
+
286
+ ## License
287
+
288
+ MIT
289
+
290
+ ## References
291
+
292
+ - **Elastos Carrier** — https://github.com/elastos/Elastos.NET.Carrier.Swift
293
+ - **Decent Network** — https://github.com/0xli/decent-network
294
+ - **Tailscale** — For VPN/LAN concepts
295
+ - **A2A Protocol** — Agent-to-agent communication
296
+ - **MCP** — Model Context Protocol for tool/AI integration
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,43 @@
1
+ /**
2
+ * ACL evaluation engine
3
+ * Decides whether a peer can access a destination IP+port
4
+ */
5
+ import type { Policy } from "./policy.js";
6
+ import type { AuditLog } from "./audit.js";
7
+ import type { Ipam } from "../ipam/ipam.js";
8
+ import type { AccessRequest, AccessResult } from "./types.js";
9
+ export interface AclEngineOptions {
10
+ policy: Policy;
11
+ ipam?: Ipam;
12
+ auditLog?: AuditLog;
13
+ }
14
+ export declare class AclEngine {
15
+ private policy;
16
+ private ipam?;
17
+ private auditLog?;
18
+ private logger;
19
+ constructor(opts: AclEngineOptions);
20
+ /**
21
+ * Evaluate an access request.
22
+ * Returns whether it's allowed and the reason.
23
+ */
24
+ evaluate(req: AccessRequest): AccessResult;
25
+ /**
26
+ * Add a grant for a peer (delegates to Policy)
27
+ */
28
+ grant(opts: {
29
+ peer: string;
30
+ ports: number[];
31
+ proto?: "tcp" | "udp" | "any";
32
+ expiresMs?: number;
33
+ purpose?: string;
34
+ direction?: "inbound" | "outbound" | "both";
35
+ }): void;
36
+ /**
37
+ * Revoke all grants for a peer
38
+ */
39
+ revoke(peer: string, reason?: string): boolean;
40
+ private evaluateInternal;
41
+ private matchesPermission;
42
+ private resolveHost;
43
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * ACL evaluation engine
3
+ * Decides whether a peer can access a destination IP+port
4
+ */
5
+ import { Logger } from "../utils/logger.js";
6
+ export class AclEngine {
7
+ policy;
8
+ ipam;
9
+ auditLog;
10
+ logger;
11
+ constructor(opts) {
12
+ this.policy = opts.policy;
13
+ this.ipam = opts.ipam;
14
+ this.auditLog = opts.auditLog;
15
+ this.logger = new Logger({ prefix: "AclEngine" });
16
+ }
17
+ /**
18
+ * Evaluate an access request.
19
+ * Returns whether it's allowed and the reason.
20
+ */
21
+ evaluate(req) {
22
+ const now = req.now || new Date();
23
+ const direction = req.direction;
24
+ const result = this.evaluateInternal(req, now, direction);
25
+ // Audit
26
+ if (this.auditLog) {
27
+ const peerName = this.ipam?.resolveCarrierId(req.srcPubkey)?.name;
28
+ this.auditLog.logAccess({
29
+ srcPubkey: req.srcPubkey,
30
+ srcName: peerName,
31
+ dstIp: req.dstIp,
32
+ dstPort: req.dstPort,
33
+ proto: req.proto,
34
+ allowed: result.allowed,
35
+ reason: result.reason,
36
+ });
37
+ }
38
+ return result;
39
+ }
40
+ /**
41
+ * Add a grant for a peer (delegates to Policy)
42
+ */
43
+ grant(opts) {
44
+ const proto = opts.proto || "tcp";
45
+ const direction = opts.direction || "inbound";
46
+ const expiresAt = opts.expiresMs ? Date.now() + opts.expiresMs : undefined;
47
+ const allow = opts.ports.map((port) => ({
48
+ proto,
49
+ port,
50
+ purpose: opts.purpose,
51
+ }));
52
+ const rule = {
53
+ peer: opts.peer,
54
+ direction,
55
+ allow,
56
+ expiresAt,
57
+ audit: true,
58
+ };
59
+ this.policy.addRule(rule);
60
+ this.logger.info(`Granted peer ${opts.peer} access to ${proto} ${opts.ports.join(",")} ${expiresAt ? `(expires in ${opts.expiresMs}ms)` : "(no expiration)"}`);
61
+ if (this.auditLog) {
62
+ this.auditLog.logGrant({
63
+ peer: opts.peer,
64
+ ports: opts.ports,
65
+ expiresAt,
66
+ purpose: opts.purpose,
67
+ });
68
+ }
69
+ }
70
+ /**
71
+ * Revoke all grants for a peer
72
+ */
73
+ revoke(peer, reason) {
74
+ const removed = this.policy.removePeer(peer);
75
+ if (removed) {
76
+ this.logger.info(`Revoked all access for peer ${peer}`);
77
+ if (this.auditLog) {
78
+ this.auditLog.logRevoke(peer, reason);
79
+ }
80
+ }
81
+ return removed;
82
+ }
83
+ evaluateInternal(req, now, direction) {
84
+ // The "peer" field on a rule is the *remote* peer — whoever you're
85
+ // talking with, not yourself. So which side of the packet identifies
86
+ // that remote peer depends on direction:
87
+ //
88
+ // INBOUND (packet arriving FROM a friend): the remote peer is the
89
+ // source. req.srcPubkey is their userid.
90
+ //
91
+ // OUTBOUND (packet going TO a friend): the remote peer is the
92
+ // destination. We don't have their userid directly — only their
93
+ // virtual IP — but IPAM gives us the carrierId for that IP.
94
+ //
95
+ // Without this, granting "peer=X --tcp 8888" only opens inbound from X;
96
+ // outbound to X gets default-denied because no rule matches our own
97
+ // pubkey. That manifests as: ICMP works (no ACL check), but TCP to a
98
+ // granted peer silently drops at the packet router.
99
+ const peerUserId = direction === "outbound"
100
+ ? this.ipam?.resolveIp(req.dstIp)?.carrierId
101
+ : req.srcPubkey;
102
+ const peerName = peerUserId
103
+ ? this.ipam?.resolveCarrierId(peerUserId)?.name
104
+ : undefined;
105
+ const rulesByUserId = peerUserId ? this.policy.getRulesForPeer(peerUserId) : [];
106
+ const rulesByName = peerName ? this.policy.getRulesForPeer(peerName) : [];
107
+ const allRules = [...rulesByUserId, ...rulesByName];
108
+ for (const rule of allRules) {
109
+ // Check direction
110
+ const ruleDirection = rule.direction || "inbound";
111
+ if (ruleDirection !== "both" && ruleDirection !== direction) {
112
+ continue;
113
+ }
114
+ // Check expiration
115
+ if (rule.expiresAt && now.getTime() > rule.expiresAt) {
116
+ continue;
117
+ }
118
+ // Check explicit deny first
119
+ if (rule.deny) {
120
+ for (const perm of rule.deny) {
121
+ if (this.matchesPermission(req, perm)) {
122
+ return {
123
+ allowed: false,
124
+ matchedRule: rule.peer,
125
+ reason: `denied by rule (peer: ${rule.peer}, ${perm.purpose || "no reason"})`,
126
+ };
127
+ }
128
+ }
129
+ }
130
+ // Check allow
131
+ if (rule.allow) {
132
+ for (const perm of rule.allow) {
133
+ if (this.matchesPermission(req, perm)) {
134
+ return {
135
+ allowed: true,
136
+ matchedRule: rule.peer,
137
+ reason: `allowed by rule (peer: ${rule.peer}, ${perm.purpose || "granted"})`,
138
+ };
139
+ }
140
+ }
141
+ }
142
+ }
143
+ // No matching rule, fall back to default action
144
+ const defaultAction = this.policy.getDefaultAction();
145
+ return {
146
+ allowed: defaultAction === "allow",
147
+ reason: `default policy: ${defaultAction}`,
148
+ };
149
+ }
150
+ matchesPermission(req, perm) {
151
+ // Protocol match
152
+ if (perm.proto !== "any" && perm.proto !== req.proto) {
153
+ return false;
154
+ }
155
+ // Port match. Allow if EITHER dstPort or srcPort matches the granted
156
+ // port. This is what makes a single "tcp:8888" grant cover an entire
157
+ // TCP conversation:
158
+ // client → server SYN: dstPort=8888 (matches)
159
+ // server → client SYN-ACK: srcPort=8888 (matches), dstPort=ephemeral
160
+ // server → client data: srcPort=8888 (matches), dstPort=ephemeral
161
+ // client → server data: dstPort=8888 (matches)
162
+ // Without the srcPort check, server replies were default-denied because
163
+ // their dst port was the client's ephemeral port (e.g. 54016), and the
164
+ // TCP handshake never completed.
165
+ if (perm.port !== undefined && perm.port !== req.dstPort && perm.port !== req.srcPort) {
166
+ return false;
167
+ }
168
+ // Host match (if specified)
169
+ if (perm.host) {
170
+ const hostIp = this.resolveHost(perm.host);
171
+ if (hostIp && hostIp !== req.dstIp) {
172
+ return false;
173
+ }
174
+ }
175
+ return true;
176
+ }
177
+ resolveHost(host) {
178
+ if (!this.ipam) {
179
+ return null;
180
+ }
181
+ // If looks like IP, return as-is
182
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
183
+ return host;
184
+ }
185
+ // Try to resolve as name (strip .agentnet suffix)
186
+ const name = host.replace(/\.agentnet$/, "");
187
+ return this.ipam.resolveName(name);
188
+ }
189
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Audit logger for ACL decisions and connection events
3
+ * Append-only JSONL file format
4
+ */
5
+ import type { AuditEntry } from "../types.js";
6
+ export declare class AuditLog {
7
+ private filePath;
8
+ private logger;
9
+ private buffer;
10
+ private bufferLimit;
11
+ constructor(filePath: string);
12
+ /**
13
+ * Log an access attempt (allowed or denied)
14
+ */
15
+ logAccess(opts: {
16
+ srcPubkey: string;
17
+ srcName?: string;
18
+ dstIp: string;
19
+ dstPort: number;
20
+ proto: "tcp" | "udp";
21
+ allowed: boolean;
22
+ reason?: string;
23
+ }): void;
24
+ /**
25
+ * Log a grant event
26
+ */
27
+ logGrant(opts: {
28
+ peer: string;
29
+ ports: number[];
30
+ expiresAt?: number;
31
+ purpose?: string;
32
+ }): void;
33
+ /**
34
+ * Log a revoke event
35
+ */
36
+ logRevoke(peer: string, reason?: string): void;
37
+ /**
38
+ * Log a connection event
39
+ */
40
+ logConnection(opts: {
41
+ srcPubkey: string;
42
+ type: "connect" | "disconnect";
43
+ }): void;
44
+ /**
45
+ * Log a CONNECT proxy tunnel opening.
46
+ */
47
+ logProxyOpen(opts: {
48
+ srcIp: string;
49
+ srcName?: string;
50
+ target: string;
51
+ }): void;
52
+ /**
53
+ * Log a CONNECT proxy tunnel closing.
54
+ */
55
+ logProxyClose(opts: {
56
+ srcIp: string;
57
+ srcName?: string;
58
+ target: string;
59
+ bytesTransferred: number;
60
+ }): void;
61
+ /**
62
+ * Read recent entries (from buffer + tail of file)
63
+ */
64
+ readRecent(limit?: number): AuditEntry[];
65
+ /**
66
+ * Get all entries since a timestamp
67
+ */
68
+ readSince(timestamp: number): AuditEntry[];
69
+ private write;
70
+ }