@decentnetwork/lan 0.1.179 → 0.1.181
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/proxy/multi-exit-router.js +35 -1
- package/dist/proxy/route-learner.d.ts +12 -2
- package/dist/proxy/route-learner.js +13 -3
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -131,6 +131,15 @@ const STALL_MIN_BYTES = 16 * 1024; // delivered at least this = a healthy, real
|
|
|
131
131
|
const STALL_DEAD_BYTES = 1024; // relayed LESS than this = the exit is genuinely dead (only THIS counts as a stall); a small-but-real request (playlist/key/keep-alive, 1-16 KB) is NEUTRAL, so it can't flap a healthy exit out of the pool
|
|
132
132
|
const TRIP_AFTER_STALLS = 3; // consecutive DEAD tunnels before an exit is tripped out (hysteresis)
|
|
133
133
|
const TRIP_AFTER_UPSTREAM_FAILS = 5; // consecutive non-200 CONNECT responses (exit's proxy broken, e.g. 502s everything) before tripping
|
|
134
|
+
// Keep a proven exit in rotation only if its measured speed is at least this
|
|
135
|
+
// fraction of the region's FASTEST proven exit. Below it (e.g. a 0.4 Mbps node
|
|
136
|
+
// next to a 1.2 Mbps one) it's benched while a healthy exit exists, so heavy
|
|
137
|
+
// streams don't stall on it. Unproven (unmeasured) exits are never benched.
|
|
138
|
+
const SLOW_EXIT_FLOOR_FRACTION = 0.4;
|
|
139
|
+
// Hosts that must NEVER be learned into an exit region — global services
|
|
140
|
+
// reachable direct from anywhere; routing them through a china exit only 502s
|
|
141
|
+
// and starves it. Guards against the recurring google→china mislearning.
|
|
142
|
+
const NEVER_LEARN = /(^|\.)(google|gstatic|googleapis|googleusercontent|ggpht|gvt1|gvt2|youtube|ytimg|doubleclick|google-analytics|googletagmanager|firebase|crashlytics|facebook|fbcdn|instagram|twitter|twimg|cloudflare|cloudfront|akamai|akamaihd|amazonaws|apple|icloud|mzstatic|microsoft|windows|office|live|azureedge|github|githubusercontent|gitlab|cdn\.jsdelivr|unpkg|npmjs)\.(com|net|org|io|co|edu|dev|app)$/i;
|
|
134
143
|
const TRIP_COOLDOWN_MS = 30_000; // how long a tripped exit stays excluded before it's retried
|
|
135
144
|
// Measures peak short-window throughput of a byte stream. Feed it every chunk
|
|
136
145
|
// length via add(); read peakBps at the end. Peak (not average) shrugs off TCP
|
|
@@ -252,6 +261,12 @@ export function startMultiExitRouter(opts) {
|
|
|
252
261
|
function noteDirectBad(host) {
|
|
253
262
|
if (!fallbackRegion || !regionHasExits(fallbackRegion))
|
|
254
263
|
return;
|
|
264
|
+
// Never learn a clearly-GLOBAL host into an exit region. These are reachable
|
|
265
|
+
// direct from anywhere; a china exit only 502s them and wastes its bandwidth
|
|
266
|
+
// (the recurring google→china pollution). Belt-and-suspenders alongside the
|
|
267
|
+
// preconnect fix in suspiciousClose.
|
|
268
|
+
if (NEVER_LEARN.test(host))
|
|
269
|
+
return;
|
|
255
270
|
if (learner.recordDirectBad(host))
|
|
256
271
|
learner.learn(host, fallbackRegion);
|
|
257
272
|
}
|
|
@@ -430,6 +445,20 @@ export function startMultiExitRouter(opts) {
|
|
|
430
445
|
// Unproven exits still get overflow traffic once the proven ones fill up,
|
|
431
446
|
// which measures them safely without risking the video.
|
|
432
447
|
const unprovenBps = measuredVals.length > 0 ? measuredVals[0] : 800_000;
|
|
448
|
+
// Speed FLOOR: exclude exits that are dramatically slower than the best
|
|
449
|
+
// PROVEN exit. `score = speed/(active+1)` self-balances, but once the fast
|
|
450
|
+
// exit fills, a bandwidth-heavy stream (CCTV opens dozens of parallel
|
|
451
|
+
// segment tunnels) spills onto a degraded node (observed 0.4 Mbps vs 1.2
|
|
452
|
+
// Mbps) and stalls/rebuffers. Keep such nodes OUT of rotation while a
|
|
453
|
+
// healthy-speed exit exists; unproven exits (null speed) stay in so they
|
|
454
|
+
// still get measured, and we never empty the pool.
|
|
455
|
+
if (measuredVals.length >= 2) {
|
|
456
|
+
const best = measuredVals[measuredVals.length - 1];
|
|
457
|
+
const floor = best * SLOW_EXIT_FLOOR_FRACTION;
|
|
458
|
+
const fast = pool.filter((e) => e.speedEwma === null || e.speedEwma >= floor);
|
|
459
|
+
if (fast.length > 0)
|
|
460
|
+
pool = fast;
|
|
461
|
+
}
|
|
433
462
|
const score = (e) => (e.speedEwma ?? unprovenBps) / (e.active + 1);
|
|
434
463
|
return [...pool].sort((a, b) => score(b) - score(a) || a.served - b.served);
|
|
435
464
|
}
|
|
@@ -650,7 +679,9 @@ export function startMultiExitRouter(opts) {
|
|
|
650
679
|
// fallback region for next time.
|
|
651
680
|
const t0 = Date.now();
|
|
652
681
|
let down = 0;
|
|
682
|
+
let sentUp = head?.length ?? 0; // bytes the CLIENT sent upstream (ClientHello etc.)
|
|
653
683
|
up.on("data", (d) => { down += d.length; });
|
|
684
|
+
client.on("data", (d) => { sentUp += d.length; });
|
|
654
685
|
let closed = false;
|
|
655
686
|
const done = () => {
|
|
656
687
|
if (closed)
|
|
@@ -659,7 +690,10 @@ export function startMultiExitRouter(opts) {
|
|
|
659
690
|
directActive = Math.max(0, directActive - 1);
|
|
660
691
|
if (local)
|
|
661
692
|
return;
|
|
662
|
-
|
|
693
|
+
// A short close only counts as a geo-block if the client actually sent a
|
|
694
|
+
// request. A speculative preconnect (sentUp≈0) is ignored, so global
|
|
695
|
+
// hosts (google/gstatic) no longer get mislearned into a china exit.
|
|
696
|
+
if (RouteLearner.suspiciousClose(down, sentUp, Date.now() - t0))
|
|
663
697
|
noteDirectBad(h);
|
|
664
698
|
else
|
|
665
699
|
learner.recordDirectGood(h);
|
|
@@ -8,6 +8,13 @@ export declare const MISCLASSIFIED_AFTER = 2;
|
|
|
8
8
|
* and is never counted. */
|
|
9
9
|
export declare const SUSPICIOUS_MS = 3000;
|
|
10
10
|
export declare const SUSPICIOUS_BYTES = 1024;
|
|
11
|
+
/** Minimum bytes the CLIENT must have sent upstream before a short close counts
|
|
12
|
+
* as a geo-block. A real GFW RST happens AFTER the client's TLS ClientHello
|
|
13
|
+
* (>~100 bytes). A speculative preconnect that Chrome opens and closes WITHOUT
|
|
14
|
+
* sending anything (0 bytes up) is NOT a block — counting it mislearned
|
|
15
|
+
* google/gstatic/googleapis preconnects into the china exit, which then 502'd
|
|
16
|
+
* and starved CCTV. */
|
|
17
|
+
export declare const MIN_CLIENTHELLO_BYTES = 64;
|
|
11
18
|
export declare class RouteLearner {
|
|
12
19
|
#private;
|
|
13
20
|
constructor(path?: string, log?: (msg: string) => void);
|
|
@@ -19,8 +26,11 @@ export declare class RouteLearner {
|
|
|
19
26
|
recordDirectBad(host: string): boolean;
|
|
20
27
|
/** A DIRECT tunnel to the host worked properly — clear its strike counter. */
|
|
21
28
|
recordDirectGood(host: string): void;
|
|
22
|
-
/** Classify an established tunnel's ending: was it a suspicious early close
|
|
23
|
-
|
|
29
|
+
/** Classify an established tunnel's ending: was it a suspicious early close
|
|
30
|
+
* (GFW-style RST after a real request)? `bytesUp` is what the CLIENT sent —
|
|
31
|
+
* a connection that sent no ClientHello (a Chrome preconnect) is NOT a
|
|
32
|
+
* geo-block and must never be learned. */
|
|
33
|
+
static suspiciousClose(bytesDown: number, bytesUp: number, lifetimeMs: number): boolean;
|
|
24
34
|
learn(host: string, region: string): void;
|
|
25
35
|
/** The learned region failed too (all exits down for this host) — forget it
|
|
26
36
|
* so the host can try DIRECT again next time. */
|
|
@@ -25,6 +25,13 @@ export const MISCLASSIFIED_AFTER = 2;
|
|
|
25
25
|
* and is never counted. */
|
|
26
26
|
export const SUSPICIOUS_MS = 3000;
|
|
27
27
|
export const SUSPICIOUS_BYTES = 1024;
|
|
28
|
+
/** Minimum bytes the CLIENT must have sent upstream before a short close counts
|
|
29
|
+
* as a geo-block. A real GFW RST happens AFTER the client's TLS ClientHello
|
|
30
|
+
* (>~100 bytes). A speculative preconnect that Chrome opens and closes WITHOUT
|
|
31
|
+
* sending anything (0 bytes up) is NOT a block — counting it mislearned
|
|
32
|
+
* google/gstatic/googleapis preconnects into the china exit, which then 502'd
|
|
33
|
+
* and starved CCTV. */
|
|
34
|
+
export const MIN_CLIENTHELLO_BYTES = 64;
|
|
28
35
|
/** Learned entries expire after this long (the internet moves; re-verify). */
|
|
29
36
|
const LEARNED_TTL_MS = 30 * 24 * 3600 * 1000;
|
|
30
37
|
/** Cap the failure-counter table so hostile traffic can't grow it unbounded. */
|
|
@@ -68,9 +75,12 @@ export class RouteLearner {
|
|
|
68
75
|
recordDirectGood(host) {
|
|
69
76
|
this.#fails.delete(host);
|
|
70
77
|
}
|
|
71
|
-
/** Classify an established tunnel's ending: was it a suspicious early close
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
/** Classify an established tunnel's ending: was it a suspicious early close
|
|
79
|
+
* (GFW-style RST after a real request)? `bytesUp` is what the CLIENT sent —
|
|
80
|
+
* a connection that sent no ClientHello (a Chrome preconnect) is NOT a
|
|
81
|
+
* geo-block and must never be learned. */
|
|
82
|
+
static suspiciousClose(bytesDown, bytesUp, lifetimeMs) {
|
|
83
|
+
return bytesUp >= MIN_CLIENTHELLO_BYTES && lifetimeMs < SUSPICIOUS_MS && bytesDown < SUSPICIOUS_BYTES;
|
|
74
84
|
}
|
|
75
85
|
learn(host, region) {
|
|
76
86
|
this.#learned.set(host, { region, at: Date.now() });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.181",
|
|
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",
|