@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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point — yargs setup.
|
|
4
|
+
* EventEmitter.defaultMaxListeners is raised in src/index.ts BEFORE any
|
|
5
|
+
* imports here, so it's already in effect by the time the SDK loads.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from "events";
|
|
8
|
+
import yargs from "yargs";
|
|
9
|
+
import { hideBin } from "yargs/helpers";
|
|
10
|
+
// Belt-and-braces — also raise it here in case the CLI is run directly
|
|
11
|
+
// (e.g. `node dist/cli/index.js` rather than via dist/index.js).
|
|
12
|
+
EventEmitter.defaultMaxListeners = 100;
|
|
13
|
+
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDiag, cmdDnsInstall, cmdDnsHosts, } from "./commands.js";
|
|
14
|
+
async function main() {
|
|
15
|
+
await yargs(hideBin(process.argv))
|
|
16
|
+
.scriptName("agentnet")
|
|
17
|
+
.usage("Usage: $0 <command> [options]")
|
|
18
|
+
.command("init", "Initialize ~/.agentnet directory", (y) => y
|
|
19
|
+
.option("name", { type: "string", describe: "Node name" })
|
|
20
|
+
.option("config-dir", { type: "string", describe: "Config directory" }), async (argv) => {
|
|
21
|
+
await cmdInit({ name: argv.name, configDir: argv["config-dir"] });
|
|
22
|
+
})
|
|
23
|
+
.command("identity show", "Display Carrier identity", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
24
|
+
await cmdIdentityShow({ configDir: argv["config-dir"] });
|
|
25
|
+
})
|
|
26
|
+
.command("peers list", "List configured peers", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
27
|
+
await cmdPeersList({ configDir: argv["config-dir"] });
|
|
28
|
+
})
|
|
29
|
+
.command("ipam assign", "Register peer with virtual IP", (y) => y
|
|
30
|
+
.option("peer", { type: "string", demandOption: true, describe: "Carrier ID" })
|
|
31
|
+
.option("ip", { type: "string", describe: "Virtual IP (auto if omitted)" })
|
|
32
|
+
.option("name", { type: "string", demandOption: true, describe: "Hostname" })
|
|
33
|
+
.option("service", {
|
|
34
|
+
type: "array",
|
|
35
|
+
string: true,
|
|
36
|
+
describe: "Service spec (name:proto:port)",
|
|
37
|
+
})
|
|
38
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
39
|
+
await cmdIpamAssign({
|
|
40
|
+
peer: argv.peer,
|
|
41
|
+
ip: argv.ip,
|
|
42
|
+
name: argv.name,
|
|
43
|
+
services: argv.service,
|
|
44
|
+
configDir: argv["config-dir"],
|
|
45
|
+
});
|
|
46
|
+
})
|
|
47
|
+
.command("grant", "Grant access to a peer", (y) => y
|
|
48
|
+
.option("peer", { type: "string", demandOption: true })
|
|
49
|
+
.option("tcp", { type: "array", number: true, describe: "TCP ports" })
|
|
50
|
+
.option("udp", { type: "array", number: true, describe: "UDP ports" })
|
|
51
|
+
.option("expires", { type: "string", describe: "Duration (1h, 24h, 7d)" })
|
|
52
|
+
.option("purpose", { type: "string" })
|
|
53
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
54
|
+
await cmdGrant({
|
|
55
|
+
peer: argv.peer,
|
|
56
|
+
tcp: argv.tcp,
|
|
57
|
+
udp: argv.udp,
|
|
58
|
+
expires: argv.expires,
|
|
59
|
+
purpose: argv.purpose,
|
|
60
|
+
configDir: argv["config-dir"],
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
.command("revoke", "Revoke access from a peer", (y) => y
|
|
64
|
+
.option("peer", { type: "string", demandOption: true })
|
|
65
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
66
|
+
await cmdRevoke({ peer: argv.peer, configDir: argv["config-dir"] });
|
|
67
|
+
})
|
|
68
|
+
.command("resolve <name>", "Resolve hostname to virtual IP", (y) => y
|
|
69
|
+
.positional("name", { type: "string", demandOption: true })
|
|
70
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
71
|
+
await cmdResolve({ name: argv.name, configDir: argv["config-dir"] });
|
|
72
|
+
})
|
|
73
|
+
.command("status", "Show daemon status", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
74
|
+
await cmdStatus({ configDir: argv["config-dir"] });
|
|
75
|
+
})
|
|
76
|
+
.command("diag", "Query the running daemon over IPC for a JSON snapshot of its runtime state (stats, friends, IPAM)", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
77
|
+
await cmdDiag({ configDir: argv["config-dir"] });
|
|
78
|
+
})
|
|
79
|
+
.command("dns", "Manage OS-side DNS resolver wiring for the .decent zone", (y) => y
|
|
80
|
+
.command("install", "Route .decent queries to the local daemon (writes /etc/resolver on macOS or resolvectl on Linux)", (yy) => yy
|
|
81
|
+
.option("uninstall", { type: "boolean", default: false, describe: "Remove the resolver config" })
|
|
82
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
83
|
+
await cmdDnsInstall({ uninstall: argv.uninstall, configDir: argv["config-dir"] });
|
|
84
|
+
})
|
|
85
|
+
.command("hosts", "Print /etc/hosts-format entries for the current peer roster", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
86
|
+
await cmdDnsHosts({ configDir: argv["config-dir"] });
|
|
87
|
+
})
|
|
88
|
+
.demandCommand(1, "Specify a dns subcommand (run 'agentnet dns --help')"), () => {
|
|
89
|
+
// parent handler — never invoked because demandCommand
|
|
90
|
+
})
|
|
91
|
+
.command("up", "Start the daemon", (y) => y
|
|
92
|
+
.option("name", { type: "string" })
|
|
93
|
+
.option("real-tun", {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
default: false,
|
|
96
|
+
describe: "Use real TUN (needs root)",
|
|
97
|
+
})
|
|
98
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
99
|
+
await cmdUp({
|
|
100
|
+
name: argv.name,
|
|
101
|
+
realTun: argv["real-tun"],
|
|
102
|
+
configDir: argv["config-dir"],
|
|
103
|
+
});
|
|
104
|
+
})
|
|
105
|
+
.command("friend-request", "Send a friend request (run while daemon is down)", (y) => y
|
|
106
|
+
.option("address", { type: "string", demandOption: true, describe: "Recipient Carrier address" })
|
|
107
|
+
.option("hello", { type: "string", describe: "Greeting message" })
|
|
108
|
+
.option("wait-ms", { type: "number", default: 8000, describe: "Wait time for relay delivery" })
|
|
109
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
110
|
+
await cmdFriendRequest({
|
|
111
|
+
address: argv.address,
|
|
112
|
+
hello: argv.hello,
|
|
113
|
+
waitMs: argv["wait-ms"],
|
|
114
|
+
configDir: argv["config-dir"],
|
|
115
|
+
});
|
|
116
|
+
})
|
|
117
|
+
.command("friend-accept", "Accept a friend request (run while daemon is down)", (y) => y
|
|
118
|
+
.option("pubkey", { type: "string", describe: "Sender pubkey (omit with --wait to receive interactively)" })
|
|
119
|
+
.option("wait", { type: "boolean", default: false, describe: "Wait for incoming request" })
|
|
120
|
+
.option("wait-ms", { type: "number", default: 120000, describe: "Time to wait for request (ms)" })
|
|
121
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
122
|
+
await cmdFriendAccept({
|
|
123
|
+
pubkey: argv.pubkey,
|
|
124
|
+
waitForRequest: argv.wait,
|
|
125
|
+
waitMs: argv["wait-ms"],
|
|
126
|
+
configDir: argv["config-dir"],
|
|
127
|
+
});
|
|
128
|
+
})
|
|
129
|
+
.command("friends list", "List Carrier friends", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
130
|
+
await cmdFriendsList({ configDir: argv["config-dir"] });
|
|
131
|
+
})
|
|
132
|
+
.command("audit log", "View audit log", (y) => y.option("tail", { type: "number", default: 50 }).option("config-dir", { type: "string" }), async (argv) => {
|
|
133
|
+
await cmdAuditLog({ tail: argv.tail, configDir: argv["config-dir"] });
|
|
134
|
+
})
|
|
135
|
+
// Proxy commands — nested under a single `proxy` command so yargs
|
|
136
|
+
// builds a proper subcommand tree. Without nesting, the dotted form
|
|
137
|
+
// "proxy enable" / "proxy use" collides and the last-registered
|
|
138
|
+
// subcommand swallows the others.
|
|
139
|
+
.command("proxy", "Manage the built-in CONNECT proxy (run 'agentnet proxy --help')", (y) => y
|
|
140
|
+
.command("enable", "Enable the built-in CONNECT proxy on this node", (yy) => yy
|
|
141
|
+
.option("port", { type: "number", default: 8888, describe: "Listener TCP port" })
|
|
142
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
143
|
+
await cmdProxyEnable({ port: argv.port, configDir: argv["config-dir"] });
|
|
144
|
+
})
|
|
145
|
+
.command("disable", "Disable the built-in CONNECT proxy on this node", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
146
|
+
await cmdProxyDisable({ configDir: argv["config-dir"] });
|
|
147
|
+
})
|
|
148
|
+
.command("status", "Show proxy configuration", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
149
|
+
await cmdProxyStatus({ configDir: argv["config-dir"] });
|
|
150
|
+
})
|
|
151
|
+
.command("allow-host <host>", "Add a host glob to the CONNECT allowlist (e.g. '*.binance.com')", (yy) => yy
|
|
152
|
+
.positional("host", { type: "string", demandOption: true })
|
|
153
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
154
|
+
await cmdProxyAllowHost({ host: argv.host, configDir: argv["config-dir"] });
|
|
155
|
+
})
|
|
156
|
+
.command("revoke-host <host>", "Remove a host glob from the CONNECT allowlist", (yy) => yy
|
|
157
|
+
.positional("host", { type: "string", demandOption: true })
|
|
158
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
159
|
+
await cmdProxyRevokeHost({ host: argv.host, configDir: argv["config-dir"] });
|
|
160
|
+
})
|
|
161
|
+
.command("list-hosts", "List the CONNECT host allowlist", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
162
|
+
await cmdProxyListHosts({ configDir: argv["config-dir"] });
|
|
163
|
+
})
|
|
164
|
+
.command("use", "Print env vars to route HTTPS via a peer's proxy node", (yy) => yy
|
|
165
|
+
.option("peer", { type: "string", demandOption: true, describe: "Peer name or carrier ID" })
|
|
166
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
167
|
+
await cmdProxyUse({ peer: argv.peer, configDir: argv["config-dir"] });
|
|
168
|
+
})
|
|
169
|
+
.demandCommand(1, "Specify a proxy subcommand (run 'agentnet proxy --help')"), () => {
|
|
170
|
+
// parent handler — never invoked because demandCommand above
|
|
171
|
+
})
|
|
172
|
+
.command("dora", "Manage the dora (DHCP-style) registry integration (run 'agentnet dora --help')", (y) => y
|
|
173
|
+
.command("enable", "Enable dora and register a server's Carrier userid", (yy) => yy
|
|
174
|
+
.option("userid", { type: "string", demandOption: true, describe: "Dora server's Carrier userid" })
|
|
175
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
176
|
+
await cmdDoraEnable({ userid: argv.userid, configDir: argv["config-dir"] });
|
|
177
|
+
})
|
|
178
|
+
.command("disable", "Disable dora integration (keep configured server list)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
179
|
+
await cmdDoraDisable({ configDir: argv["config-dir"] });
|
|
180
|
+
})
|
|
181
|
+
.command("status", "Show dora configuration", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
182
|
+
await cmdDoraStatus({ configDir: argv["config-dir"] });
|
|
183
|
+
})
|
|
184
|
+
.demandCommand(1, "Specify a dora subcommand (run 'agentnet dora --help')"), () => {
|
|
185
|
+
// parent handler — never invoked because demandCommand above
|
|
186
|
+
})
|
|
187
|
+
.demandCommand(1, "Please specify a command. Run with --help for usage.")
|
|
188
|
+
.help()
|
|
189
|
+
.alias("help", "h")
|
|
190
|
+
.strict()
|
|
191
|
+
.parse();
|
|
192
|
+
}
|
|
193
|
+
main().catch((error) => {
|
|
194
|
+
console.error("Error:", error.message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DecentAgentNetConfig } from "../types.js";
|
|
2
|
+
export declare class ConfigLoader {
|
|
3
|
+
static defaultConfigPath(): string;
|
|
4
|
+
static defaultConfigDir(): string;
|
|
5
|
+
static load(filePath?: string): Promise<DecentAgentNetConfig>;
|
|
6
|
+
static loadOrCreateDefault(nodeName: string, configDir?: string): Promise<DecentAgentNetConfig>;
|
|
7
|
+
static createDefault(nodeName: string, configDir?: string): DecentAgentNetConfig;
|
|
8
|
+
static save(config: DecentAgentNetConfig, filePath?: string): Promise<void>;
|
|
9
|
+
private static normalizeConfig;
|
|
10
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { resolve, dirname } from "path";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
const DEFAULT_CONFIG_DIR = resolve(homedir(), ".agentnet");
|
|
7
|
+
const DEFAULT_CONFIG_FILE = resolve(DEFAULT_CONFIG_DIR, "config.yaml");
|
|
8
|
+
// Production bootstrap nodes (Carrier Native SDK 1.8.x compatible)
|
|
9
|
+
// Mirrored from peer SDK's legacy-bootstraps.mjs
|
|
10
|
+
// 45.207.220.155 omitted — observed timing out repeatedly during testing,
|
|
11
|
+
// causing relay#2 to flap which knocks the friend connection offline.
|
|
12
|
+
const DEFAULT_BOOTSTRAP_NODES = [
|
|
13
|
+
{ host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
|
|
14
|
+
{ host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" },
|
|
15
|
+
{ host: "52.83.171.135", port: 443, pk: "5tuHgK1Q4CYf4K5PutsEPK5E3Z7cbtEBdx7LwmdzqXHL" },
|
|
16
|
+
{ host: "52.83.191.228", port: 33445, pk: "3khtxZo89SBScAMaHhTvD68pPHiKxgZT6hTCSZZVgNEm" },
|
|
17
|
+
{ host: "13.58.208.50", port: 33445, pk: "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh" },
|
|
18
|
+
{ host: "18.216.102.47", port: 33445, pk: "G5z8MqiNDFTadFUPfMdYsYtkUDbX5mNCMVHMZtsCnFeb" },
|
|
19
|
+
{ host: "18.216.6.197", port: 33445, pk: "H8sqhRrQuJZ6iLtP2wanxt4LzdNrN2NNFnpPdq1uJ9n2" },
|
|
20
|
+
{ host: "54.193.141.205", port: 33445, pk: "7TfZWZNV8vnBxxWzJXuvKgX2QyKkLpg2oXx3LQ5tg8LW" },
|
|
21
|
+
{ host: "52.74.215.181", port: 33445, pk: "Xv6d34WaUw9bPn7YihzVAFw7D2igbQJZ3jwmzzfYVFV" },
|
|
22
|
+
];
|
|
23
|
+
const DEFAULT_EXPRESS_NODES = [
|
|
24
|
+
{ host: "lens.beagle.chat", port: 443, pk: "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95" },
|
|
25
|
+
];
|
|
26
|
+
export class ConfigLoader {
|
|
27
|
+
static defaultConfigPath() {
|
|
28
|
+
return DEFAULT_CONFIG_FILE;
|
|
29
|
+
}
|
|
30
|
+
static defaultConfigDir() {
|
|
31
|
+
return DEFAULT_CONFIG_DIR;
|
|
32
|
+
}
|
|
33
|
+
static async load(filePath) {
|
|
34
|
+
const path = filePath || DEFAULT_CONFIG_FILE;
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(path, "utf-8");
|
|
37
|
+
const config = yaml.load(content);
|
|
38
|
+
return this.normalizeConfig(config);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error &&
|
|
42
|
+
error.message.includes("ENOENT")) {
|
|
43
|
+
throw new Error(`Config file not found: ${path}. Run 'agentnet init' first.`);
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
static async loadOrCreateDefault(nodeName, configDir) {
|
|
49
|
+
const dir = configDir || DEFAULT_CONFIG_DIR;
|
|
50
|
+
const filePath = resolve(dir, "config.yaml");
|
|
51
|
+
try {
|
|
52
|
+
return await this.load(filePath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return this.createDefault(nodeName, dir);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
static createDefault(nodeName, configDir) {
|
|
59
|
+
const dir = configDir || DEFAULT_CONFIG_DIR;
|
|
60
|
+
return {
|
|
61
|
+
node: {
|
|
62
|
+
name: nodeName,
|
|
63
|
+
namespace: "agentnet-main",
|
|
64
|
+
},
|
|
65
|
+
carrier: {
|
|
66
|
+
// Isolated from other Carrier-using apps (Beagle, OpenClaw, etc.)
|
|
67
|
+
// by default. User can point at a shared identity by editing the config.
|
|
68
|
+
dataDir: resolve(dir, "carrier"),
|
|
69
|
+
bootstrapNodes: DEFAULT_BOOTSTRAP_NODES,
|
|
70
|
+
expressNodes: DEFAULT_EXPRESS_NODES,
|
|
71
|
+
},
|
|
72
|
+
network: {
|
|
73
|
+
interface: "agentnet0",
|
|
74
|
+
ip: "10.86.1.10",
|
|
75
|
+
subnet: "10.86.0.0/16",
|
|
76
|
+
// .decent is the canonical TLD for the Decent Network's
|
|
77
|
+
// private virtual LAN. Existing configs with `dnsDomain:
|
|
78
|
+
// agentnet` keep working since `ConfigLoader.load` preserves
|
|
79
|
+
// whatever's on disk — only new `agentnet init` runs pick
|
|
80
|
+
// this up.
|
|
81
|
+
dnsDomain: "decent",
|
|
82
|
+
// 5353 is the mDNS convention and collides with avahi /
|
|
83
|
+
// mDNSResponder / openclaw-gateway on a lot of boxes. 5354
|
|
84
|
+
// is adjacent and routinely free.
|
|
85
|
+
dnsPort: 5354,
|
|
86
|
+
},
|
|
87
|
+
paths: {
|
|
88
|
+
ipamFile: resolve(dir, "ipam.yaml"),
|
|
89
|
+
policyFile: resolve(dir, "policy.yaml"),
|
|
90
|
+
auditLog: resolve(dir, "audit.log"),
|
|
91
|
+
},
|
|
92
|
+
// Proxy is opt-in. Off by default; `agentnet proxy enable` flips it on.
|
|
93
|
+
proxy: {
|
|
94
|
+
enabled: false,
|
|
95
|
+
port: 8888,
|
|
96
|
+
},
|
|
97
|
+
// Auto-accept incoming friend requests by default. The Carrier
|
|
98
|
+
// network is already a friend network — if you don't want a peer,
|
|
99
|
+
// don't share your address with them. Disable with
|
|
100
|
+
// `friends.autoAccept: false` for stricter control.
|
|
101
|
+
friends: {
|
|
102
|
+
autoAccept: true,
|
|
103
|
+
},
|
|
104
|
+
// Dora integration is opt-in. Leave userids empty to fall back to
|
|
105
|
+
// manual ipam.yaml mode. Once a dora server's userid is added,
|
|
106
|
+
// the daemon registers itself on startup and pulls the roster.
|
|
107
|
+
dora: {
|
|
108
|
+
enabled: false,
|
|
109
|
+
userids: [],
|
|
110
|
+
refreshIntervalMs: 60_000,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
static async save(config, filePath) {
|
|
115
|
+
const path = filePath || DEFAULT_CONFIG_FILE;
|
|
116
|
+
const dir = dirname(path);
|
|
117
|
+
// Create directory if it doesn't exist
|
|
118
|
+
mkdirSync(dir, { recursive: true });
|
|
119
|
+
const content = yaml.dump(config, { lineWidth: -1 });
|
|
120
|
+
const fs = await import("fs/promises");
|
|
121
|
+
await fs.writeFile(path, content, "utf-8");
|
|
122
|
+
}
|
|
123
|
+
static normalizeConfig(config) {
|
|
124
|
+
const defaults = this.createDefault("unnamed-node");
|
|
125
|
+
return {
|
|
126
|
+
node: {
|
|
127
|
+
name: config.node?.name || defaults.node.name,
|
|
128
|
+
namespace: config.node?.namespace || defaults.node.namespace,
|
|
129
|
+
},
|
|
130
|
+
carrier: {
|
|
131
|
+
dataDir: config.carrier?.dataDir || defaults.carrier.dataDir,
|
|
132
|
+
bootstrapNodes: config.carrier?.bootstrapNodes || defaults.carrier.bootstrapNodes,
|
|
133
|
+
expressNodes: config.carrier?.expressNodes || defaults.carrier.expressNodes,
|
|
134
|
+
},
|
|
135
|
+
network: {
|
|
136
|
+
interface: config.network?.interface || defaults.network.interface,
|
|
137
|
+
ip: config.network?.ip || defaults.network.ip,
|
|
138
|
+
subnet: config.network?.subnet || defaults.network.subnet,
|
|
139
|
+
dnsDomain: config.network?.dnsDomain || defaults.network.dnsDomain,
|
|
140
|
+
dnsPort: config.network?.dnsPort || defaults.network.dnsPort,
|
|
141
|
+
},
|
|
142
|
+
paths: {
|
|
143
|
+
ipamFile: config.paths?.ipamFile || defaults.paths.ipamFile,
|
|
144
|
+
policyFile: config.paths?.policyFile || defaults.paths.policyFile,
|
|
145
|
+
auditLog: config.paths?.auditLog || defaults.paths.auditLog,
|
|
146
|
+
},
|
|
147
|
+
proxy: config.proxy ?? defaults.proxy,
|
|
148
|
+
friends: config.friends ?? defaults.friends,
|
|
149
|
+
dora: config.dora ?? defaults.dora,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DaemonServer, type DaemonOptions } from "./server.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DaemonServer } from "./server.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC server — Unix-domain-socket bridge so the CLI can drive the
|
|
3
|
+
* running daemon's Carrier identity without spawning a second Peer.
|
|
4
|
+
*
|
|
5
|
+
* Why: the daemon owns the keypair while it's up. A `node dist/cli
|
|
6
|
+
* friend-request` call from the same user previously opened its own
|
|
7
|
+
* Peer with the same keyfile, which stomped on the daemon's Carrier
|
|
8
|
+
* session (racy session-establishment, duplicate handshake bursts,
|
|
9
|
+
* and "Replacing stale session" cascades). The CLI guard now
|
|
10
|
+
* refuses, but that leaves the operator with no way to ask the
|
|
11
|
+
* running daemon "please send a friend-request to <address>" — this
|
|
12
|
+
* IPC closes that loop.
|
|
13
|
+
*
|
|
14
|
+
* Protocol: newline-delimited JSON. One request per connection, one
|
|
15
|
+
* response, then both sides close. Keeping it one-shot avoids
|
|
16
|
+
* dealing with multiplexing or reconnect logic for v0.1.
|
|
17
|
+
*
|
|
18
|
+
* Socket location: <dataDir>/daemon.sock — same directory as the
|
|
19
|
+
* pidfile. Permissions are loosened to 0666 so the user's CLI can
|
|
20
|
+
* connect when the daemon ran via sudo (otherwise the socket is
|
|
21
|
+
* root-owned). That's acceptable for a single-user box; multi-user
|
|
22
|
+
* hardening can come later via SO_PEERCRED filtering.
|
|
23
|
+
*/
|
|
24
|
+
export interface IpcHandlers {
|
|
25
|
+
/** Send a friend-request via the daemon's Carrier peer. Returns
|
|
26
|
+
* void on success or throws on failure (CLI surfaces the message). */
|
|
27
|
+
friendRequest: (address: string, hello?: string) => Promise<void>;
|
|
28
|
+
/** Return a JSON-friendly snapshot of the daemon's runtime state.
|
|
29
|
+
* Used by `agentnet diag` to inspect a live daemon without
|
|
30
|
+
* restarting it with AGENTNET_LOG_LEVEL=debug. */
|
|
31
|
+
diag: () => Promise<Record<string, unknown>>;
|
|
32
|
+
}
|
|
33
|
+
export interface IpcRequest {
|
|
34
|
+
op: "friend-request" | "ping" | "diag";
|
|
35
|
+
address?: string;
|
|
36
|
+
hello?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface IpcResponseOk {
|
|
39
|
+
ok: true;
|
|
40
|
+
data?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface IpcResponseErr {
|
|
43
|
+
ok: false;
|
|
44
|
+
error: string;
|
|
45
|
+
}
|
|
46
|
+
export type IpcResponse = IpcResponseOk | IpcResponseErr;
|
|
47
|
+
export declare class IpcServer {
|
|
48
|
+
private socketPath;
|
|
49
|
+
private handlers;
|
|
50
|
+
private server?;
|
|
51
|
+
private logger;
|
|
52
|
+
constructor(socketPath: string, handlers: IpcHandlers);
|
|
53
|
+
start(): Promise<void>;
|
|
54
|
+
stop(): Promise<void>;
|
|
55
|
+
private handleConnection;
|
|
56
|
+
private dispatch;
|
|
57
|
+
}
|
|
58
|
+
/** Derive the socket path that pairs with a given carrier data dir.
|
|
59
|
+
* Kept as a one-liner helper so daemon and client agree. */
|
|
60
|
+
export declare function ipcSocketPath(dataDir: string): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC server — Unix-domain-socket bridge so the CLI can drive the
|
|
3
|
+
* running daemon's Carrier identity without spawning a second Peer.
|
|
4
|
+
*
|
|
5
|
+
* Why: the daemon owns the keypair while it's up. A `node dist/cli
|
|
6
|
+
* friend-request` call from the same user previously opened its own
|
|
7
|
+
* Peer with the same keyfile, which stomped on the daemon's Carrier
|
|
8
|
+
* session (racy session-establishment, duplicate handshake bursts,
|
|
9
|
+
* and "Replacing stale session" cascades). The CLI guard now
|
|
10
|
+
* refuses, but that leaves the operator with no way to ask the
|
|
11
|
+
* running daemon "please send a friend-request to <address>" — this
|
|
12
|
+
* IPC closes that loop.
|
|
13
|
+
*
|
|
14
|
+
* Protocol: newline-delimited JSON. One request per connection, one
|
|
15
|
+
* response, then both sides close. Keeping it one-shot avoids
|
|
16
|
+
* dealing with multiplexing or reconnect logic for v0.1.
|
|
17
|
+
*
|
|
18
|
+
* Socket location: <dataDir>/daemon.sock — same directory as the
|
|
19
|
+
* pidfile. Permissions are loosened to 0666 so the user's CLI can
|
|
20
|
+
* connect when the daemon ran via sudo (otherwise the socket is
|
|
21
|
+
* root-owned). That's acceptable for a single-user box; multi-user
|
|
22
|
+
* hardening can come later via SO_PEERCRED filtering.
|
|
23
|
+
*/
|
|
24
|
+
import { createServer } from "net";
|
|
25
|
+
import { chmodSync, existsSync, unlinkSync } from "fs";
|
|
26
|
+
import { Logger } from "../utils/logger.js";
|
|
27
|
+
export class IpcServer {
|
|
28
|
+
socketPath;
|
|
29
|
+
handlers;
|
|
30
|
+
server;
|
|
31
|
+
logger;
|
|
32
|
+
constructor(socketPath, handlers) {
|
|
33
|
+
this.socketPath = socketPath;
|
|
34
|
+
this.handlers = handlers;
|
|
35
|
+
this.logger = new Logger({ prefix: "IPC" });
|
|
36
|
+
}
|
|
37
|
+
async start() {
|
|
38
|
+
// A stale socket from a previously-crashed daemon will make
|
|
39
|
+
// listen() throw EADDRINUSE — unlink it first. Pidfile-guard
|
|
40
|
+
// already ensures no LIVE daemon is running, so deletion is safe.
|
|
41
|
+
if (existsSync(this.socketPath)) {
|
|
42
|
+
try {
|
|
43
|
+
unlinkSync(this.socketPath);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
this.logger.warn(`Could not remove stale socket ${this.socketPath}: ${err}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.server = createServer((sock) => this.handleConnection(sock));
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
this.server.once("error", reject);
|
|
52
|
+
this.server.listen(this.socketPath, () => {
|
|
53
|
+
this.server.off("error", reject);
|
|
54
|
+
resolve();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
// 0666 so non-root CLI can reach a sudo-launched daemon. SO_PEERCRED
|
|
58
|
+
// / SO_PEEREID would be a tighter filter, but for v0.1 the trust
|
|
59
|
+
// boundary is "anyone with shell access to this box".
|
|
60
|
+
try {
|
|
61
|
+
chmodSync(this.socketPath, 0o666);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
this.logger.warn(`chmod ${this.socketPath}: ${err}`);
|
|
65
|
+
}
|
|
66
|
+
this.logger.info(`Listening on ${this.socketPath}`);
|
|
67
|
+
}
|
|
68
|
+
async stop() {
|
|
69
|
+
if (!this.server)
|
|
70
|
+
return;
|
|
71
|
+
await new Promise((resolve) => {
|
|
72
|
+
this.server.close(() => resolve());
|
|
73
|
+
});
|
|
74
|
+
this.server = undefined;
|
|
75
|
+
try {
|
|
76
|
+
if (existsSync(this.socketPath))
|
|
77
|
+
unlinkSync(this.socketPath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// best-effort
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
handleConnection(sock) {
|
|
84
|
+
let buf = "";
|
|
85
|
+
let handled = false;
|
|
86
|
+
const reply = (res) => {
|
|
87
|
+
try {
|
|
88
|
+
sock.write(JSON.stringify(res) + "\n");
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// socket may have closed; nothing to do
|
|
92
|
+
}
|
|
93
|
+
sock.end();
|
|
94
|
+
};
|
|
95
|
+
sock.on("data", (chunk) => {
|
|
96
|
+
if (handled)
|
|
97
|
+
return;
|
|
98
|
+
buf += chunk.toString("utf-8");
|
|
99
|
+
const nl = buf.indexOf("\n");
|
|
100
|
+
if (nl < 0)
|
|
101
|
+
return; // wait for full line
|
|
102
|
+
handled = true;
|
|
103
|
+
const line = buf.slice(0, nl);
|
|
104
|
+
let req;
|
|
105
|
+
try {
|
|
106
|
+
req = JSON.parse(line);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
reply({ ok: false, error: "malformed request (not JSON)" });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
void this.dispatch(req)
|
|
113
|
+
.then((data) => reply({ ok: true, ...(data ? { data } : {}) }))
|
|
114
|
+
.catch((err) => reply({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
|
115
|
+
});
|
|
116
|
+
sock.on("error", (err) => {
|
|
117
|
+
// Common during shutdown — log at debug, don't propagate.
|
|
118
|
+
this.logger.debug(`socket error: ${err.message}`);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async dispatch(req) {
|
|
122
|
+
switch (req.op) {
|
|
123
|
+
case "ping":
|
|
124
|
+
return { pong: true };
|
|
125
|
+
case "friend-request": {
|
|
126
|
+
if (!req.address)
|
|
127
|
+
throw new Error("address is required");
|
|
128
|
+
await this.handlers.friendRequest(req.address, req.hello);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
case "diag":
|
|
132
|
+
return await this.handlers.diag();
|
|
133
|
+
default:
|
|
134
|
+
throw new Error(`unknown op: ${req.op}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Derive the socket path that pairs with a given carrier data dir.
|
|
139
|
+
* Kept as a one-liner helper so daemon and client agree. */
|
|
140
|
+
export function ipcSocketPath(dataDir) {
|
|
141
|
+
// Node's net.listen on Unix sockets supports paths up to ~104 bytes
|
|
142
|
+
// on macOS; <dataDir>/daemon.sock fits comfortably for sane homedirs.
|
|
143
|
+
return `${dataDir.replace(/\/+$/, "")}/daemon.sock`;
|
|
144
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Server — wires together all components
|
|
3
|
+
*/
|
|
4
|
+
import { PeerManager } from "../carrier/peer-manager.js";
|
|
5
|
+
import { Ipam } from "../ipam/ipam.js";
|
|
6
|
+
import { DnsResolver } from "../dns/resolver.js";
|
|
7
|
+
import { AuditLog } from "../acl/audit.js";
|
|
8
|
+
import { AclEngine } from "../acl/acl-engine.js";
|
|
9
|
+
import { PacketRouter } from "../router/packet-router.js";
|
|
10
|
+
import { ConnectProxy } from "../proxy/connect-proxy.js";
|
|
11
|
+
import { DoraIntegration } from "../dora/dora-integration.js";
|
|
12
|
+
import type { DecentAgentNetConfig, DaemonStatus } from "../types.js";
|
|
13
|
+
export interface DaemonOptions {
|
|
14
|
+
config: DecentAgentNetConfig;
|
|
15
|
+
configDir?: string;
|
|
16
|
+
useMockTun?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class DaemonServer {
|
|
19
|
+
private config;
|
|
20
|
+
private useMockTun;
|
|
21
|
+
private logger;
|
|
22
|
+
private peerManager?;
|
|
23
|
+
private tunDevice?;
|
|
24
|
+
private routeManager?;
|
|
25
|
+
private ipam?;
|
|
26
|
+
private dnsResolver?;
|
|
27
|
+
private policy?;
|
|
28
|
+
private auditLog?;
|
|
29
|
+
private aclEngine?;
|
|
30
|
+
private packetRouter?;
|
|
31
|
+
private connectProxy?;
|
|
32
|
+
private doraIntegration?;
|
|
33
|
+
private ipcServer?;
|
|
34
|
+
private dnsServer?;
|
|
35
|
+
private startedAt;
|
|
36
|
+
private isRunning;
|
|
37
|
+
private pidFile?;
|
|
38
|
+
constructor(opts: DaemonOptions);
|
|
39
|
+
start(): Promise<void>;
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
getStatus(): DaemonStatus;
|
|
42
|
+
/**
|
|
43
|
+
* Periodically poke the SDK to (re-)initiate a session with the friend.
|
|
44
|
+
* This works around the asymmetric friendOnline issue: without this,
|
|
45
|
+
* one side may never spontaneously try to talk to the other and the
|
|
46
|
+
* friend stays perpetually offline.
|
|
47
|
+
*/
|
|
48
|
+
private kickSessionForever;
|
|
49
|
+
/**
|
|
50
|
+
* Repeatedly call waitForFriendConnected for the given peer until the
|
|
51
|
+
* daemon is stopped. Logs ONLINE/OFFLINE transitions.
|
|
52
|
+
*/
|
|
53
|
+
private probeFriendForever;
|
|
54
|
+
getPeerManager(): PeerManager | undefined;
|
|
55
|
+
getIpam(): Ipam | undefined;
|
|
56
|
+
getDnsResolver(): DnsResolver | undefined;
|
|
57
|
+
getAclEngine(): AclEngine | undefined;
|
|
58
|
+
getAuditLog(): AuditLog | undefined;
|
|
59
|
+
getPacketRouter(): PacketRouter | undefined;
|
|
60
|
+
getConnectProxy(): ConnectProxy | undefined;
|
|
61
|
+
getDoraIntegration(): DoraIntegration | undefined;
|
|
62
|
+
private cleanup;
|
|
63
|
+
}
|