@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.
- package/LICENSE +31 -0
- package/README.md +296 -0
- package/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/acl/acl-engine.d.ts +43 -0
- package/dist/acl/acl-engine.js +189 -0
- package/dist/acl/audit.d.ts +70 -0
- package/dist/acl/audit.js +144 -0
- package/dist/acl/index.d.ts +4 -0
- package/dist/acl/index.js +3 -0
- package/dist/acl/policy.d.ts +31 -0
- package/dist/acl/policy.js +102 -0
- package/dist/acl/types.d.ts +18 -0
- package/dist/acl/types.js +4 -0
- package/dist/carrier/frame.d.ts +18 -0
- package/dist/carrier/frame.js +66 -0
- package/dist/carrier/index.d.ts +5 -0
- package/dist/carrier/index.js +4 -0
- package/dist/carrier/packet-session.d.ts +32 -0
- package/dist/carrier/packet-session.js +151 -0
- package/dist/carrier/peer-manager.d.ts +113 -0
- package/dist/carrier/peer-manager.js +392 -0
- package/dist/carrier/types.d.ts +10 -0
- package/dist/carrier/types.js +11 -0
- package/dist/cli/commands.d.ts +223 -0
- package/dist/cli/commands.js +932 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +196 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +152 -0
- package/dist/daemon/index.d.ts +1 -0
- package/dist/daemon/index.js +1 -0
- package/dist/daemon/ipc.d.ts +60 -0
- package/dist/daemon/ipc.js +144 -0
- package/dist/daemon/server.d.ts +63 -0
- package/dist/daemon/server.js +510 -0
- package/dist/dns/index.d.ts +1 -0
- package/dist/dns/index.js +1 -0
- package/dist/dns/resolver.d.ts +44 -0
- package/dist/dns/resolver.js +82 -0
- package/dist/dns/server.d.ts +70 -0
- package/dist/dns/server.js +393 -0
- package/dist/dora/dora-integration.d.ts +90 -0
- package/dist/dora/dora-integration.js +325 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +15 -0
- package/dist/ipam/index.d.ts +1 -0
- package/dist/ipam/index.js +1 -0
- package/dist/ipam/ipam.d.ts +99 -0
- package/dist/ipam/ipam.js +254 -0
- package/dist/proxy/connect-proxy.d.ts +78 -0
- package/dist/proxy/connect-proxy.js +204 -0
- package/dist/router/index.d.ts +5 -0
- package/dist/router/index.js +4 -0
- package/dist/router/ip-parser.d.ts +36 -0
- package/dist/router/ip-parser.js +127 -0
- package/dist/router/packet-router.d.ts +49 -0
- package/dist/router/packet-router.js +251 -0
- package/dist/router/session-manager.d.ts +50 -0
- package/dist/router/session-manager.js +138 -0
- package/dist/router/types.d.ts +21 -0
- package/dist/router/types.js +6 -0
- package/dist/tun/index.d.ts +3 -0
- package/dist/tun/index.js +2 -0
- package/dist/tun/route-manager.d.ts +59 -0
- package/dist/tun/route-manager.js +353 -0
- package/dist/tun/tun-device.d.ts +45 -0
- package/dist/tun/tun-device.js +265 -0
- package/dist/tun/types.d.ts +28 -0
- package/dist/tun/types.js +4 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.js +4 -0
- package/dist/utils/logger.d.ts +20 -0
- package/dist/utils/logger.js +43 -0
- package/docs/CONFIGURATION.md +197 -0
- package/docs/INSTALL.md +145 -0
- 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
|
+
}
|