@decentnetwork/lan 0.1.180 → 0.1.182

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
@@ -136,6 +136,10 @@ const TRIP_AFTER_UPSTREAM_FAILS = 5; // consecutive non-200 CONNECT responses (e
136
136
  // next to a 1.2 Mbps one) it's benched while a healthy exit exists, so heavy
137
137
  // streams don't stall on it. Unproven (unmeasured) exits are never benched.
138
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;
139
143
  const TRIP_COOLDOWN_MS = 30_000; // how long a tripped exit stays excluded before it's retried
140
144
  // Measures peak short-window throughput of a byte stream. Feed it every chunk
141
145
  // length via add(); read peakBps at the end. Peak (not average) shrugs off TCP
@@ -236,6 +240,20 @@ export function startMultiExitRouter(opts) {
236
240
  // route kept failing get auto-promoted into the fallback region (and
237
241
  // persisted). Explicit domain lists always win over learned entries.
238
242
  const learner = new RouteLearner(opts.learnedRoutesPath, log);
243
+ // Auto-purge on startup: drop any EXISTING learned entry for a global host
244
+ // (google/gstatic/…) that older builds mislearned into an exit. So a user just
245
+ // updating + restarting is self-healed — no manual learned-routes cleanup.
246
+ {
247
+ let purged = 0;
248
+ for (const { host } of learner.entries()) {
249
+ if (NEVER_LEARN.test(host)) {
250
+ learner.unlearn(host);
251
+ purged++;
252
+ }
253
+ }
254
+ if (purged > 0)
255
+ log(`purged ${purged} stale global-host route(s) mislearned into an exit`);
256
+ }
239
257
  const fallbackRegion = opts.fallbackRegion
240
258
  ?? regions.find((r) => opts.exits.some((e) => (e.region ?? "default") === r.name))?.name
241
259
  ?? null;
@@ -257,6 +275,12 @@ export function startMultiExitRouter(opts) {
257
275
  function noteDirectBad(host) {
258
276
  if (!fallbackRegion || !regionHasExits(fallbackRegion))
259
277
  return;
278
+ // Never learn a clearly-GLOBAL host into an exit region. These are reachable
279
+ // direct from anywhere; a china exit only 502s them and wastes its bandwidth
280
+ // (the recurring google→china pollution). Belt-and-suspenders alongside the
281
+ // preconnect fix in suspiciousClose.
282
+ if (NEVER_LEARN.test(host))
283
+ return;
260
284
  if (learner.recordDirectBad(host))
261
285
  learner.learn(host, fallbackRegion);
262
286
  }
@@ -669,7 +693,9 @@ export function startMultiExitRouter(opts) {
669
693
  // fallback region for next time.
670
694
  const t0 = Date.now();
671
695
  let down = 0;
696
+ let sentUp = head?.length ?? 0; // bytes the CLIENT sent upstream (ClientHello etc.)
672
697
  up.on("data", (d) => { down += d.length; });
698
+ client.on("data", (d) => { sentUp += d.length; });
673
699
  let closed = false;
674
700
  const done = () => {
675
701
  if (closed)
@@ -678,7 +704,10 @@ export function startMultiExitRouter(opts) {
678
704
  directActive = Math.max(0, directActive - 1);
679
705
  if (local)
680
706
  return;
681
- if (RouteLearner.suspiciousClose(down, Date.now() - t0))
707
+ // A short close only counts as a geo-block if the client actually sent a
708
+ // request. A speculative preconnect (sentUp≈0) is ignored, so global
709
+ // hosts (google/gstatic) no longer get mislearned into a china exit.
710
+ if (RouteLearner.suspiciousClose(down, sentUp, Date.now() - t0))
682
711
  noteDirectBad(h);
683
712
  else
684
713
  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.180",
3
+ "version": "0.1.182",
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",