@decentnetwork/lan 0.1.196 → 0.1.197
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/hls-prefetch.d.ts +8 -0
- package/dist/proxy/hls-prefetch.js +25 -0
- package/dist/proxy/multi-exit-router.js +59 -35
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -29,6 +29,14 @@ export declare function capM3u8Variants(playlist: string, maxBandwidth: number):
|
|
|
29
29
|
* reloading the capped master playlist. The proxy can use this map to fetch the
|
|
30
30
|
* safe media playlist while preserving the browser-facing request URL. */
|
|
31
31
|
export declare function buildM3u8VariantFallbacks(playlist: string, baseUrl: string, maxBandwidth: number): Map<string, string>;
|
|
32
|
+
/** Resolve one HTTP byte-range against a complete cached body. Multiple ranges
|
|
33
|
+
* are intentionally rejected (the CCTV player uses only a single range). */
|
|
34
|
+
export declare function sliceHttpByteRange(body: Buffer, header: string): {
|
|
35
|
+
body: Buffer;
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
total: number;
|
|
39
|
+
} | null;
|
|
32
40
|
/**
|
|
33
41
|
* Extract the media-segment URLs from an m3u8 playlist, resolved against the
|
|
34
42
|
* playlist's own URL. Returns [] for master playlists (variant lists) — those
|
|
@@ -128,6 +128,31 @@ export function buildM3u8VariantFallbacks(playlist, baseUrl, maxBandwidth) {
|
|
|
128
128
|
}
|
|
129
129
|
return out;
|
|
130
130
|
}
|
|
131
|
+
/** Resolve one HTTP byte-range against a complete cached body. Multiple ranges
|
|
132
|
+
* are intentionally rejected (the CCTV player uses only a single range). */
|
|
133
|
+
export function sliceHttpByteRange(body, header) {
|
|
134
|
+
const match = /^bytes=(\d*)-(\d*)$/i.exec(header.trim());
|
|
135
|
+
if (!match || (match[1] === "" && match[2] === "") || body.length === 0)
|
|
136
|
+
return null;
|
|
137
|
+
const total = body.length;
|
|
138
|
+
let start;
|
|
139
|
+
let end;
|
|
140
|
+
if (match[1] === "") {
|
|
141
|
+
const suffix = Number(match[2]);
|
|
142
|
+
if (!Number.isSafeInteger(suffix) || suffix <= 0)
|
|
143
|
+
return null;
|
|
144
|
+
start = Math.max(0, total - suffix);
|
|
145
|
+
end = total - 1;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
start = Number(match[1]);
|
|
149
|
+
end = match[2] === "" ? total - 1 : Number(match[2]);
|
|
150
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start >= total || end < start)
|
|
151
|
+
return null;
|
|
152
|
+
end = Math.min(end, total - 1);
|
|
153
|
+
}
|
|
154
|
+
return { body: body.subarray(start, end + 1), start, end, total };
|
|
155
|
+
}
|
|
131
156
|
/**
|
|
132
157
|
* Extract the media-segment URLs from an m3u8 playlist, resolved against the
|
|
133
158
|
* playlist's own URL. Returns [] for master playlists (variant lists) — those
|
|
@@ -21,7 +21,7 @@ import https from "node:https";
|
|
|
21
21
|
import net from "node:net";
|
|
22
22
|
import tls from "node:tls";
|
|
23
23
|
import { egressConnect } from "./egress.js";
|
|
24
|
-
import { HlsPrefetcher, buildM3u8VariantFallbacks, capM3u8Variants, looksLikePlaylist, looksLikeSegment, parseM3u8Variants, parseM3u8Segments } from "./hls-prefetch.js";
|
|
24
|
+
import { HlsPrefetcher, buildM3u8VariantFallbacks, capM3u8Variants, looksLikePlaylist, looksLikeSegment, parseM3u8Variants, parseM3u8Segments, sliceHttpByteRange } from "./hls-prefetch.js";
|
|
25
25
|
import { RouteLearner } from "./route-learner.js";
|
|
26
26
|
import { GeoCnClassifier } from "./geo-cn.js";
|
|
27
27
|
import { MitmGateway } from "./mitm-gateway.js";
|
|
@@ -164,15 +164,16 @@ const NEAR_DEAD_EXIT_BPS = 60_000;
|
|
|
164
164
|
// threshold admitted 10.86.15.133 on a short peak even though its full CCTV
|
|
165
165
|
// segments averaged only 0.7-1.2 Mbit/s, causing each half-range to take 4-6s.
|
|
166
166
|
const HLS_DELIVERY_BPS = 200_000;
|
|
167
|
-
// Below this speed even
|
|
167
|
+
// Below this speed even a 0.9 Mbps stream has no useful headroom. If
|
|
168
168
|
// unproven exits remain, keep exploring them instead of locking startup onto
|
|
169
169
|
// the first measured-but-dead path (observed: 0.3 Mbps 15.133 hid the actually
|
|
170
170
|
// usable 134.139 for the remainder of the router process).
|
|
171
171
|
const HLS_STARTUP_EXPLORE_BPS = 100_000;
|
|
172
|
-
//
|
|
173
|
-
//
|
|
172
|
+
// Quality selection belongs to the player/user by default. Operators may set a
|
|
173
|
+
// positive value to impose an explicit site-local ceiling; 0 exposes every
|
|
174
|
+
// origin variant.
|
|
174
175
|
const HLS_MAX_VARIANT_BPS = process.env.AGENTNET_HLS_MAX_BPS === undefined
|
|
175
|
-
?
|
|
176
|
+
? 0
|
|
176
177
|
: Math.max(0, Number(process.env.AGENTNET_HLS_MAX_BPS));
|
|
177
178
|
export function isUsefulRecoverySample(peakBps, averageBps, minBps = HLS_DELIVERY_BPS) {
|
|
178
179
|
return peakBps >= minBps && averageBps >= minBps;
|
|
@@ -1376,17 +1377,14 @@ export function startMultiExitRouter(opts) {
|
|
|
1376
1377
|
tryIdx(0);
|
|
1377
1378
|
});
|
|
1378
1379
|
}
|
|
1379
|
-
// Striping
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
//
|
|
1386
|
-
|
|
1387
|
-
// split fetching the same segment twice. concurrency:2 keeps at most 2
|
|
1388
|
-
// segments (≈6 stripes over ~3 exits) in flight so the pool isn't overwhelmed.
|
|
1389
|
-
const AGG = process.env.AGENTNET_HLS_STRIPES ? Math.max(1, Number(process.env.AGENTNET_HLS_STRIPES)) : 3;
|
|
1380
|
+
// Striping can aggregate genuinely independent exits, but it is harmful when
|
|
1381
|
+
// every range converges on one useful shared exit. Three stripes, a hedge per
|
|
1382
|
+
// stripe, and retries produced 6-11 simultaneous requests to 10.86.134.139;
|
|
1383
|
+
// its measured throughput collapsed from ~2 Mbps to ~0.3 Mbps and every
|
|
1384
|
+
// client sharing that exit stalled. Default to one whole-segment fetch (with
|
|
1385
|
+
// the existing single late hedge). Operators may explicitly opt into more
|
|
1386
|
+
// stripes after proving their exits have independent capacity.
|
|
1387
|
+
const AGG = process.env.AGENTNET_HLS_STRIPES ? Math.max(1, Number(process.env.AGENTNET_HLS_STRIPES)) : 1;
|
|
1390
1388
|
// How many china exits are ACTUALLY delivering right now — healthy (or with
|
|
1391
1389
|
// live traffic), not tripped, and not speed-penalized (an exit that returned
|
|
1392
1390
|
// empty/0-byte gets its score floored, so it drops out here until it serves
|
|
@@ -1408,14 +1406,11 @@ export function startMultiExitRouter(opts) {
|
|
|
1408
1406
|
return exits.some((e) => e.region === fallbackRegion && e.trippedUntil <= now &&
|
|
1409
1407
|
e.flooredAtMs === null && e.speedEwma !== null && e.speedEwma >= HLS_STARTUP_EXPLORE_BPS);
|
|
1410
1408
|
};
|
|
1411
|
-
// Stripe count is a connection count, not a distinct-exit count.
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1415
|
-
//
|
|
1416
|
-
// exit has delivered a real fast sample; otherwise the unproven startup pool
|
|
1417
|
-
// would scatter half-ranges onto every slow exit and hold assembly hostage.
|
|
1418
|
-
// Set AGENTNET_HLS_STRIPES=1 for an emergency rollback.
|
|
1409
|
+
// Stripe count is a connection count, not a distinct-exit count. During
|
|
1410
|
+
// startup, keep whole-segment mode until at least one exit has delivered a
|
|
1411
|
+
// real fast sample; otherwise the unproven startup pool would scatter ranges
|
|
1412
|
+
// onto every slow exit and hold assembly hostage. AGENTNET_HLS_STRIPES=N is
|
|
1413
|
+
// an explicit opt-in for pools where N independent exits are proven useful.
|
|
1419
1414
|
const usableChinaExits = () => AGG <= 1 || !hasMeasuredChinaExit() ? 1 : AGG;
|
|
1420
1415
|
const hlsConcurrency = () => hasMeasuredChinaExit() ? deliveringChinaExits() : 1;
|
|
1421
1416
|
// Prefetch parallelism = the number of exits ACTUALLY delivering right now
|
|
@@ -1483,23 +1478,36 @@ export function startMultiExitRouter(opts) {
|
|
|
1483
1478
|
res.end();
|
|
1484
1479
|
return;
|
|
1485
1480
|
}
|
|
1486
|
-
const
|
|
1481
|
+
const isPlaylist = looksLikePlaylist(u.pathname || "");
|
|
1482
|
+
const isSegment = looksLikeSegment(u.pathname || "");
|
|
1483
|
+
const requestedRange = typeof headers["range"] === "string" ? headers["range"] : null;
|
|
1484
|
+
const cacheable = method === "GET";
|
|
1487
1485
|
if (cacheable) {
|
|
1488
1486
|
const hit = hls.lookup(fullUrl);
|
|
1489
1487
|
if (hit) {
|
|
1488
|
+
const ranged = requestedRange && isSegment ? sliceHttpByteRange(hit.body, requestedRange) : null;
|
|
1489
|
+
if (requestedRange && isSegment && !ranged) {
|
|
1490
|
+
const hh = { "content-range": `bytes */${hit.body.length}`, "content-length": 0 };
|
|
1491
|
+
addCors(hh);
|
|
1492
|
+
res.writeHead(416, hh);
|
|
1493
|
+
res.end();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const body = ranged?.body ?? hit.body;
|
|
1490
1497
|
const hh = {
|
|
1491
1498
|
"content-type": hit.contentType ?? "application/octet-stream",
|
|
1492
|
-
"content-length":
|
|
1499
|
+
"content-length": body.length,
|
|
1493
1500
|
"x-agentnet-cache": "hit",
|
|
1501
|
+
"accept-ranges": "bytes",
|
|
1494
1502
|
};
|
|
1503
|
+
if (ranged)
|
|
1504
|
+
hh["content-range"] = `bytes ${ranged.start}-${ranged.end}/${ranged.total}`;
|
|
1495
1505
|
addCors(hh);
|
|
1496
|
-
res.writeHead(200, hh);
|
|
1497
|
-
res.end(
|
|
1506
|
+
res.writeHead(ranged ? 206 : 200, hh);
|
|
1507
|
+
res.end(body);
|
|
1498
1508
|
bumpHlsStats();
|
|
1499
1509
|
return;
|
|
1500
1510
|
}
|
|
1501
|
-
const isPlaylist = looksLikePlaylist(u.pathname || "");
|
|
1502
|
-
const isSegment = looksLikeSegment(u.pathname || "");
|
|
1503
1511
|
if (isPlaylist || isSegment) {
|
|
1504
1512
|
const fwd = {};
|
|
1505
1513
|
for (const [k, v] of Object.entries(headers)) {
|
|
@@ -1545,17 +1553,33 @@ export function startMultiExitRouter(opts) {
|
|
|
1545
1553
|
log(` playlist ${host} is MEDIA — ${segs.length} segments, prefetching ahead`);
|
|
1546
1554
|
}
|
|
1547
1555
|
}
|
|
1548
|
-
|
|
1556
|
+
let responseStatus = r.status;
|
|
1557
|
+
let responseBody = r.body;
|
|
1558
|
+
const outHeaders = { ...r.headers };
|
|
1559
|
+
if (requestedRange && isSegment && r.status === 200) {
|
|
1560
|
+
const ranged = sliceHttpByteRange(r.body, requestedRange);
|
|
1561
|
+
if (!ranged) {
|
|
1562
|
+
responseStatus = 416;
|
|
1563
|
+
responseBody = Buffer.alloc(0);
|
|
1564
|
+
outHeaders["content-range"] = `bytes */${r.body.length}`;
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
responseStatus = 206;
|
|
1568
|
+
responseBody = ranged.body;
|
|
1569
|
+
outHeaders["content-range"] = `bytes ${ranged.start}-${ranged.end}/${ranged.total}`;
|
|
1570
|
+
outHeaders["accept-ranges"] = "bytes";
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
log(` MITM ${method} ${host}${(path || "/").slice(0, 80)} → ${responseStatus} (${responseBody.length}B)`);
|
|
1549
1574
|
if (r.status >= 400) {
|
|
1550
1575
|
const snip = r.body.slice(0, 300).toString("utf8").replace(/\s+/g, " ");
|
|
1551
1576
|
log(` body: ${snip}`);
|
|
1552
1577
|
}
|
|
1553
|
-
const outHeaders = { ...r.headers };
|
|
1554
1578
|
delete outHeaders["transfer-encoding"];
|
|
1555
|
-
outHeaders["content-length"] =
|
|
1579
|
+
outHeaders["content-length"] = responseBody.length;
|
|
1556
1580
|
addCors(outHeaders); // cache/dedup/striped paths drop the CDN's CORS header — always re-add
|
|
1557
|
-
res.writeHead(
|
|
1558
|
-
res.end(
|
|
1581
|
+
res.writeHead(responseStatus, outHeaders);
|
|
1582
|
+
res.end(responseBody);
|
|
1559
1583
|
if (r.status === 200) {
|
|
1560
1584
|
const ct = r.headers["content-type"];
|
|
1561
1585
|
if (isPlaylist) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.197",
|
|
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",
|