@decentnetwork/lan 0.1.156 → 0.1.158
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/carrier/peer-manager.d.ts +8 -0
- package/dist/carrier/peer-manager.js +18 -0
- package/dist/daemon/server.js +33 -0
- package/dist/proxy/multi-exit-router.js +28 -3
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -62,6 +62,14 @@ export declare class PeerManager extends EventEmitter {
|
|
|
62
62
|
sign(message: Uint8Array): Uint8Array;
|
|
63
63
|
/** Offer a file to a friend (toxcore-standard transfer). Returns the fileId. */
|
|
64
64
|
sendFile(userid: string, data: Uint8Array, name: string): string | null;
|
|
65
|
+
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
66
|
+
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
67
|
+
* receive online. Use for native friends; JS friends keep sendFile(). */
|
|
68
|
+
sendInlineFile(userid: string, data: Uint8Array, name: string): Promise<void>;
|
|
69
|
+
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
70
|
+
* relay/DHT key differs from its identity key. JS peers announce their
|
|
71
|
+
* identity key as their DHT key, so the two match. */
|
|
72
|
+
isNativeFriend(userid: string): boolean;
|
|
65
73
|
/** Accept an incoming file offer. */
|
|
66
74
|
acceptFile(userid: string, fileNumber: number): void;
|
|
67
75
|
/** Cancel an in-flight send by content fileId. Returns true if it was found. */
|
|
@@ -121,6 +121,21 @@ export class PeerManager extends EventEmitter {
|
|
|
121
121
|
throw new Error("Peer not created. Call create() first.");
|
|
122
122
|
return this.peer.sendFile(userid, data, { name });
|
|
123
123
|
}
|
|
124
|
+
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
125
|
+
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
126
|
+
* receive online. Use for native friends; JS friends keep sendFile(). */
|
|
127
|
+
async sendInlineFile(userid, data, name) {
|
|
128
|
+
if (!this.peer)
|
|
129
|
+
throw new Error("Peer not created. Call create() first.");
|
|
130
|
+
await this.peer.sendInlineFile(userid, { name, data });
|
|
131
|
+
}
|
|
132
|
+
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
133
|
+
* relay/DHT key differs from its identity key. JS peers announce their
|
|
134
|
+
* identity key as their DHT key, so the two match. */
|
|
135
|
+
isNativeFriend(userid) {
|
|
136
|
+
const rec = this.peer?.friends().find((f) => f.pubkey === userid);
|
|
137
|
+
return !!(rec?.dhtPubkey && rec.dhtPubkey !== rec.pubkey);
|
|
138
|
+
}
|
|
124
139
|
/** Accept an incoming file offer. */
|
|
125
140
|
acceptFile(userid, fileNumber) {
|
|
126
141
|
this.peer?.acceptFile(userid, fileNumber);
|
|
@@ -516,6 +531,9 @@ export class PeerManager extends EventEmitter {
|
|
|
516
531
|
}
|
|
517
532
|
this.emit("message", message.pubkey, message.text, message.via);
|
|
518
533
|
});
|
|
534
|
+
// Inline files (iOS/C Carrier style: FileModel JSON over bulkmsg).
|
|
535
|
+
// The SDK reassembles + decodes; forward to the daemon to save + log.
|
|
536
|
+
this.peer.onInlineFile((f) => this.emit("inline-file", f));
|
|
519
537
|
// Toxcore-standard file transfer (native-compatible). Forward offers,
|
|
520
538
|
// progress, and completions to the daemon, which auto-accepts and saves.
|
|
521
539
|
this.peer.onFile((o) => this.emit("file-offer", o));
|
package/dist/daemon/server.js
CHANGED
|
@@ -455,6 +455,23 @@ export class DaemonServer {
|
|
|
455
455
|
this.logger.info(`Queued file "${name}" (${data.length}B) for offline ${userid.slice(0, 8)} (delivers on reconnect)`);
|
|
456
456
|
return { queued: true, name: safe, size: data.length };
|
|
457
457
|
}
|
|
458
|
+
// NATIVE friends (iOS/Android/C Carrier) cannot see the toxcore
|
|
459
|
+
// file-transfer protocol at all — the only online file path they
|
|
460
|
+
// receive is the inline FileModel-JSON-over-bulkmsg envelope the
|
|
461
|
+
// Beagle apps themselves use. Detected via the DHT-key signature.
|
|
462
|
+
if (this.peerManager?.isNativeFriend(userid)) {
|
|
463
|
+
const INLINE_MAX = 2 * 1024 * 1024; // base64 inflates ~1.37x; protocol cap is 5MB
|
|
464
|
+
if (data.length > INLINE_MAX) {
|
|
465
|
+
throw new Error(`This friend is a native (iOS/Android) client — files are sent inline and limited to ` +
|
|
466
|
+
`${(INLINE_MAX / 1024 / 1024) | 0} MB (this is ${(data.length / 1024 / 1024).toFixed(1)} MB).`);
|
|
467
|
+
}
|
|
468
|
+
await this.peerManager.sendInlineFile(userid, new Uint8Array(data), name);
|
|
469
|
+
this.logger.info(`Sent inline file "${name}" (${data.length}B) to native peer ${userid.slice(0, 8)}`);
|
|
470
|
+
this.messageStore?.appendFile(userid, "out", { name: sanitizeFileName(name), size: data.length, status: "sent", sent: data.length });
|
|
471
|
+
this.friendMeta?.ensure(userid);
|
|
472
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
473
|
+
return { inline: true, name, size: data.length };
|
|
474
|
+
}
|
|
458
475
|
const fileId = this.peerManager?.sendFile(userid, new Uint8Array(data), name);
|
|
459
476
|
if (!fileId)
|
|
460
477
|
throw new Error("No free transfer slot (or friend unknown)");
|
|
@@ -898,6 +915,22 @@ export class DaemonServer {
|
|
|
898
915
|
this.ipcEvents.emit("event", { type: "chat", userid: p.friendId, dir: "in" });
|
|
899
916
|
})().catch((e) => this.logger.warn(`Failed to save file: ${e.message}`));
|
|
900
917
|
});
|
|
918
|
+
// Inline files (iOS/C Carrier style — FileModel JSON over bulkmsg,
|
|
919
|
+
// reassembled + decoded by the SDK). Save like a completed transfer so
|
|
920
|
+
// the chat shows a media chip instead of a wall of base64 "text".
|
|
921
|
+
this.peerManager.on("inline-file", (f) => {
|
|
922
|
+
void (async () => {
|
|
923
|
+
const dir = resolve(this.configDir, "downloads");
|
|
924
|
+
const fs = await import("fs/promises");
|
|
925
|
+
await fs.mkdir(dir, { recursive: true });
|
|
926
|
+
const safe = sanitizeFileName(f.name);
|
|
927
|
+
await fs.writeFile(resolve(dir, safe), Buffer.from(f.data));
|
|
928
|
+
this.logger.info(`Saved inline file "${f.name}" (${f.data.length}B, ${f.via ?? "online"}) from ${f.pubkey.slice(0, 8)} → ${resolve(dir, safe)}`);
|
|
929
|
+
this.messageStore?.appendFile(f.pubkey, "in", { name: safe, size: f.data.length });
|
|
930
|
+
this.friendMeta?.ensure(f.pubkey);
|
|
931
|
+
this.ipcEvents.emit("event", { type: "chat", userid: f.pubkey, dir: "in" });
|
|
932
|
+
})().catch((e) => this.logger.warn(`Failed to save inline file: ${e.message}`));
|
|
933
|
+
});
|
|
901
934
|
this.peerManager.on("friend-request", (req) => {
|
|
902
935
|
void pubkeyHexToUserid(req.pubkey).then((userid) => {
|
|
903
936
|
const who = `${req.name || "(unnamed)"} ${userid}`;
|
|
@@ -42,6 +42,14 @@ export const BUILTIN_REGION_DOMAINS = {
|
|
|
42
42
|
// subdomains that resolve to a non-China edge, so pin them statically.
|
|
43
43
|
".v.myalicdn.com", ".myalicdn.com",
|
|
44
44
|
".kcdnvip.com", ".ks-cdn.com", ".ksyuncdn.com",
|
|
45
|
+
// CCTV live also rotates onto Baidu (bdydns) and Wangsu/网宿 (wscdns)
|
|
46
|
+
// edges — observed segment hosts ldcctvwbcdbd.a.bdydns.com and
|
|
47
|
+
// ldcctvwbcdcnc.v.wscdns.com. These aren't .cn, so pin the provider
|
|
48
|
+
// suffixes so they deterministically ride a China exit instead of
|
|
49
|
+
// depending on geo-classify timing (which flaps to direct → geo-block
|
|
50
|
+
// when a China exit is momentarily busy). Tencent live is .myqcloud.com
|
|
51
|
+
// below.
|
|
52
|
+
".bdydns.com", ".wscdns.com",
|
|
45
53
|
".bilibili.com", ".biliapi.net", ".bilivideo.com", ".bilivideo.cn", ".hdslb.com",
|
|
46
54
|
".youku.com", ".iqiyi.com", ".iq.com", ".mgtv.com", ".hitv.com",
|
|
47
55
|
".miguvideo.com", ".cmvideo.cn", ".migu.cn",
|
|
@@ -254,7 +262,14 @@ export function startMultiExitRouter(opts) {
|
|
|
254
262
|
const geoCn = new GeoCnClassifier();
|
|
255
263
|
async function resolveRegion(host) {
|
|
256
264
|
let region = routeFor(host); // static domains + persistent learned routes
|
|
257
|
-
|
|
265
|
+
// Consult the geo classifier even when the fallback region currently has
|
|
266
|
+
// NO healthy exits. A China-allocated host (e.g. a CCTV live CDN edge)
|
|
267
|
+
// must be pinned to the China region and 503 (retriable) when the region
|
|
268
|
+
// is momentarily down — NOT leaked to `direct`, where the CDN geo-blocks
|
|
269
|
+
// our non-China egress outright ("您所在的地区,暂不支持播放该视频") and the
|
|
270
|
+
// directConnect failover can even unlearn the route, turning a transient
|
|
271
|
+
// exit blip into a persistent geo-block until re-learned.
|
|
272
|
+
if (region === "direct" && !isLocalDest(host) && fallbackRegion) {
|
|
258
273
|
// peek() is sync for IP literals / cached verdicts; classify() resolves.
|
|
259
274
|
const cn = geoCn.peek(host);
|
|
260
275
|
if (cn === true || (cn === null && await geoCn.classify(host))) {
|
|
@@ -265,8 +280,13 @@ export function startMultiExitRouter(opts) {
|
|
|
265
280
|
region = fallbackRegion;
|
|
266
281
|
}
|
|
267
282
|
}
|
|
268
|
-
|
|
283
|
+
// Downgrade a region-with-no-exits to direct ONLY for hosts we don't know
|
|
284
|
+
// to be China. A known-China host to direct is a guaranteed geo-block, so
|
|
285
|
+
// keep it in-region (the caller 503s, the player retries, and a recovered
|
|
286
|
+
// China exit serves the retry).
|
|
287
|
+
if (region !== "direct" && !regionHasExits(region) && geoCn.peek(host) !== true) {
|
|
269
288
|
region = "direct";
|
|
289
|
+
}
|
|
270
290
|
return region;
|
|
271
291
|
}
|
|
272
292
|
const seenGeo = new Set();
|
|
@@ -599,7 +619,12 @@ export function startMultiExitRouter(opts) {
|
|
|
599
619
|
let i = 0;
|
|
600
620
|
const tryNext = () => {
|
|
601
621
|
if (i >= order.length) {
|
|
602
|
-
|
|
622
|
+
// Only unlearn if we DON'T know this is a China host. Unlearning a
|
|
623
|
+
// China-geo host back to `direct` is the geo-block trap: direct is
|
|
624
|
+
// guaranteed to fail for it, so keep the learned China route and let
|
|
625
|
+
// the player retry when a China exit recovers.
|
|
626
|
+
if (geoCn.peek(h) !== true)
|
|
627
|
+
learner.unlearn(h);
|
|
603
628
|
client.end("HTTP/1.1 502 All routes failed\r\n\r\n");
|
|
604
629
|
return;
|
|
605
630
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.158",
|
|
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.13",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.83",
|
|
83
83
|
"ink": "^5.2.1",
|
|
84
84
|
"js-yaml": "^4.1.0",
|
|
85
85
|
"node-forge": "^1.4.0",
|