@decentnetwork/lan 0.1.180 → 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
@@ -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
@@ -257,6 +261,12 @@ export function startMultiExitRouter(opts) {
257
261
  function noteDirectBad(host) {
258
262
  if (!fallbackRegion || !regionHasExits(fallbackRegion))
259
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;
260
270
  if (learner.recordDirectBad(host))
261
271
  learner.learn(host, fallbackRegion);
262
272
  }
@@ -669,7 +679,9 @@ export function startMultiExitRouter(opts) {
669
679
  // fallback region for next time.
670
680
  const t0 = Date.now();
671
681
  let down = 0;
682
+ let sentUp = head?.length ?? 0; // bytes the CLIENT sent upstream (ClientHello etc.)
672
683
  up.on("data", (d) => { down += d.length; });
684
+ client.on("data", (d) => { sentUp += d.length; });
673
685
  let closed = false;
674
686
  const done = () => {
675
687
  if (closed)
@@ -678,7 +690,10 @@ export function startMultiExitRouter(opts) {
678
690
  directActive = Math.max(0, directActive - 1);
679
691
  if (local)
680
692
  return;
681
- 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))
682
697
  noteDirectBad(h);
683
698
  else
684
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.180",
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",