@decentnetwork/lan 0.1.12 → 0.1.14
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/cli/index.js +14 -4
- package/dist/router/packet-router.d.ts +11 -0
- package/dist/router/packet-router.js +49 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -126,21 +126,31 @@ async function main() {
|
|
|
126
126
|
configDir: argv["config-dir"],
|
|
127
127
|
});
|
|
128
128
|
})
|
|
129
|
-
|
|
129
|
+
// `friends X` MUST be defined as a nested subcommand tree, not as
|
|
130
|
+
// four flat `.command("friends X", ...)` entries — yargs treats the
|
|
131
|
+
// second word as a positional in that flat form and the
|
|
132
|
+
// last-registered subcommand silently swallows the others.
|
|
133
|
+
// Symptom: `agentnet friends pending` would run `friends reject` and
|
|
134
|
+
// complain about a missing --userid.
|
|
135
|
+
.command("friends", "Manage Carrier friends (run 'agentnet friends --help')", (y) => y
|
|
136
|
+
.command("list", "List Carrier friends (daemon must be down)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
130
137
|
await cmdFriendsList({ configDir: argv["config-dir"] });
|
|
131
138
|
})
|
|
132
|
-
.command("
|
|
139
|
+
.command("pending", "List queued friend-requests (over IPC; daemon must be up)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
133
140
|
await cmdFriendsPending({ configDir: argv["config-dir"] });
|
|
134
141
|
})
|
|
135
|
-
.command("
|
|
142
|
+
.command("accept", "Accept a queued friend-request by userid (over IPC; daemon stays up)", (yy) => yy
|
|
136
143
|
.option("userid", { type: "string", demandOption: true, describe: "Sender's Carrier userid (base58)" })
|
|
137
144
|
.option("config-dir", { type: "string" }), async (argv) => {
|
|
138
145
|
await cmdFriendsAccept({ userid: argv.userid, configDir: argv["config-dir"] });
|
|
139
146
|
})
|
|
140
|
-
.command("
|
|
147
|
+
.command("reject", "Drop a queued friend-request by userid (over IPC; daemon stays up)", (yy) => yy
|
|
141
148
|
.option("userid", { type: "string", demandOption: true })
|
|
142
149
|
.option("config-dir", { type: "string" }), async (argv) => {
|
|
143
150
|
await cmdFriendsReject({ userid: argv.userid, configDir: argv["config-dir"] });
|
|
151
|
+
})
|
|
152
|
+
.demandCommand(1, "Specify a friends subcommand (run 'agentnet friends --help')"), () => {
|
|
153
|
+
// parent handler — never invoked because demandCommand above
|
|
144
154
|
})
|
|
145
155
|
.command("audit log", "View audit log", (y) => y.option("tail", { type: "number", default: 50 }).option("config-dir", { type: "string" }), async (argv) => {
|
|
146
156
|
await cmdAuditLog({ tail: argv.tail, configDir: argv["config-dir"] });
|
|
@@ -20,6 +20,7 @@ export interface PacketRouterOptions {
|
|
|
20
20
|
export declare class PacketRouter extends EventEmitter {
|
|
21
21
|
private tunDevice;
|
|
22
22
|
private peerManager;
|
|
23
|
+
private ipam;
|
|
23
24
|
private acl;
|
|
24
25
|
private sessionManager;
|
|
25
26
|
private logger;
|
|
@@ -54,6 +55,16 @@ export declare class PacketRouter extends EventEmitter {
|
|
|
54
55
|
*/
|
|
55
56
|
private sendKeepalivesToActiveSessions;
|
|
56
57
|
getStats(): ForwardingStats;
|
|
58
|
+
/**
|
|
59
|
+
* Add (or correct) a (carrierId, virtualIp) mapping in IPAM based on
|
|
60
|
+
* an authenticated inbound packet. Idempotent and cheap — does
|
|
61
|
+
* nothing when the mapping already matches.
|
|
62
|
+
*
|
|
63
|
+
* The IP can change for a single peer (dora re-allocation, daemon
|
|
64
|
+
* restart with a different fallback IP) so we always overwrite on
|
|
65
|
+
* mismatch rather than first-write-wins.
|
|
66
|
+
*/
|
|
67
|
+
private learnPeerMapping;
|
|
57
68
|
/**
|
|
58
69
|
* Record a drop and bump per-destination diagnostics. Logs the FIRST
|
|
59
70
|
* occurrence of each (dstIp, reason) at info level so the failure mode
|
|
@@ -9,9 +9,11 @@ import { EventEmitter } from "events";
|
|
|
9
9
|
import { IpParser } from "./ip-parser.js";
|
|
10
10
|
import { SessionManager } from "./session-manager.js";
|
|
11
11
|
import { Logger } from "../utils/logger.js";
|
|
12
|
+
const SUBNET_PREFIX = "10.86.";
|
|
12
13
|
export class PacketRouter extends EventEmitter {
|
|
13
14
|
tunDevice;
|
|
14
15
|
peerManager;
|
|
16
|
+
ipam;
|
|
15
17
|
acl;
|
|
16
18
|
sessionManager;
|
|
17
19
|
logger;
|
|
@@ -36,6 +38,7 @@ export class PacketRouter extends EventEmitter {
|
|
|
36
38
|
super();
|
|
37
39
|
this.tunDevice = opts.tunDevice;
|
|
38
40
|
this.peerManager = opts.peerManager;
|
|
41
|
+
this.ipam = opts.ipam;
|
|
39
42
|
this.acl = opts.acl;
|
|
40
43
|
this.sessionManager = new SessionManager({
|
|
41
44
|
peerManager: opts.peerManager,
|
|
@@ -154,6 +157,33 @@ export class PacketRouter extends EventEmitter {
|
|
|
154
157
|
dropsByDst,
|
|
155
158
|
};
|
|
156
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Add (or correct) a (carrierId, virtualIp) mapping in IPAM based on
|
|
162
|
+
* an authenticated inbound packet. Idempotent and cheap — does
|
|
163
|
+
* nothing when the mapping already matches.
|
|
164
|
+
*
|
|
165
|
+
* The IP can change for a single peer (dora re-allocation, daemon
|
|
166
|
+
* restart with a different fallback IP) so we always overwrite on
|
|
167
|
+
* mismatch rather than first-write-wins.
|
|
168
|
+
*/
|
|
169
|
+
learnPeerMapping(srcPubkey, srcIp) {
|
|
170
|
+
const existing = this.ipam.resolveCarrierId(srcPubkey);
|
|
171
|
+
if (existing && existing.virtualIp === srcIp)
|
|
172
|
+
return;
|
|
173
|
+
// Skip our own pubkey — we already own our IPAM entry.
|
|
174
|
+
if (srcPubkey === this.peerManager.getPubkey())
|
|
175
|
+
return;
|
|
176
|
+
const name = existing?.name ?? `peer-${srcPubkey.slice(0, 8)}`;
|
|
177
|
+
this.ipam.assignPeer({
|
|
178
|
+
name,
|
|
179
|
+
carrierId: srcPubkey,
|
|
180
|
+
virtualIp: srcIp,
|
|
181
|
+
services: existing?.services ?? [],
|
|
182
|
+
});
|
|
183
|
+
this.logger.info(existing
|
|
184
|
+
? `IPAM updated from inbound: ${name} ${existing.virtualIp} -> ${srcIp}`
|
|
185
|
+
: `IPAM learned from inbound: ${name} (${srcPubkey.slice(0, 12)}...) -> ${srcIp}`);
|
|
186
|
+
}
|
|
157
187
|
/**
|
|
158
188
|
* Record a drop and bump per-destination diagnostics. Logs the FIRST
|
|
159
189
|
* occurrence of each (dstIp, reason) at info level so the failure mode
|
|
@@ -253,6 +283,25 @@ export class PacketRouter extends EventEmitter {
|
|
|
253
283
|
this.logger.debug("Dropped invalid incoming IP packet");
|
|
254
284
|
return;
|
|
255
285
|
}
|
|
286
|
+
// Auto-learn (srcPubkey -> srcIp) into IPAM. Without this, ubuntu
|
|
287
|
+
// can receive cn's ICMP fine but the reply path fails: ubuntu's
|
|
288
|
+
// outbound handleOutgoingPacket calls ipam.resolveIp("10.86.1.15")
|
|
289
|
+
// which returns null when ubuntu's dora roster sync is broken
|
|
290
|
+
// (dora friend offline, no other source for cn's virtualIp).
|
|
291
|
+
//
|
|
292
|
+
// We trust the (srcPubkey, srcIp) pairing for two reasons:
|
|
293
|
+
// 1. srcPubkey is Carrier-authenticated — only the real cn can
|
|
294
|
+
// open a PacketSession against ubuntu under cn's userid.
|
|
295
|
+
// 2. The only damage a lying sender could cause is shadowing
|
|
296
|
+
// their OWN reply routing — the IP is the destination for
|
|
297
|
+
// packets WE send back to THEM, no third party affected.
|
|
298
|
+
//
|
|
299
|
+
// Scope to the agentnet subnet (10.86.0.0/16) so a buggy peer
|
|
300
|
+
// leaking RFC1918 traffic through the TUN can't pollute our IPAM
|
|
301
|
+
// with 192.168.x.x entries.
|
|
302
|
+
if (parsed.srcIp.startsWith(SUBNET_PREFIX)) {
|
|
303
|
+
this.learnPeerMapping(srcPubkey, parsed.srcIp);
|
|
304
|
+
}
|
|
256
305
|
const proto = IpParser.protoName(parsed.protocol);
|
|
257
306
|
// ACL check (inbound)
|
|
258
307
|
if (parsed.dstPort !== undefined && (proto === "tcp" || proto === "udp")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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",
|