@decentnetwork/lan 0.1.137 → 0.1.138
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 +21 -0
- package/dist/cli/commands.js +96 -0
- package/dist/cli/index.js +14 -1
- package/dist/daemon/ipc.d.ts +7 -1
- package/dist/daemon/ipc.js +4 -0
- package/dist/daemon/server.js +31 -0
- package/dist/proxy/connect-proxy.d.ts +17 -0
- package/dist/proxy/connect-proxy.js +29 -1
- package/dist/ui/desktop/app.js +1 -1
- package/dist/ui/desktop/index.html +1 -1
- package/dist/ui/server.js +1 -1
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -218,6 +218,27 @@ export declare function cmdProxyRevokeHost(args: {
|
|
|
218
218
|
export declare function cmdProxyListHosts(args: {
|
|
219
219
|
configDir?: string;
|
|
220
220
|
}): Promise<void>;
|
|
221
|
+
/**
|
|
222
|
+
* `agentnet proxy who` — show who is using THIS node's exit right now:
|
|
223
|
+
* per-client active tunnels, total tunnels opened, bytes moved, and refusals.
|
|
224
|
+
* Reads live stats from the running daemon (diag → proxy). Lets an exit
|
|
225
|
+
* operator spot a heavy user before deciding to `agentnet revoke` them.
|
|
226
|
+
*/
|
|
227
|
+
export declare function cmdProxyWho(args: {
|
|
228
|
+
configDir?: string;
|
|
229
|
+
}): Promise<void>;
|
|
230
|
+
/**
|
|
231
|
+
* `agentnet proxy block|allow <peer>` — live per-peer exit access control.
|
|
232
|
+
* Under the default "allow" policy, every friend can tunnel through this exit;
|
|
233
|
+
* `block` adds a deny rule on the proxy port (applied to the running daemon
|
|
234
|
+
* instantly, no restart), `allow` removes it. Accepts a userid or a friendly
|
|
235
|
+
* name (resolved via the live roster).
|
|
236
|
+
*/
|
|
237
|
+
export declare function cmdProxyAccess(args: {
|
|
238
|
+
peer: string;
|
|
239
|
+
allow: boolean;
|
|
240
|
+
configDir?: string;
|
|
241
|
+
}): Promise<void>;
|
|
221
242
|
/**
|
|
222
243
|
* Print the env-var line for a peer to use this node's proxy.
|
|
223
244
|
*/
|
package/dist/cli/commands.js
CHANGED
|
@@ -1040,6 +1040,102 @@ export async function cmdProxyListHosts(args) {
|
|
|
1040
1040
|
for (const h of allowHosts)
|
|
1041
1041
|
console.log(` ${h}`);
|
|
1042
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* `agentnet proxy who` — show who is using THIS node's exit right now:
|
|
1045
|
+
* per-client active tunnels, total tunnels opened, bytes moved, and refusals.
|
|
1046
|
+
* Reads live stats from the running daemon (diag → proxy). Lets an exit
|
|
1047
|
+
* operator spot a heavy user before deciding to `agentnet revoke` them.
|
|
1048
|
+
*/
|
|
1049
|
+
export async function cmdProxyWho(args) {
|
|
1050
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
1051
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
1052
|
+
const pid = daemonPid(config);
|
|
1053
|
+
if (pid === null) {
|
|
1054
|
+
console.error("No running daemon found for this config — start it with 'agentnet up'.");
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
const res = await ipcCall(config, { op: "diag" });
|
|
1058
|
+
if (!res.ok)
|
|
1059
|
+
throw new Error(`Daemon diag failed: ${res.error}`);
|
|
1060
|
+
const proxy = res.data?.proxy;
|
|
1061
|
+
if (!proxy) {
|
|
1062
|
+
console.log("The CONNECT proxy isn't running on this node (nobody can use it as an exit).");
|
|
1063
|
+
console.log("Enable it with: agentnet proxy enable");
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const fmtBytes = (n) => {
|
|
1067
|
+
if (n >= 1024 ** 3)
|
|
1068
|
+
return `${(n / 1024 ** 3).toFixed(2)} GB`;
|
|
1069
|
+
if (n >= 1024 ** 2)
|
|
1070
|
+
return `${(n / 1024 ** 2).toFixed(1)} MB`;
|
|
1071
|
+
if (n >= 1024)
|
|
1072
|
+
return `${(n / 1024).toFixed(0)} KB`;
|
|
1073
|
+
return `${n} B`;
|
|
1074
|
+
};
|
|
1075
|
+
const ago = (ms) => {
|
|
1076
|
+
if (!ms)
|
|
1077
|
+
return "—";
|
|
1078
|
+
const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
|
|
1079
|
+
if (s < 60)
|
|
1080
|
+
return `${s}s ago`;
|
|
1081
|
+
if (s < 3600)
|
|
1082
|
+
return `${Math.floor(s / 60)}m ago`;
|
|
1083
|
+
return `${Math.floor(s / 3600)}h ago`;
|
|
1084
|
+
};
|
|
1085
|
+
console.log(`Exit proxy: ${proxy.active} active tunnel(s), ${proxy.totalOpened} opened total, ` +
|
|
1086
|
+
`${fmtBytes(proxy.bytesTransferred)} moved, ${proxy.totalRefused} refused.`);
|
|
1087
|
+
if (!proxy.peers.length) {
|
|
1088
|
+
console.log("No clients have used this exit yet.");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
console.log("");
|
|
1092
|
+
console.log(" CLIENT ACTIVE OPENED BYTES REFUSED LAST");
|
|
1093
|
+
for (const p of proxy.peers) {
|
|
1094
|
+
const who = (p.srcName ? `${p.srcName} (${p.src})` : p.src).padEnd(22).slice(0, 22);
|
|
1095
|
+
console.log(` ${who} ${String(p.active).padStart(6)} ${String(p.totalOpened).padStart(6)} ` +
|
|
1096
|
+
`${fmtBytes(p.bytesTransferred).padEnd(11)} ${String(p.totalRefused).padStart(7)} ${ago(p.lastSeenMs)}`);
|
|
1097
|
+
}
|
|
1098
|
+
console.log("");
|
|
1099
|
+
console.log("Cut off a heavy user: agentnet proxy block <userid-or-name>");
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* `agentnet proxy block|allow <peer>` — live per-peer exit access control.
|
|
1103
|
+
* Under the default "allow" policy, every friend can tunnel through this exit;
|
|
1104
|
+
* `block` adds a deny rule on the proxy port (applied to the running daemon
|
|
1105
|
+
* instantly, no restart), `allow` removes it. Accepts a userid or a friendly
|
|
1106
|
+
* name (resolved via the live roster).
|
|
1107
|
+
*/
|
|
1108
|
+
export async function cmdProxyAccess(args) {
|
|
1109
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
1110
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
1111
|
+
const pid = daemonPid(config);
|
|
1112
|
+
if (pid === null) {
|
|
1113
|
+
console.error("No running daemon found for this config — start it with 'agentnet up'.");
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
// Resolve a friendly name to its userid so the deny rule keys on the stable id.
|
|
1117
|
+
let userid = args.peer;
|
|
1118
|
+
try {
|
|
1119
|
+
const { peers } = await fetchLiveIpam(config);
|
|
1120
|
+
const stripDomain = (s) => s.replace(/\.[A-Za-z][A-Za-z0-9-]*$/, "");
|
|
1121
|
+
const hit = peers.find((p) => p.carrierId === args.peer || p.name === args.peer || p.name === stripDomain(args.peer));
|
|
1122
|
+
if (hit?.carrierId)
|
|
1123
|
+
userid = hit.carrierId;
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
// fall back to the raw arg — the daemon matches on userid OR name
|
|
1127
|
+
}
|
|
1128
|
+
const res = await ipcCall(config, { op: "proxy-access", userid, enabled: args.allow });
|
|
1129
|
+
if (!res.ok)
|
|
1130
|
+
throw new Error(`Daemon refused: ${res.error}`);
|
|
1131
|
+
if (args.allow) {
|
|
1132
|
+
console.log(`✓ ${args.peer} may use this exit again (block removed, live).`);
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
console.log(`✓ ${args.peer} is now BLOCKED from this exit (live — existing tunnels drop on next reconnect).`);
|
|
1136
|
+
console.log(" Re-allow with: agentnet proxy allow " + args.peer);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1043
1139
|
/**
|
|
1044
1140
|
* Print the env-var line for a peer to use this node's proxy.
|
|
1045
1141
|
*/
|
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, 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, 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,19 @@ 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("who", "Show who is using THIS node's exit (per-peer tunnels + bytes)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
322
|
+
await cmdProxyWho({ configDir: argv["config-dir"] });
|
|
323
|
+
})
|
|
324
|
+
.command("block <peer>", "Block a peer from using THIS node's exit (live, no restart)", (yy) => yy
|
|
325
|
+
.positional("peer", { type: "string", demandOption: true })
|
|
326
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
327
|
+
await cmdProxyAccess({ peer: argv.peer, allow: false, configDir: argv["config-dir"] });
|
|
328
|
+
})
|
|
329
|
+
.command("allow <peer>", "Re-allow a previously-blocked peer to use THIS node's exit (live)", (yy) => yy
|
|
330
|
+
.positional("peer", { type: "string", demandOption: true })
|
|
331
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
332
|
+
await cmdProxyAccess({ peer: argv.peer, allow: true, configDir: argv["config-dir"] });
|
|
320
333
|
})
|
|
321
334
|
.command("use", "Print env vars to route HTTPS via a peer's proxy node", (yy) => yy
|
|
322
335
|
.option("peer", { type: "string", demandOption: true, describe: "Peer name or carrier ID" })
|
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -84,6 +84,12 @@ export interface IpcHandlers {
|
|
|
84
84
|
* restart (which drops every Carrier session). Returns the applied
|
|
85
85
|
* allowlist for the CLI to echo. */
|
|
86
86
|
proxyReload: () => Promise<Record<string, unknown>>;
|
|
87
|
+
/** Block or allow a specific peer's access to THIS node's exit proxy port,
|
|
88
|
+
* LIVE (mutates the running ACL + persists policy.yaml). `allow=true`
|
|
89
|
+
* removes the block; `allow=false` adds a deny rule so the peer can no
|
|
90
|
+
* longer tunnel through this exit. Lets an operator cut off a heavy user
|
|
91
|
+
* without restarting the daemon. */
|
|
92
|
+
proxyAccess: (userid: string, allow: boolean) => Promise<Record<string, unknown>>;
|
|
87
93
|
/** Re-exec the running daemon with its original argv. The new
|
|
88
94
|
* process inherits the current process's privileges (so a
|
|
89
95
|
* root-owned daemon re-execs as root with no sudo prompt),
|
|
@@ -94,7 +100,7 @@ export interface IpcHandlers {
|
|
|
94
100
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
95
101
|
}
|
|
96
102
|
export interface IpcRequest {
|
|
97
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "self-restart";
|
|
103
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
98
104
|
address?: string;
|
|
99
105
|
hello?: string;
|
|
100
106
|
userid?: string;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -233,6 +233,10 @@ export class IpcServer {
|
|
|
233
233
|
}
|
|
234
234
|
case "proxy-reload":
|
|
235
235
|
return await this.handlers.proxyReload();
|
|
236
|
+
case "proxy-access":
|
|
237
|
+
if (!req.userid)
|
|
238
|
+
throw new Error("proxy-access requires a userid");
|
|
239
|
+
return await this.handlers.proxyAccess(req.userid, req.enabled !== false);
|
|
236
240
|
case "self-restart":
|
|
237
241
|
return await this.handlers.selfRestart();
|
|
238
242
|
default:
|
package/dist/daemon/server.js
CHANGED
|
@@ -507,6 +507,33 @@ export class DaemonServer {
|
|
|
507
507
|
}
|
|
508
508
|
return { applied: true, allowHosts };
|
|
509
509
|
},
|
|
510
|
+
proxyAccess: async (userid, allow) => {
|
|
511
|
+
// Live block/allow a peer's access to THIS node's exit proxy port.
|
|
512
|
+
// Mutates the running ACL (evaluated per-packet, so it's instant) and
|
|
513
|
+
// persists policy.yaml. Under the default "allow" policy, a friend can
|
|
514
|
+
// use the exit unless we add an explicit DENY — so "block" adds a deny
|
|
515
|
+
// rule on the proxy port and "allow" removes the peer's rules.
|
|
516
|
+
if (!this.policy)
|
|
517
|
+
throw new Error("ACL policy not initialised");
|
|
518
|
+
const port = this.config.proxy?.port ?? 8888;
|
|
519
|
+
// Accept a userid or a friendly name; store the rule under whatever
|
|
520
|
+
// identifier the ACL matches on (both are checked at evaluate time).
|
|
521
|
+
const peerKey = userid;
|
|
522
|
+
if (allow) {
|
|
523
|
+
this.policy.removePeer(peerKey);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
this.policy.addRule({
|
|
527
|
+
peer: peerKey,
|
|
528
|
+
direction: "both",
|
|
529
|
+
deny: [{ proto: "tcp", port, purpose: "exit-block" }],
|
|
530
|
+
audit: true,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
await this.policy.save();
|
|
534
|
+
this.logger.info(`Exit access ${allow ? "ALLOWED" : "BLOCKED"} for ${peerKey.slice(0, 12)} (tcp:${port})`);
|
|
535
|
+
return { applied: true, userid: peerKey, allowed: allow, port };
|
|
536
|
+
},
|
|
510
537
|
selfRestart: async () => {
|
|
511
538
|
// Detach a child process that waits briefly and then re-execs
|
|
512
539
|
// the same argv we were started with. Inherits privileges
|
|
@@ -586,6 +613,10 @@ export class DaemonServer {
|
|
|
586
613
|
session: this.peerManager?.getSessionStatus(f.pubkey) ?? null,
|
|
587
614
|
})),
|
|
588
615
|
ipam: ipamPeers,
|
|
616
|
+
// Exit-operator view: live per-client usage of THIS node's CONNECT
|
|
617
|
+
// proxy (who's tunnelling through me + how much). Null when the
|
|
618
|
+
// proxy isn't running.
|
|
619
|
+
proxy: this.connectProxy?.getStats() ?? null,
|
|
589
620
|
};
|
|
590
621
|
},
|
|
591
622
|
});
|
|
@@ -51,6 +51,17 @@ export interface ConnectProxyOptions {
|
|
|
51
51
|
bytesTransferred: number;
|
|
52
52
|
}) => void;
|
|
53
53
|
}
|
|
54
|
+
/** Per-client (per source virtual-IP) usage of the exit, so the operator can
|
|
55
|
+
* see WHO is using their exit and HOW MUCH — spot heavy/abusive users. */
|
|
56
|
+
export interface ProxyPeerUsage {
|
|
57
|
+
src: string;
|
|
58
|
+
srcName?: string;
|
|
59
|
+
active: number;
|
|
60
|
+
totalOpened: number;
|
|
61
|
+
totalRefused: number;
|
|
62
|
+
bytesTransferred: number;
|
|
63
|
+
lastSeenMs: number;
|
|
64
|
+
}
|
|
54
65
|
export interface ConnectProxyStats {
|
|
55
66
|
bindIp: string;
|
|
56
67
|
port: number;
|
|
@@ -59,13 +70,19 @@ export interface ConnectProxyStats {
|
|
|
59
70
|
totalOpened: number;
|
|
60
71
|
totalRefused: number;
|
|
61
72
|
bytesTransferred: number;
|
|
73
|
+
/** Per-client breakdown, heaviest (by bytes) first. */
|
|
74
|
+
peers: ProxyPeerUsage[];
|
|
62
75
|
}
|
|
63
76
|
export declare class ConnectProxy {
|
|
64
77
|
private opts;
|
|
65
78
|
private server;
|
|
66
79
|
private logger;
|
|
67
80
|
private stats;
|
|
81
|
+
/** Live per-client usage, keyed by source virtual IP. */
|
|
82
|
+
private perPeer;
|
|
68
83
|
constructor(opts: ConnectProxyOptions);
|
|
84
|
+
/** Get-or-create the per-client usage row for a source IP. */
|
|
85
|
+
private peerRow;
|
|
69
86
|
start(): Promise<void>;
|
|
70
87
|
stop(): Promise<void>;
|
|
71
88
|
getStats(): ConnectProxyStats;
|
|
@@ -27,6 +27,8 @@ export class ConnectProxy {
|
|
|
27
27
|
server = null;
|
|
28
28
|
logger;
|
|
29
29
|
stats;
|
|
30
|
+
/** Live per-client usage, keyed by source virtual IP. */
|
|
31
|
+
perPeer = new Map();
|
|
30
32
|
constructor(opts) {
|
|
31
33
|
this.opts = opts;
|
|
32
34
|
this.logger = new Logger({ prefix: "ConnectProxy" });
|
|
@@ -38,8 +40,20 @@ export class ConnectProxy {
|
|
|
38
40
|
totalOpened: 0,
|
|
39
41
|
totalRefused: 0,
|
|
40
42
|
bytesTransferred: 0,
|
|
43
|
+
peers: [],
|
|
41
44
|
};
|
|
42
45
|
}
|
|
46
|
+
/** Get-or-create the per-client usage row for a source IP. */
|
|
47
|
+
peerRow(src, srcName) {
|
|
48
|
+
let row = this.perPeer.get(src);
|
|
49
|
+
if (!row) {
|
|
50
|
+
row = { src, srcName, active: 0, totalOpened: 0, totalRefused: 0, bytesTransferred: 0, lastSeenMs: 0 };
|
|
51
|
+
this.perPeer.set(src, row);
|
|
52
|
+
}
|
|
53
|
+
if (srcName && !row.srcName)
|
|
54
|
+
row.srcName = srcName;
|
|
55
|
+
return row;
|
|
56
|
+
}
|
|
43
57
|
async start() {
|
|
44
58
|
if (this.server) {
|
|
45
59
|
throw new Error("ConnectProxy already started");
|
|
@@ -84,7 +98,10 @@ export class ConnectProxy {
|
|
|
84
98
|
this.logger.info("Stopped");
|
|
85
99
|
}
|
|
86
100
|
getStats() {
|
|
87
|
-
|
|
101
|
+
const peers = [...this.perPeer.values()]
|
|
102
|
+
.sort((a, b) => b.bytesTransferred - a.bytesTransferred || b.totalOpened - a.totalOpened)
|
|
103
|
+
.map((p) => ({ ...p }));
|
|
104
|
+
return { ...this.stats, peers };
|
|
88
105
|
}
|
|
89
106
|
/**
|
|
90
107
|
* Update the host allowlist (and optionally the port allowlist) of a
|
|
@@ -139,6 +156,10 @@ export class ConnectProxy {
|
|
|
139
156
|
clientSocket.destroy();
|
|
140
157
|
this.stats.active = Math.max(0, this.stats.active - 1);
|
|
141
158
|
this.stats.bytesTransferred += bytes;
|
|
159
|
+
const row = this.peerRow(src, srcName);
|
|
160
|
+
row.active = Math.max(0, row.active - 1);
|
|
161
|
+
row.bytesTransferred += bytes;
|
|
162
|
+
row.lastSeenMs = Date.now();
|
|
142
163
|
this.opts.onTunnelClose?.({
|
|
143
164
|
src,
|
|
144
165
|
srcName,
|
|
@@ -161,6 +182,10 @@ export class ConnectProxy {
|
|
|
161
182
|
}
|
|
162
183
|
this.stats.active += 1;
|
|
163
184
|
this.stats.totalOpened += 1;
|
|
185
|
+
const row = this.peerRow(src, srcName);
|
|
186
|
+
row.active += 1;
|
|
187
|
+
row.totalOpened += 1;
|
|
188
|
+
row.lastSeenMs = Date.now();
|
|
164
189
|
this.opts.onTunnelOpen?.({ src, srcName, target: `${host}:${port}` });
|
|
165
190
|
this.logger.info(`tunnel open ${src}${srcName ? ` (${srcName})` : ""} -> ${host}:${port}`);
|
|
166
191
|
});
|
|
@@ -208,6 +233,9 @@ export class ConnectProxy {
|
|
|
208
233
|
}
|
|
209
234
|
refuse(clientSocket, src, srcName, target, code, reason) {
|
|
210
235
|
this.stats.totalRefused += 1;
|
|
236
|
+
const row = this.peerRow(src, srcName);
|
|
237
|
+
row.totalRefused += 1;
|
|
238
|
+
row.lastSeenMs = Date.now();
|
|
211
239
|
this.logger.info(`tunnel refused ${src}${srcName ? ` (${srcName})` : ""} -> ${target}: ${reason}`);
|
|
212
240
|
try {
|
|
213
241
|
const statusText = code === 400 ? "Bad Request" : "Forbidden";
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1607,7 +1607,7 @@ function DkApp() {
|
|
|
1607
1607
|
{ id: "network", icon: "network", label: T.network },
|
|
1608
1608
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
1609
1609
|
];
|
|
1610
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "
|
|
1610
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
1611
1611
|
TweakRadio,
|
|
1612
1612
|
{
|
|
1613
1613
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>
|
|
6
|
+
<title>Beagle</title>
|
|
7
7
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
|
8
8
|
<style>
|
|
9
9
|
* { box-sizing: border-box; }
|
package/dist/ui/server.js
CHANGED
|
@@ -651,7 +651,7 @@ export function startFriendUi(opts) {
|
|
|
651
651
|
const PAGE = `<!doctype html>
|
|
652
652
|
<html lang="en"><head><meta charset="utf-8"/>
|
|
653
653
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
654
|
-
<title>
|
|
654
|
+
<title>Beagle — friends</title>
|
|
655
655
|
<style>
|
|
656
656
|
:root { color-scheme: light dark; }
|
|
657
657
|
body { font: 15px/1.5 -apple-system, system-ui, sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.138",
|
|
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",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@decentnetwork/dora": "^0.1.11",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.71",
|
|
83
83
|
"ink": "^5.2.1",
|
|
84
84
|
"js-yaml": "^4.1.0",
|
|
85
85
|
"react": "^18.3.1",
|