@torrent-tv/proxy 2.5.5 → 2.5.8

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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ ## 2.5.7
2
+
3
+ - **Fix**: WebRTC connection failure behind symmetric NAT — all ICE candidates (private and public) are now sent to the browser immediately. The browser attempts all paths in parallel; the local LAN path succeeds when browser and proxy are on the same network. Chrome's Private Network Access dialog appears once on first connect.
4
+
5
+ ## 2.5.6
6
+
7
+ - **Fix**: ICE candidate filtering — private host candidates (RFC 1918, Docker bridge IPs, IPv6 ULA/loopback) are now buffered and suppressed when a public srflx candidate is available. This eliminates the Chrome/Brave Private Network Access permission dialog when connecting from a page served over HTTPS. Falls back to private candidates if no public srflx candidate is gathered (e.g. STUN unreachable), so connectivity is preserved at the cost of the PNA dialog.
8
+
9
+ ## 2.5.5
10
+
11
+ - **Fix**: Tunnel keepalive — proxy now sends a WebSocket ping to the server every 30 s to prevent Cloudflare's ~100 s idle-connection timeout from dropping the tunnel.
12
+
13
+ ## 2.5.3
14
+
15
+ - Internal: improved tunnel reconnect logic and error logging.
16
+
17
+ ## 2.0.0
18
+
19
+ - **New**: WebRTC P2P tunnel architecture — replaced direct HTTP streaming with a persistent WebSocket tunnel to the server. Video is delivered from the proxy to the browser over a WebRTC data channel; the server acts only as a signalling relay.
20
+ - **New**: `node-datachannel` dependency for server-side WebRTC.
21
+ - **Removed**: `public_base_url` config — no longer needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.5.5",
3
+ "version": "2.5.8",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -15,6 +15,49 @@ import nodeDataChannel from "node-datachannel";
15
15
 
16
16
  const ICE_SERVERS = ["stun:stun.l.google.com:19302"];
17
17
 
18
+ /**
19
+ * Return true when the ICE candidate string describes a `typ host` candidate
20
+ * with a private (RFC 1918 / ULA / loopback) IP address.
21
+ *
22
+ * Browsers enforce Private Network Access (PNA) and show a permission dialog
23
+ * when a page served from a public origin (e.g. webauth.courses) attempts a
24
+ * WebRTC connection to a private-network address. Filtering these candidates
25
+ * out before forwarding them to the browser lets the connection proceed via the
26
+ * server-reflexive (srflx) candidate — the proxy's public IP as seen by STUN —
27
+ * which does not trigger PNA.
28
+ *
29
+ * Edge case: if no srflx candidate is available (STUN unreachable, symmetric
30
+ * NAT that maps differently per destination, etc.) all host candidates are
31
+ * suppressed and the connection will fail. We accept this trade-off; STUN is
32
+ * a hard dependency of the WebRTC path in any case.
33
+ *
34
+ * @param {string} candidate - Raw candidate attribute string from node-datachannel.
35
+ * @returns {boolean}
36
+ */
37
+ function isPrivateHostCandidate(candidate) {
38
+ // Only care about "typ host" — srflx and relay are already public/relay.
39
+ if (!candidate.includes("typ host")) {
40
+ return false;
41
+ }
42
+ // RFC 1918 IPv4 private ranges.
43
+ if (/\b(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/.test(candidate)) {
44
+ return true;
45
+ }
46
+ // Docker / typical private subnets not covered above (100.64–127 are special).
47
+ if (/\b127\./.test(candidate)) {
48
+ return true;
49
+ }
50
+ // IPv6 loopback.
51
+ if (/\s::1\s/.test(candidate)) {
52
+ return true;
53
+ }
54
+ // IPv6 Unique Local Addresses (ULA): fc00::/7 — starts with fc or fd.
55
+ if (/\s(?:fc|fd)[0-9a-f]{2}:/i.test(candidate)) {
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+
18
61
  /**
19
62
  * Configuration for the WebRTC manager.
20
63
  *
@@ -79,11 +122,23 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog }) {
79
122
  iceServers: ICE_SERVERS
80
123
  });
81
124
 
82
- // Forward our ICE candidates to the browser through the tunnel.
125
+ // Forward all ICE candidates to the browser through the tunnel.
126
+ //
127
+ // All candidates — both private (RFC 1918) and public (srflx) — are sent
128
+ // immediately. The browser will attempt all paths in parallel and use
129
+ // whichever connects first. Private candidates trigger Chrome's Private
130
+ // Network Access (PNA) permission dialog once; after the user allows it
131
+ // the connection proceeds via the local LAN path.
83
132
  pc.onLocalCandidate((candidate, mid) => {
133
+ const isPrivate = isPrivateHostCandidate(candidate);
134
+ log(`[webrtc] Session ${sessionId.slice(0, 8)}: sending ${isPrivate ? "private" : "public"} candidate`);
84
135
  sendSignal(sessionId, { type: "candidate", candidate, mid });
85
136
  });
86
137
 
138
+ pc.onGatheringStateChange((state) => {
139
+ log(`[webrtc] Session ${sessionId.slice(0, 8)}: gathering → ${state}`);
140
+ });
141
+
87
142
  // Forward our SDP answer to the browser through the tunnel.
88
143
  pc.onLocalDescription((sdp, type) => {
89
144
  log(`[webrtc] Session ${sessionId.slice(0, 8)}: sending ${type}`);