@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,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dora integration — registers this node with a dora (DHCP-style) server
|
|
3
|
+
* and pulls the roster of known peers into the in-memory IPAM.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the manual ipam.yaml dance once an operator points the config
|
|
6
|
+
* at a dora server's userid. The yaml file is still loaded first as a
|
|
7
|
+
* fallback; whatever the dora server says wins on conflict.
|
|
8
|
+
*
|
|
9
|
+
* Failure mode: if no configured dora server answers within the timeout,
|
|
10
|
+
* we log a warning and fall through to whatever IP/IPAM was loaded from
|
|
11
|
+
* disk. Decentlan is fully functional without dora — it's just that
|
|
12
|
+
* peers need to manually share ipam.yaml entries.
|
|
13
|
+
*/
|
|
14
|
+
import { DoraClient, AllRegistriesUnavailableError } from "@decentnetwork/dora";
|
|
15
|
+
import { Logger } from "../utils/logger.js";
|
|
16
|
+
export class DoraIntegration {
|
|
17
|
+
opts;
|
|
18
|
+
client;
|
|
19
|
+
logger;
|
|
20
|
+
refreshTimer;
|
|
21
|
+
retryBootstrapTimer;
|
|
22
|
+
/** The IP dora handed us. Falls back to preferredIp on registry failure. */
|
|
23
|
+
allocatedIp;
|
|
24
|
+
/** Userids we've already attempted to friend this session — keeps the
|
|
25
|
+
* 60s roster refresh from spamming sendFriendRequest. */
|
|
26
|
+
friendRequested = new Set();
|
|
27
|
+
/** Set once we've successfully registered. The retry-bootstrap loop
|
|
28
|
+
* uses this to know when to stop trying. */
|
|
29
|
+
registered = false;
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
this.opts = opts;
|
|
32
|
+
this.logger = new Logger({ prefix: "Dora" });
|
|
33
|
+
this.client = new DoraClient({
|
|
34
|
+
registryUserids: opts.config.userids ?? [],
|
|
35
|
+
sendText: (toUserid, text) => opts.peerManager.sendText(toUserid, text),
|
|
36
|
+
onText: (handler) => {
|
|
37
|
+
opts.peerManager.on("dora-message", (fromUserid, text) => {
|
|
38
|
+
handler(fromUserid, text);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
timeoutMs: 10_000,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Register self with dora and pull the roster. Idempotent; safe to
|
|
46
|
+
* retry. Returns the IP dora allocated (or preferredIp if all servers
|
|
47
|
+
* were unreachable).
|
|
48
|
+
*/
|
|
49
|
+
async bootstrap() {
|
|
50
|
+
const myUserid = this.opts.peerManager.getPubkey();
|
|
51
|
+
const userids = this.opts.config.userids ?? [];
|
|
52
|
+
if (userids.length === 0) {
|
|
53
|
+
this.logger.warn("dora.userids empty — skipping dora registration");
|
|
54
|
+
return this.opts.preferredIp ?? "";
|
|
55
|
+
}
|
|
56
|
+
// Kick session establishment with each registry so the first call
|
|
57
|
+
// doesn't fail on cold-start cookie/handshake delay. peer-manager
|
|
58
|
+
// swallows the "friend offline" error internally.
|
|
59
|
+
for (const id of userids) {
|
|
60
|
+
this.opts.peerManager
|
|
61
|
+
.kickSessionEstablishment(id)
|
|
62
|
+
.catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
// Wait up to 30s for ANY configured dora server to come online.
|
|
65
|
+
// Without this, the first `register` call races the underlying
|
|
66
|
+
// Carrier session establishment and fails with "friend is offline
|
|
67
|
+
// and no express node is configured", forcing the daemon onto the
|
|
68
|
+
// fallback (config) IP. Use Promise.race so the first responder
|
|
69
|
+
// wins — slow registries don't hold up the bootstrap.
|
|
70
|
+
const onlineWaits = userids.map((id) => this.opts.peerManager
|
|
71
|
+
.waitForFriendConnected(id, 30_000)
|
|
72
|
+
.then((online) => ({ id, online }))
|
|
73
|
+
.catch(() => ({ id, online: false })));
|
|
74
|
+
const anyOnline = await Promise.race([
|
|
75
|
+
Promise.any(onlineWaits.map((p) => p.then((r) => (r.online ? r : Promise.reject(new Error("offline")))))).catch(() => null),
|
|
76
|
+
new Promise((r) => setTimeout(() => r(null), 30_000)),
|
|
77
|
+
]);
|
|
78
|
+
if (!anyOnline) {
|
|
79
|
+
this.logger.warn("No dora server became reachable within 30s — falling back to config IP");
|
|
80
|
+
return this.opts.preferredIp ?? "";
|
|
81
|
+
}
|
|
82
|
+
this.logger.debug(`Dora friend online: ${anyOnline.id}`);
|
|
83
|
+
// Include our address so other peers fetched from list() can call
|
|
84
|
+
// sendFriendRequest on us. Userid alone is the bare pubkey-derived
|
|
85
|
+
// id; sendFriendRequest needs the address form (with nospam).
|
|
86
|
+
const myAddress = this.opts.peerManager.getAddress();
|
|
87
|
+
// Wrap the entire register-and-roster-fetch flow in ONE try/catch
|
|
88
|
+
// so any failure (registry unreachable, transient session blip,
|
|
89
|
+
// unexpected error) cleanly falls back to the config IP. An
|
|
90
|
+
// earlier version split this into two try blocks, with the result
|
|
91
|
+
// that a non-IP-collision error from the first register call
|
|
92
|
+
// re-threw past the AllRegistriesUnavailableError handler and
|
|
93
|
+
// crashed the daemon on startup.
|
|
94
|
+
try {
|
|
95
|
+
let record;
|
|
96
|
+
try {
|
|
97
|
+
record = await this.tryRegister(myUserid, myAddress, this.opts.preferredIp);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// Common case: the preferred IP from config.yaml collides
|
|
101
|
+
// with another peer's reservation (every fresh `agentnet
|
|
102
|
+
// init` defaults to 10.86.1.10, so the second peer to
|
|
103
|
+
// register always loses the race). Retry once without
|
|
104
|
+
// requestedIp so dora picks any free slot. Errors that
|
|
105
|
+
// aren't IP-collision flavored fall through to the outer
|
|
106
|
+
// catch and trigger the fallback path.
|
|
107
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
108
|
+
const looksLikeIpCollision = /held by|in use|already taken|already in use/i.test(msg);
|
|
109
|
+
if (!looksLikeIpCollision)
|
|
110
|
+
throw err;
|
|
111
|
+
this.logger.warn(`Preferred IP ${this.opts.preferredIp} not available (${msg}) — requesting any free IP`);
|
|
112
|
+
record = await this.tryRegister(myUserid, myAddress);
|
|
113
|
+
}
|
|
114
|
+
this.allocatedIp = record.virtualIp;
|
|
115
|
+
this.registered = true;
|
|
116
|
+
this.logger.info(`Registered with dora: ${record.name} -> ${record.virtualIp}`);
|
|
117
|
+
// Add self to the in-memory IPAM so the local DNS server can
|
|
118
|
+
// answer `<my-name>.<domain>` queries from this very host.
|
|
119
|
+
// mergeRosterIntoIpam skips own entry (sensible — peers don't
|
|
120
|
+
// need to "auto-friend themselves"), but the DNS resolver
|
|
121
|
+
// looks at the same IPAM, so without this entry `dig snoopy.
|
|
122
|
+
// decent` from snoopy itself NXDOMAINs.
|
|
123
|
+
this.opts.ipam.assignPeer({
|
|
124
|
+
name: record.name,
|
|
125
|
+
carrierId: myUserid,
|
|
126
|
+
virtualIp: record.virtualIp,
|
|
127
|
+
services: [],
|
|
128
|
+
});
|
|
129
|
+
// Pull the full roster. We don't fail the whole bootstrap if
|
|
130
|
+
// list() throws — we still have our own address allocated.
|
|
131
|
+
try {
|
|
132
|
+
const roster = await this.client.list();
|
|
133
|
+
this.mergeRosterIntoIpam(roster, myUserid);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
this.logger.warn(`Initial roster fetch failed: ${err}`);
|
|
137
|
+
}
|
|
138
|
+
// Start periodic refresh.
|
|
139
|
+
const interval = this.opts.config.refreshIntervalMs ?? 60_000;
|
|
140
|
+
this.refreshTimer = setInterval(() => {
|
|
141
|
+
this.refreshRoster(myUserid).catch((err) => {
|
|
142
|
+
this.logger.debug(`Roster refresh: ${err}`);
|
|
143
|
+
});
|
|
144
|
+
}, interval);
|
|
145
|
+
return this.allocatedIp;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (err instanceof AllRegistriesUnavailableError) {
|
|
149
|
+
this.logger.warn(`All dora servers unreachable — falling back to config IP ${this.opts.preferredIp}: ${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Treat unexpected errors the same as unreachable — the
|
|
153
|
+
// daemon should still come up. Operator sees the warning
|
|
154
|
+
// and can fix.
|
|
155
|
+
this.logger.warn(`Dora register failed — falling back to config IP ${this.opts.preferredIp}: ${err instanceof Error ? err.message : err}`);
|
|
156
|
+
}
|
|
157
|
+
// Schedule a background retry. The TUN is already up at the
|
|
158
|
+
// fallback IP; we don't bring it down on success, so the
|
|
159
|
+
// daemon keeps running with the fallback even when register
|
|
160
|
+
// eventually succeeds — but at least subsequent peers
|
|
161
|
+
// (Ubuntu's daemon doing list()) will see our record with our
|
|
162
|
+
// address and can auto-friend us.
|
|
163
|
+
this.scheduleBootstrapRetry();
|
|
164
|
+
return this.opts.preferredIp ?? "";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Re-attempt the register flow every 30 seconds until it succeeds.
|
|
169
|
+
* Without this, a daemon that started before its dora server came
|
|
170
|
+
* online never makes it into the roster — peers can't auto-friend
|
|
171
|
+
* us, and auto-IP allocation is lost. bootstrap() re-derives its
|
|
172
|
+
* own myUserid so we don't need to thread it through.
|
|
173
|
+
*/
|
|
174
|
+
scheduleBootstrapRetry() {
|
|
175
|
+
if (this.retryBootstrapTimer || this.registered)
|
|
176
|
+
return;
|
|
177
|
+
const tick = () => {
|
|
178
|
+
if (this.registered)
|
|
179
|
+
return;
|
|
180
|
+
this.logger.debug("Retrying dora bootstrap…");
|
|
181
|
+
// Don't await — let it run async and clear the timer if it
|
|
182
|
+
// succeeds, otherwise leave the timer running.
|
|
183
|
+
void this.bootstrap().then(() => {
|
|
184
|
+
if (this.registered && this.retryBootstrapTimer) {
|
|
185
|
+
clearInterval(this.retryBootstrapTimer);
|
|
186
|
+
this.retryBootstrapTimer = undefined;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
this.retryBootstrapTimer = setInterval(tick, 30_000);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Wrap a single register call with up to 3 retries spaced 2s apart.
|
|
194
|
+
* The first call often races the underlying Carrier session even
|
|
195
|
+
* after waitForFriendConnected resolves — the SDK has a transient
|
|
196
|
+
* gap between the "online" event and the in-session crypto channel
|
|
197
|
+
* being ready for the next outgoing sendText. A short retry burst
|
|
198
|
+
* hides that without needing express fallback.
|
|
199
|
+
*/
|
|
200
|
+
async tryRegister(myUserid, myAddress, requestedIp) {
|
|
201
|
+
const maxAttempts = 3;
|
|
202
|
+
let lastErr;
|
|
203
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
204
|
+
try {
|
|
205
|
+
return await this.client.register({
|
|
206
|
+
userid: myUserid,
|
|
207
|
+
name: this.opts.nodeName,
|
|
208
|
+
address: myAddress,
|
|
209
|
+
requestedIp,
|
|
210
|
+
replace: true,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
lastErr = err;
|
|
215
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
216
|
+
// Don't retry on definitive rejections (name/IP collision):
|
|
217
|
+
// those are protocol-level errors that won't fix themselves.
|
|
218
|
+
if (/held by|in use|already taken|already in use|out of range|already registered/i.test(msg)) {
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
if (attempt < maxAttempts) {
|
|
222
|
+
this.logger.debug(`register attempt ${attempt}/${maxAttempts} failed: ${msg} — retrying in 2s`);
|
|
223
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw lastErr;
|
|
228
|
+
}
|
|
229
|
+
stop() {
|
|
230
|
+
if (this.refreshTimer) {
|
|
231
|
+
clearInterval(this.refreshTimer);
|
|
232
|
+
this.refreshTimer = undefined;
|
|
233
|
+
}
|
|
234
|
+
if (this.retryBootstrapTimer) {
|
|
235
|
+
clearInterval(this.retryBootstrapTimer);
|
|
236
|
+
this.retryBootstrapTimer = undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
getAllocatedIp() {
|
|
240
|
+
return this.allocatedIp;
|
|
241
|
+
}
|
|
242
|
+
async refreshRoster(myUserid) {
|
|
243
|
+
const roster = await this.client.list();
|
|
244
|
+
this.mergeRosterIntoIpam(roster, myUserid);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Stamp dora records into the in-memory IPAM. We DON'T persist to
|
|
248
|
+
* ipam.yaml — that file remains operator-managed. Dora is treated as
|
|
249
|
+
* a live overlay that's refreshed on every restart.
|
|
250
|
+
*
|
|
251
|
+
* Conflict policy: dora wins. If ipam.yaml had a peer with a stale IP,
|
|
252
|
+
* the dora record overwrites it (assignPeer dedupes by name + carrierId).
|
|
253
|
+
*/
|
|
254
|
+
mergeRosterIntoIpam(roster, myUserid) {
|
|
255
|
+
let added = 0;
|
|
256
|
+
let updated = 0;
|
|
257
|
+
for (const entry of roster) {
|
|
258
|
+
if (entry.userid === myUserid)
|
|
259
|
+
continue; // skip self
|
|
260
|
+
const existing = this.opts.ipam.resolveCarrierId(entry.userid);
|
|
261
|
+
if (!existing) {
|
|
262
|
+
added += 1;
|
|
263
|
+
}
|
|
264
|
+
else if (existing.virtualIp !== entry.virtualIp ||
|
|
265
|
+
existing.name !== entry.name) {
|
|
266
|
+
updated += 1;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Entry unchanged in IPAM, but still attempt friending — a
|
|
270
|
+
// roster refresh may pick up a peer that registered earlier
|
|
271
|
+
// and is already in our IPAM (because we previously fetched
|
|
272
|
+
// them) but never got friended.
|
|
273
|
+
this.maybeFriend(entry);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
this.opts.ipam.assignPeer({
|
|
277
|
+
name: entry.name,
|
|
278
|
+
carrierId: entry.userid,
|
|
279
|
+
virtualIp: entry.virtualIp,
|
|
280
|
+
services: existing?.services ?? [],
|
|
281
|
+
});
|
|
282
|
+
this.maybeFriend(entry);
|
|
283
|
+
}
|
|
284
|
+
if (added > 0 || updated > 0) {
|
|
285
|
+
this.logger.info(`Roster refreshed: ${added} new, ${updated} updated, ${roster.length} total`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Send an outbound friend request to a roster entry if we haven't
|
|
290
|
+
* already (and aren't already friends). Idempotent at multiple
|
|
291
|
+
* levels: in-process guard via `friendRequested`, SDK-level
|
|
292
|
+
* short-circuit on `acceptedAt`, and recipient-side auto-accept.
|
|
293
|
+
*
|
|
294
|
+
* Address is optional in the protocol for backward-compat with
|
|
295
|
+
* older dora records; without it, we can't initiate the request
|
|
296
|
+
* (the SDK needs the nospam token embedded in the address). The
|
|
297
|
+
* caller logs a one-time warning so the operator knows to either
|
|
298
|
+
* re-register the peer or fall back to a manual friend-request.
|
|
299
|
+
*/
|
|
300
|
+
maybeFriend(entry) {
|
|
301
|
+
if (this.friendRequested.has(entry.userid))
|
|
302
|
+
return;
|
|
303
|
+
if (this.opts.peerManager.isFriend(entry.userid)) {
|
|
304
|
+
this.friendRequested.add(entry.userid);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!entry.address) {
|
|
308
|
+
this.logger.warn(`Roster entry ${entry.name} (${entry.userid.slice(0, 12)}...) has no address — can't auto-friend. ` +
|
|
309
|
+
`Have them re-register against a newer dora server, or run 'agentnet friend-request' manually.`);
|
|
310
|
+
this.friendRequested.add(entry.userid); // don't keep warning every 60s
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.friendRequested.add(entry.userid);
|
|
314
|
+
this.opts.peerManager
|
|
315
|
+
.sendFriendRequest(entry.address, `dora roster auto-friend (${this.opts.nodeName})`)
|
|
316
|
+
.then(() => {
|
|
317
|
+
this.logger.info(`Auto-friend sent to ${entry.name} (${entry.userid.slice(0, 12)}...)`);
|
|
318
|
+
})
|
|
319
|
+
.catch((err) => {
|
|
320
|
+
this.logger.warn(`Auto-friend ${entry.name}: ${err instanceof Error ? err.message : err}`);
|
|
321
|
+
// Allow another attempt on the next refresh cycle.
|
|
322
|
+
this.friendRequested.delete(entry.userid);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Decent AgentNet — Main entry point
|
|
4
|
+
* Dispatches to CLI handler.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: the EventEmitter.defaultMaxListeners adjustment MUST happen before
|
|
7
|
+
* the SDK is imported. The Carrier SDK's UdpTransport attaches one listener
|
|
8
|
+
* per bootstrap node and per relay, easily exceeding Node's default cap of
|
|
9
|
+
* 10. If we raise the cap after the SDK loads, EventEmitter instances
|
|
10
|
+
* created during the SDK's module initialization still use the old default
|
|
11
|
+
* and produce a wall of MaxListenersExceededWarning lines at runtime.
|
|
12
|
+
*/
|
|
13
|
+
import "./cli/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Decent AgentNet — Main entry point
|
|
4
|
+
* Dispatches to CLI handler.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: the EventEmitter.defaultMaxListeners adjustment MUST happen before
|
|
7
|
+
* the SDK is imported. The Carrier SDK's UdpTransport attaches one listener
|
|
8
|
+
* per bootstrap node and per relay, easily exceeding Node's default cap of
|
|
9
|
+
* 10. If we raise the cap after the SDK loads, EventEmitter instances
|
|
10
|
+
* created during the SDK's module initialization still use the old default
|
|
11
|
+
* and produce a wall of MaxListenersExceededWarning lines at runtime.
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from "events";
|
|
14
|
+
EventEmitter.defaultMaxListeners = 100;
|
|
15
|
+
import "./cli/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Ipam, type IpamConfig } from "./ipam.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Ipam } from "./ipam.js";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPAM - IP Address Management
|
|
3
|
+
* Maps Carrier IDs to virtual IPs, hostnames, and services
|
|
4
|
+
*/
|
|
5
|
+
import type { IpamRecord, Service } from "../types.js";
|
|
6
|
+
export interface IpamConfig {
|
|
7
|
+
namespace: string;
|
|
8
|
+
peers: IpamRecord[];
|
|
9
|
+
}
|
|
10
|
+
export declare class Ipam {
|
|
11
|
+
private config;
|
|
12
|
+
private filePath;
|
|
13
|
+
private logger;
|
|
14
|
+
private ipCache;
|
|
15
|
+
private idCache;
|
|
16
|
+
private nameCache;
|
|
17
|
+
constructor(config: IpamConfig, filePath: string);
|
|
18
|
+
static load(filePath: string): Promise<Ipam>;
|
|
19
|
+
static loadOrCreate(filePath: string, namespace?: string): Promise<Ipam>;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve hostname to virtual IP
|
|
22
|
+
*/
|
|
23
|
+
resolveName(name: string): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve virtual IP to IpamRecord
|
|
26
|
+
*/
|
|
27
|
+
resolveIp(ip: string): IpamRecord | null;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve Carrier ID to IpamRecord
|
|
30
|
+
*/
|
|
31
|
+
resolveCarrierId(carrierId: string): IpamRecord | null;
|
|
32
|
+
/**
|
|
33
|
+
* Get all peers
|
|
34
|
+
*/
|
|
35
|
+
getPeers(): IpamRecord[];
|
|
36
|
+
/**
|
|
37
|
+
* Add or update a peer
|
|
38
|
+
*/
|
|
39
|
+
assignPeer(record: IpamRecord): void;
|
|
40
|
+
/**
|
|
41
|
+
* Remove a peer by name or Carrier ID
|
|
42
|
+
*/
|
|
43
|
+
removePeer(identifier: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Check if IP is allocated
|
|
46
|
+
*/
|
|
47
|
+
isIpAllocated(ip: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Find next available IP in subnet
|
|
50
|
+
* Assumes subnet like "10.86.0.0/16"
|
|
51
|
+
*/
|
|
52
|
+
findNextAvailableIp(subnet: string, start?: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Get service port for peer
|
|
55
|
+
*/
|
|
56
|
+
getServicePort(peerName: string, serviceName: string): number | null;
|
|
57
|
+
/**
|
|
58
|
+
* Get all services for peer
|
|
59
|
+
*/
|
|
60
|
+
getServices(peerName: string): Service[];
|
|
61
|
+
/**
|
|
62
|
+
* Save IPAM config to file
|
|
63
|
+
*/
|
|
64
|
+
save(): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Check if peer is expired
|
|
67
|
+
*/
|
|
68
|
+
isExpired(peerName: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Deterministically derive a virtual IP from a Carrier userid, in the
|
|
71
|
+
* same style as ARP/DHCP-by-MAC: every peer computes the same IP for the
|
|
72
|
+
* same userid, so the network is consistent without manual coordination.
|
|
73
|
+
*
|
|
74
|
+
* Hash(userid)[0..1] → last two octets of 10.86.X.Y. Avoids x.0 and x.255.
|
|
75
|
+
* Returns the IP regardless of whether it's already in the IPAM.
|
|
76
|
+
*/
|
|
77
|
+
static deterministicIpForUserid(userid: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Ensure each friend has an IPAM record. Returns the number of new
|
|
80
|
+
* records added. Existing entries (manual or previously auto-assigned)
|
|
81
|
+
* are preserved.
|
|
82
|
+
*
|
|
83
|
+
* On collision (different userid hashes to an already-taken IP), walks
|
|
84
|
+
* the 4th octet until a free slot is found.
|
|
85
|
+
*/
|
|
86
|
+
autoAssignFriends(friends: {
|
|
87
|
+
pubkey: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
}[], selfUserid?: string): number;
|
|
90
|
+
/**
|
|
91
|
+
* Get config
|
|
92
|
+
*/
|
|
93
|
+
getConfig(): IpamConfig;
|
|
94
|
+
/**
|
|
95
|
+
* Get namespace
|
|
96
|
+
*/
|
|
97
|
+
getNamespace(): string;
|
|
98
|
+
private rebuildCache;
|
|
99
|
+
}
|