@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.
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
- if (RouteLearner.suspiciousClose(down, Date.now() - t0))
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
- static suspiciousClose(bytesDown: number, lifetimeMs: number): boolean;
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
- static suspiciousClose(bytesDown, lifetimeMs) {
73
- return lifetimeMs < SUSPICIOUS_MS && bytesDown < SUSPICIOUS_BYTES;
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.179",
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",