@decentnetwork/lan 0.1.154 → 0.1.155
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/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/cli/commands.d.ts +19 -0
- package/dist/cli/commands.js +63 -0
- package/dist/cli/index.js +16 -1
- package/dist/daemon/server.js +12 -1
- package/dist/proxy/connect-proxy.d.ts +41 -0
- package/dist/proxy/connect-proxy.js +76 -3
- package/dist/proxy/multi-exit-router.js +21 -4
- package/dist/tun/tun-device.js +19 -1
- package/dist/types.d.ts +2 -0
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -205,6 +205,25 @@ export declare function cmdProxyAllowHost(args: {
|
|
|
205
205
|
host: string;
|
|
206
206
|
configDir?: string;
|
|
207
207
|
}): Promise<void>;
|
|
208
|
+
/**
|
|
209
|
+
* Manage the exit's free-tier whitelist: userids that are always served, even
|
|
210
|
+
* when the exit is busy (the paid / priority set). `action` is add | remove |
|
|
211
|
+
* list. Applies live to the running proxy — no restart, no session churn.
|
|
212
|
+
*/
|
|
213
|
+
export declare function cmdProxyWhitelist(args: {
|
|
214
|
+
action: "add" | "remove" | "list";
|
|
215
|
+
userid?: string;
|
|
216
|
+
configDir?: string;
|
|
217
|
+
}): Promise<void>;
|
|
218
|
+
/**
|
|
219
|
+
* Set (or clear, with 0) the exit's busy threshold in Mbps. Above this smoothed
|
|
220
|
+
* egress rate the exit serves only whitelisted peers; 0 = free for all.
|
|
221
|
+
* Applies live to the running proxy.
|
|
222
|
+
*/
|
|
223
|
+
export declare function cmdProxyBusyMbps(args: {
|
|
224
|
+
mbps: number;
|
|
225
|
+
configDir?: string;
|
|
226
|
+
}): Promise<void>;
|
|
208
227
|
/**
|
|
209
228
|
* Remove a host glob from the proxy allowlist.
|
|
210
229
|
*/
|
package/dist/cli/commands.js
CHANGED
|
@@ -976,6 +976,69 @@ export async function cmdProxyAllowHost(args) {
|
|
|
976
976
|
console.log(`Added '${args.host}' to proxy allow-hosts.`);
|
|
977
977
|
await applyProxyReloadIfRunning(config);
|
|
978
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Manage the exit's free-tier whitelist: userids that are always served, even
|
|
981
|
+
* when the exit is busy (the paid / priority set). `action` is add | remove |
|
|
982
|
+
* list. Applies live to the running proxy — no restart, no session churn.
|
|
983
|
+
*/
|
|
984
|
+
export async function cmdProxyWhitelist(args) {
|
|
985
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
986
|
+
const configPath = resolve(dir, "config.yaml");
|
|
987
|
+
const config = await ConfigLoader.load(configPath);
|
|
988
|
+
const proxy = config.proxy ?? { enabled: false, port: 8888 };
|
|
989
|
+
const whitelist = new Set(proxy.whitelist ?? []);
|
|
990
|
+
if (args.action === "list") {
|
|
991
|
+
if (whitelist.size === 0) {
|
|
992
|
+
console.log("Exit whitelist is empty (when busy, no one is prioritized — free users are refused).");
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
console.log(`Exit whitelist (${whitelist.size} always-served userid(s)):`);
|
|
996
|
+
for (const u of whitelist)
|
|
997
|
+
console.log(` - ${u}`);
|
|
998
|
+
}
|
|
999
|
+
const busy = proxy.busyMbps ?? 0;
|
|
1000
|
+
console.log(busy > 0
|
|
1001
|
+
? `Busy threshold: ${busy}Mbps (above this, only whitelisted peers get new tunnels).`
|
|
1002
|
+
: `Busy threshold: OFF — exit is free for all. Set one with 'agentnet proxy busy-mbps <N>'.`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (!args.userid) {
|
|
1006
|
+
console.log(`A userid is required for '${args.action}'.`);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (args.action === "add") {
|
|
1010
|
+
whitelist.add(args.userid);
|
|
1011
|
+
console.log(`Added ${args.userid} to the exit whitelist (always served, even when busy).`);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
if (!whitelist.delete(args.userid)) {
|
|
1015
|
+
console.log(`${args.userid} was not on the whitelist.`);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
console.log(`Removed ${args.userid} from the exit whitelist.`);
|
|
1019
|
+
}
|
|
1020
|
+
config.proxy = { ...proxy, whitelist: [...whitelist] };
|
|
1021
|
+
await ConfigLoader.save(config, configPath);
|
|
1022
|
+
await applyProxyReloadIfRunning(config);
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Set (or clear, with 0) the exit's busy threshold in Mbps. Above this smoothed
|
|
1026
|
+
* egress rate the exit serves only whitelisted peers; 0 = free for all.
|
|
1027
|
+
* Applies live to the running proxy.
|
|
1028
|
+
*/
|
|
1029
|
+
export async function cmdProxyBusyMbps(args) {
|
|
1030
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
1031
|
+
const configPath = resolve(dir, "config.yaml");
|
|
1032
|
+
const config = await ConfigLoader.load(configPath);
|
|
1033
|
+
const proxy = config.proxy ?? { enabled: false, port: 8888 };
|
|
1034
|
+
const mbps = Math.max(0, Math.floor(args.mbps));
|
|
1035
|
+
config.proxy = { ...proxy, busyMbps: mbps };
|
|
1036
|
+
await ConfigLoader.save(config, configPath);
|
|
1037
|
+
console.log(mbps > 0
|
|
1038
|
+
? `Exit busy threshold set to ${mbps}Mbps — above this, only whitelisted peers get new tunnels.`
|
|
1039
|
+
: `Exit busy threshold cleared — the exit is now free for all.`);
|
|
1040
|
+
await applyProxyReloadIfRunning(config);
|
|
1041
|
+
}
|
|
979
1042
|
/**
|
|
980
1043
|
* If the daemon is up, push the freshly-saved proxy allowlist into the
|
|
981
1044
|
* running proxy over IPC — no restart, no Carrier-session churn. Prints
|
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hideBin } from "yargs/helpers";
|
|
|
10
10
|
// Belt-and-braces — also raise it here in case the CLI is run directly
|
|
11
11
|
// (e.g. `node dist/cli/index.js` rather than via dist/index.js).
|
|
12
12
|
EventEmitter.defaultMaxListeners = 100;
|
|
13
|
-
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdProxyRouter, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
|
|
13
|
+
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyWhitelist, cmdProxyBusyMbps, cmdProxyUse, cmdProxyRouter, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
|
|
14
14
|
async function main() {
|
|
15
15
|
await yargs(hideBin(process.argv))
|
|
16
16
|
.scriptName("agentnet")
|
|
@@ -317,6 +317,21 @@ async function main() {
|
|
|
317
317
|
})
|
|
318
318
|
.command("list-hosts", "List the CONNECT host allowlist", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
319
319
|
await cmdProxyListHosts({ configDir: argv["config-dir"] });
|
|
320
|
+
})
|
|
321
|
+
.command("whitelist <action> [userid]", "Free-tier whitelist: userids always served when the exit is busy (add|remove|list)", (yy) => yy
|
|
322
|
+
.positional("action", { type: "string", demandOption: true })
|
|
323
|
+
.positional("userid", { type: "string" })
|
|
324
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
325
|
+
if (argv.action !== "add" && argv.action !== "remove" && argv.action !== "list") {
|
|
326
|
+
console.log("Usage: agentnet proxy whitelist <add|remove|list> [userid]");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
await cmdProxyWhitelist({ action: argv.action, userid: argv.userid, configDir: argv["config-dir"] });
|
|
330
|
+
})
|
|
331
|
+
.command("busy-mbps <mbps>", "Set the exit's busy threshold (Mbps); above it, only whitelisted peers get new tunnels. 0 = free for all", (yy) => yy
|
|
332
|
+
.positional("mbps", { type: "number", demandOption: true })
|
|
333
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
334
|
+
await cmdProxyBusyMbps({ mbps: argv.mbps, configDir: argv["config-dir"] });
|
|
320
335
|
})
|
|
321
336
|
.command("who", "Show who is using THIS node's exit (per-peer tunnels + bytes)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
322
337
|
await cmdProxyWho({ configDir: argv["config-dir"] });
|
package/dist/daemon/server.js
CHANGED
|
@@ -541,13 +541,21 @@ export class DaemonServer {
|
|
|
541
541
|
};
|
|
542
542
|
}
|
|
543
543
|
this.connectProxy.updateAllowlist(allowHosts, allowConnectPorts);
|
|
544
|
+
// Also refresh the free-tier admission control (busy threshold +
|
|
545
|
+
// whitelist) from the same fresh config, so `proxy whitelist add`
|
|
546
|
+
// and `proxy busy-mbps` apply live without a restart.
|
|
547
|
+
const whitelist = fresh.proxy?.whitelist ?? [];
|
|
548
|
+
const busyMbps = fresh.proxy?.busyMbps;
|
|
549
|
+
this.connectProxy.updateAdmission({ whitelist, busyMbps });
|
|
544
550
|
// Keep the in-memory config in sync so a later diag reflects it.
|
|
545
551
|
if (this.config.proxy) {
|
|
546
552
|
this.config.proxy.allowHosts = allowHosts;
|
|
547
553
|
if (allowConnectPorts)
|
|
548
554
|
this.config.proxy.allowConnectPorts = allowConnectPorts;
|
|
555
|
+
this.config.proxy.whitelist = whitelist;
|
|
556
|
+
this.config.proxy.busyMbps = busyMbps;
|
|
549
557
|
}
|
|
550
|
-
return { applied: true, allowHosts };
|
|
558
|
+
return { applied: true, allowHosts, whitelist, busyMbps };
|
|
551
559
|
},
|
|
552
560
|
proxyAccess: async (userid, allow) => {
|
|
553
561
|
// Live block/allow a peer's access to THIS node's exit proxy port.
|
|
@@ -1031,7 +1039,10 @@ export class DaemonServer {
|
|
|
1031
1039
|
allowHosts: this.config.proxy.allowHosts ?? [],
|
|
1032
1040
|
allowConnectPorts: this.config.proxy.allowConnectPorts,
|
|
1033
1041
|
egressBindIp,
|
|
1042
|
+
busyMbps: this.config.proxy.busyMbps,
|
|
1043
|
+
whitelist: this.config.proxy.whitelist ?? [],
|
|
1034
1044
|
resolvePeerName: (srcIp) => this.ipam.resolveIp(srcIp)?.name,
|
|
1045
|
+
resolvePeerUserid: (srcIp) => this.ipam.resolveIp(srcIp)?.carrierId,
|
|
1035
1046
|
// Per-tunnel audit is opt-in: on a busy exit it floods the
|
|
1036
1047
|
// log (the 13 GB audit.log bug). Off by default; ACL and
|
|
1037
1048
|
// connect events are still always audited.
|
|
@@ -37,6 +37,16 @@ export interface ConnectProxyOptions {
|
|
|
37
37
|
egressBindIp?: string;
|
|
38
38
|
/** Hook for resolving a source IP to a friendly peer name (audit/log). */
|
|
39
39
|
resolvePeerName?: (sourceIp: string) => string | undefined;
|
|
40
|
+
/** Hook for resolving a source IP to the peer's Carrier userid. Used to test
|
|
41
|
+
* the busy-tier whitelist (which is keyed by userid). */
|
|
42
|
+
resolvePeerUserid?: (sourceIp: string) => string | undefined;
|
|
43
|
+
/** Aggregate egress throughput (Mbps) at/above which the exit counts as
|
|
44
|
+
* "busy": while busy, only whitelisted peers get NEW tunnels. 0/undefined =
|
|
45
|
+
* never busy = free for all (default, current behaviour). */
|
|
46
|
+
busyMbps?: number;
|
|
47
|
+
/** Carrier userids (or virtual IPs) always served even when busy — the paid /
|
|
48
|
+
* priority set. */
|
|
49
|
+
whitelist?: string[];
|
|
40
50
|
/** Called when a tunnel opens. */
|
|
41
51
|
onTunnelOpen?: (info: {
|
|
42
52
|
src: string;
|
|
@@ -70,6 +80,15 @@ export interface ConnectProxyStats {
|
|
|
70
80
|
totalOpened: number;
|
|
71
81
|
totalRefused: number;
|
|
72
82
|
bytesTransferred: number;
|
|
83
|
+
/** Smoothed aggregate egress throughput right now (Mbps). */
|
|
84
|
+
mbps: number;
|
|
85
|
+
/** Whether the exit is currently "busy" (mbps >= busyMbps threshold). When
|
|
86
|
+
* true, only whitelisted peers get new tunnels. */
|
|
87
|
+
busy: boolean;
|
|
88
|
+
/** Configured busy threshold (Mbps); 0 = never busy (free for all). */
|
|
89
|
+
busyMbps: number;
|
|
90
|
+
/** Number of userids on the busy-tier whitelist. */
|
|
91
|
+
whitelistSize: number;
|
|
73
92
|
/** Per-client breakdown, heaviest (by bytes) first. */
|
|
74
93
|
peers: ProxyPeerUsage[];
|
|
75
94
|
}
|
|
@@ -80,12 +99,34 @@ export declare class ConnectProxy {
|
|
|
80
99
|
private stats;
|
|
81
100
|
/** Live per-client usage, keyed by source virtual IP. */
|
|
82
101
|
private perPeer;
|
|
102
|
+
/** Smoothed aggregate egress throughput (Mbps), refreshed once a second. */
|
|
103
|
+
private mbps;
|
|
104
|
+
/** Bytes seen since the last meter tick (reset every second). */
|
|
105
|
+
private windowBytes;
|
|
106
|
+
private meterTimer?;
|
|
83
107
|
constructor(opts: ConnectProxyOptions);
|
|
108
|
+
/** The exit is "busy" when its smoothed egress rate is at/above busyMbps.
|
|
109
|
+
* A 0/undefined threshold means it is never busy — free for all (default). */
|
|
110
|
+
private isBusy;
|
|
111
|
+
/** A peer is whitelisted (always served, even when busy) if its userid — or,
|
|
112
|
+
* as a fallback, its source virtual IP — is on the whitelist. */
|
|
113
|
+
private isWhitelisted;
|
|
84
114
|
/** Get-or-create the per-client usage row for a source IP. */
|
|
85
115
|
private peerRow;
|
|
86
116
|
start(): Promise<void>;
|
|
87
117
|
stop(): Promise<void>;
|
|
88
118
|
getStats(): ConnectProxyStats;
|
|
119
|
+
/**
|
|
120
|
+
* Replace the busy-tier whitelist and/or threshold on a RUNNING proxy without
|
|
121
|
+
* a restart — handleConnect reads both at CONNECT time, so this takes effect
|
|
122
|
+
* on the next tunnel. Restarting would drop every Carrier session and trigger
|
|
123
|
+
* a slow reconvergence storm; live update is what makes `agentnet proxy
|
|
124
|
+
* whitelist add <userid>` (and a future billing webhook) instant.
|
|
125
|
+
*/
|
|
126
|
+
updateAdmission(opts: {
|
|
127
|
+
whitelist?: string[];
|
|
128
|
+
busyMbps?: number;
|
|
129
|
+
}): void;
|
|
89
130
|
/**
|
|
90
131
|
* Update the host allowlist (and optionally the port allowlist) of a
|
|
91
132
|
* RUNNING proxy without restarting it. handleConnect reads
|
|
@@ -29,6 +29,11 @@ export class ConnectProxy {
|
|
|
29
29
|
stats;
|
|
30
30
|
/** Live per-client usage, keyed by source virtual IP. */
|
|
31
31
|
perPeer = new Map();
|
|
32
|
+
/** Smoothed aggregate egress throughput (Mbps), refreshed once a second. */
|
|
33
|
+
mbps = 0;
|
|
34
|
+
/** Bytes seen since the last meter tick (reset every second). */
|
|
35
|
+
windowBytes = 0;
|
|
36
|
+
meterTimer;
|
|
32
37
|
constructor(opts) {
|
|
33
38
|
this.opts = opts;
|
|
34
39
|
this.logger = new Logger({ prefix: "ConnectProxy" });
|
|
@@ -40,9 +45,30 @@ export class ConnectProxy {
|
|
|
40
45
|
totalOpened: 0,
|
|
41
46
|
totalRefused: 0,
|
|
42
47
|
bytesTransferred: 0,
|
|
48
|
+
mbps: 0,
|
|
49
|
+
busy: false,
|
|
50
|
+
busyMbps: opts.busyMbps ?? 0,
|
|
51
|
+
whitelistSize: opts.whitelist?.length ?? 0,
|
|
43
52
|
peers: [],
|
|
44
53
|
};
|
|
45
54
|
}
|
|
55
|
+
/** The exit is "busy" when its smoothed egress rate is at/above busyMbps.
|
|
56
|
+
* A 0/undefined threshold means it is never busy — free for all (default). */
|
|
57
|
+
isBusy() {
|
|
58
|
+
const limit = this.opts.busyMbps ?? 0;
|
|
59
|
+
return limit > 0 && this.mbps >= limit;
|
|
60
|
+
}
|
|
61
|
+
/** A peer is whitelisted (always served, even when busy) if its userid — or,
|
|
62
|
+
* as a fallback, its source virtual IP — is on the whitelist. */
|
|
63
|
+
isWhitelisted(src) {
|
|
64
|
+
const wl = this.opts.whitelist;
|
|
65
|
+
if (!wl || wl.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
if (wl.includes(src))
|
|
68
|
+
return true; // whitelist-by-IP convenience
|
|
69
|
+
const uid = this.opts.resolvePeerUserid?.(src);
|
|
70
|
+
return uid !== undefined && wl.includes(uid);
|
|
71
|
+
}
|
|
46
72
|
/** Get-or-create the per-client usage row for a source IP. */
|
|
47
73
|
peerRow(src, srcName) {
|
|
48
74
|
let row = this.perPeer.get(src);
|
|
@@ -85,11 +111,27 @@ export class ConnectProxy {
|
|
|
85
111
|
});
|
|
86
112
|
});
|
|
87
113
|
this.stats.startedAt = Date.now();
|
|
88
|
-
|
|
114
|
+
// Throughput meter for busy-detection: sum bytes each second → Mbps, then
|
|
115
|
+
// EWMA-smooth so a bursty video stream (or one idle second between segments)
|
|
116
|
+
// doesn't flip "busy" on/off and readmit a flood of free users mid-spike.
|
|
117
|
+
this.meterTimer = setInterval(() => {
|
|
118
|
+
const inst = (this.windowBytes * 8) / 1e6; // Mbps in the last second
|
|
119
|
+
this.windowBytes = 0;
|
|
120
|
+
this.mbps = 0.5 * this.mbps + 0.5 * inst;
|
|
121
|
+
}, 1000);
|
|
122
|
+
this.meterTimer.unref?.();
|
|
123
|
+
this.logger.info(`Listening on ${this.opts.bindIp}:${this.opts.port}` +
|
|
124
|
+
(this.opts.busyMbps
|
|
125
|
+
? ` · free-tier admission: busy>=${this.opts.busyMbps}Mbps → whitelist-only (${this.opts.whitelist?.length ?? 0} userid(s))`
|
|
126
|
+
: " · free for all (no busy threshold set)"));
|
|
89
127
|
}
|
|
90
128
|
async stop() {
|
|
91
129
|
if (!this.server)
|
|
92
130
|
return;
|
|
131
|
+
if (this.meterTimer) {
|
|
132
|
+
clearInterval(this.meterTimer);
|
|
133
|
+
this.meterTimer = undefined;
|
|
134
|
+
}
|
|
93
135
|
const srv = this.server;
|
|
94
136
|
this.server = null;
|
|
95
137
|
await new Promise((res) => {
|
|
@@ -101,7 +143,29 @@ export class ConnectProxy {
|
|
|
101
143
|
const peers = [...this.perPeer.values()]
|
|
102
144
|
.sort((a, b) => b.bytesTransferred - a.bytesTransferred || b.totalOpened - a.totalOpened)
|
|
103
145
|
.map((p) => ({ ...p }));
|
|
104
|
-
return {
|
|
146
|
+
return {
|
|
147
|
+
...this.stats,
|
|
148
|
+
mbps: Math.round(this.mbps * 10) / 10,
|
|
149
|
+
busy: this.isBusy(),
|
|
150
|
+
busyMbps: this.opts.busyMbps ?? 0,
|
|
151
|
+
whitelistSize: this.opts.whitelist?.length ?? 0,
|
|
152
|
+
peers,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Replace the busy-tier whitelist and/or threshold on a RUNNING proxy without
|
|
157
|
+
* a restart — handleConnect reads both at CONNECT time, so this takes effect
|
|
158
|
+
* on the next tunnel. Restarting would drop every Carrier session and trigger
|
|
159
|
+
* a slow reconvergence storm; live update is what makes `agentnet proxy
|
|
160
|
+
* whitelist add <userid>` (and a future billing webhook) instant.
|
|
161
|
+
*/
|
|
162
|
+
updateAdmission(opts) {
|
|
163
|
+
if (opts.whitelist !== undefined)
|
|
164
|
+
this.opts.whitelist = opts.whitelist;
|
|
165
|
+
if (opts.busyMbps !== undefined)
|
|
166
|
+
this.opts.busyMbps = opts.busyMbps;
|
|
167
|
+
this.logger.info(`Admission updated live: busy>=${this.opts.busyMbps ?? 0}Mbps, ` +
|
|
168
|
+
`whitelist=${this.opts.whitelist?.length ?? 0} userid(s)`);
|
|
105
169
|
}
|
|
106
170
|
/**
|
|
107
171
|
* Update the host allowlist (and optionally the port allowlist) of a
|
|
@@ -149,6 +213,14 @@ export class ConnectProxy {
|
|
|
149
213
|
this.refuse(clientSocket, src, srcName, `${host}:${port}`, 403, "host not in allowHosts");
|
|
150
214
|
return;
|
|
151
215
|
}
|
|
216
|
+
// Free-tier admission control. While the exit is busy (egress at/above the
|
|
217
|
+
// configured busyMbps), only whitelisted (paid/priority) peers get NEW
|
|
218
|
+
// tunnels; free users are told to retry. Existing tunnels are never cut, and
|
|
219
|
+
// an exit with no busyMbps set is never "busy" (free for all — the default).
|
|
220
|
+
if (this.isBusy() && !this.isWhitelisted(src)) {
|
|
221
|
+
this.refuse(clientSocket, src, srcName, `${host}:${port}`, 503, `exit busy (${Math.round(this.mbps)}Mbps) — free capacity full; whitelisted/paid peers only. Retry shortly.`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
152
224
|
let bytes = 0;
|
|
153
225
|
let closed = false;
|
|
154
226
|
let lastBytes = -1;
|
|
@@ -199,6 +271,7 @@ export class ConnectProxy {
|
|
|
199
271
|
});
|
|
200
272
|
const onData = (chunk) => {
|
|
201
273
|
bytes += chunk.length;
|
|
274
|
+
this.windowBytes += chunk.length; // feed the busy-detection meter
|
|
202
275
|
};
|
|
203
276
|
upstream.on("data", onData);
|
|
204
277
|
clientSocket.on("data", onData);
|
|
@@ -246,7 +319,7 @@ export class ConnectProxy {
|
|
|
246
319
|
row.lastSeenMs = Date.now();
|
|
247
320
|
this.logger.info(`tunnel refused ${src}${srcName ? ` (${srcName})` : ""} -> ${target}: ${reason}`);
|
|
248
321
|
try {
|
|
249
|
-
const statusText = code === 400 ? "Bad Request" : "Forbidden";
|
|
322
|
+
const statusText = code === 400 ? "Bad Request" : code === 503 ? "Service Unavailable" : "Forbidden";
|
|
250
323
|
clientSocket.write(`HTTP/1.1 ${code} ${statusText}\r\nProxy-Agent: decentlan\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n${reason}\n`);
|
|
251
324
|
}
|
|
252
325
|
catch {
|
|
@@ -33,7 +33,15 @@ export const BUILTIN_REGION_DOMAINS = {
|
|
|
33
33
|
china: [
|
|
34
34
|
".cn",
|
|
35
35
|
".cctv.com", ".cctvpic.com", ".cntv.cn", ".cctv.cn",
|
|
36
|
-
|
|
36
|
+
// CCTV live rotates its HLS/FLV across several third-party video CDNs; the
|
|
37
|
+
// decision API hands back whichever is up, so ALL of them must ride a China
|
|
38
|
+
// exit or the segment host leaks direct → the CDN geo-blocks ("您所在的地区,
|
|
39
|
+
// 暂不支持播放该视频"). Ali is .v.myalicdn.com; Kingsoft is .v.kcdnvip.com
|
|
40
|
+
// (+ its ks-cdn/ksyuncdn CNAME targets); Tencent/Volcano are covered below.
|
|
41
|
+
// These .com CDNs aren't caught by the .cn rule and the geo-classifier misses
|
|
42
|
+
// subdomains that resolve to a non-China edge, so pin them statically.
|
|
43
|
+
".v.myalicdn.com", ".myalicdn.com",
|
|
44
|
+
".kcdnvip.com", ".ks-cdn.com", ".ksyuncdn.com",
|
|
37
45
|
".bilibili.com", ".biliapi.net", ".bilivideo.com", ".bilivideo.cn", ".hdslb.com",
|
|
38
46
|
".youku.com", ".iqiyi.com", ".iq.com", ".mgtv.com", ".hitv.com",
|
|
39
47
|
".miguvideo.com", ".cmvideo.cn", ".migu.cn",
|
|
@@ -535,10 +543,19 @@ export function startMultiExitRouter(opts) {
|
|
|
535
543
|
// so we just fail over. BUT if THIS exit returns non-200 for many targets
|
|
536
544
|
// in a row (reset on any success), its own proxy is broken — e.g. it 502s
|
|
537
545
|
// everything (dead upstream / wrong version). Trip it so it stops eating
|
|
538
|
-
// the first pick on every request.
|
|
539
|
-
//
|
|
546
|
+
// the first pick on every request.
|
|
547
|
+
//
|
|
548
|
+
// Guard with the health probe: only trip an exit that is ALSO failing its
|
|
549
|
+
// probe (its proxy is genuinely unreachable). A 502 means the exit's proxy
|
|
550
|
+
// ANSWERED — it's alive — and the ORIGIN failed. When several DEAD origins
|
|
551
|
+
// are hit in a row (observed on CCTV: js.data.cctv.cn + p.data.cctv.cn,
|
|
552
|
+
// both unreachable even from inside China, 502 on every exit), the streak
|
|
553
|
+
// reaches the threshold and would trip a perfectly healthy exit out of the
|
|
554
|
+
// pool mid-stream (sh got tripped this way). The probe is the real
|
|
555
|
+
// proxy-liveness signal; a run of origin 502s alone must not evict an exit.
|
|
540
556
|
exit.consecUpstreamFails++;
|
|
541
|
-
|
|
557
|
+
const proxyAlive = exit.healthy || (exit.lastSuccessMs !== null && Date.now() - exit.lastSuccessMs < RECENT_SUCCESS_MS);
|
|
558
|
+
if (!proxyAlive && exit.consecUpstreamFails >= TRIP_AFTER_UPSTREAM_FAILS && exit.trippedUntil <= Date.now()) {
|
|
542
559
|
exit.trippedUntil = Date.now() + TRIP_COOLDOWN_MS;
|
|
543
560
|
exit.healthy = false;
|
|
544
561
|
log(`✗ ${exit.name} [${exit.region}] TRIPPED for ${TRIP_COOLDOWN_MS / 1000}s (proxy returned ${status.trim()} for ${exit.consecUpstreamFails} targets in a row) — excluding from pool`);
|
package/dist/tun/tun-device.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 4-byte big-endian length-prefix framing.
|
|
9
9
|
*/
|
|
10
10
|
import { EventEmitter } from "events";
|
|
11
|
-
import { spawn } from "child_process";
|
|
11
|
+
import { spawn, execFileSync } from "child_process";
|
|
12
12
|
import { resolve, dirname } from "path";
|
|
13
13
|
import { existsSync } from "fs";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
@@ -181,6 +181,24 @@ export class TunDevice extends EventEmitter {
|
|
|
181
181
|
throw new Error(`TUN helper binary not found: ${helperPath || "(none)"}\n` +
|
|
182
182
|
`Build it with: cd helper/tun-helper && go build -o bin/tun-helper-linux-amd64 .`);
|
|
183
183
|
}
|
|
184
|
+
// On Linux the helper creates a FIXED-name interface (agentnet0). If a
|
|
185
|
+
// prior helper died uncleanly — e.g. "read tun: file descriptor in bad
|
|
186
|
+
// state" during a rebuild, or a hard kill — the interface lingers, and the
|
|
187
|
+
// new helper fails to create it (EEXIST) → the daemon loops forever on
|
|
188
|
+
// "TUN rebuild failed (code=1)" until an operator manually runs
|
|
189
|
+
// `ip link delete agentnet0`. Pre-delete any stale interface so the
|
|
190
|
+
// (re)build is idempotent and self-heals. Best-effort: on a clean first
|
|
191
|
+
// start the interface is absent and this no-ops; macOS uses a dynamic
|
|
192
|
+
// utun so there is no fixed-name collision and we skip it.
|
|
193
|
+
if (process.platform === "linux") {
|
|
194
|
+
try {
|
|
195
|
+
execFileSync("ip", ["link", "delete", this.config.name], { stdio: "ignore" });
|
|
196
|
+
this.logger.info(`Removed stale interface ${this.config.name} before (re)build`);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Interface didn't exist — normal on a clean start; nothing to clean up.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
184
202
|
this.logger.info(`Spawning TUN helper: ${helperPath}`);
|
|
185
203
|
this.helper = spawn(helperPath, ["--name", this.config.name], {
|
|
186
204
|
stdio: ["pipe", "pipe", "pipe"],
|
package/dist/types.d.ts
CHANGED
|
@@ -139,6 +139,8 @@ export interface ProxyConfig {
|
|
|
139
139
|
allowConnectPorts?: number[];
|
|
140
140
|
auditTunnels?: boolean;
|
|
141
141
|
egress?: string;
|
|
142
|
+
busyMbps?: number;
|
|
143
|
+
whitelist?: string[];
|
|
142
144
|
}
|
|
143
145
|
export interface FriendsConfig {
|
|
144
146
|
/** When true (default), the running daemon auto-accepts incoming
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.155",
|
|
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",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"build:console": "node scripts/build-console.mjs"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@decentnetwork/dora": "^0.1.
|
|
81
|
+
"@decentnetwork/dora": "^0.1.13",
|
|
82
82
|
"@decentnetwork/peer": "^0.1.79",
|
|
83
83
|
"ink": "^5.2.1",
|
|
84
84
|
"js-yaml": "^4.1.0",
|