@torrent-tv/proxy 2.5.7 → 2.5.9

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 CHANGED
@@ -1,3 +1,7 @@
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
+
1
5
  ## 2.5.6
2
6
 
3
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.5.7",
3
+ "version": "2.5.9",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -93,15 +93,6 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog }) {
93
93
  /** @type {Map<string, import("node-datachannel").PeerConnection>} */
94
94
  const peers = new Map();
95
95
 
96
- /**
97
- * Per-session ICE candidate state.
98
- * Tracks buffered private candidates and whether a public (srflx/relay)
99
- * candidate has already been forwarded to the browser.
100
- *
101
- * @type {Map<string, { privateCandidates: Array<{candidate: string, mid: string}>, sentPublic: boolean }>}
102
- */
103
- const iceState = new Map();
104
-
105
96
  /**
106
97
  * @param {string} message
107
98
  * @returns {void}
@@ -131,58 +122,21 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog }) {
131
122
  iceServers: ICE_SERVERS
132
123
  });
133
124
 
134
- // Per-session ICE state for the public-first / private-fallback strategy.
135
- const ice = { privateCandidates: [], sentPublic: false };
136
- iceState.set(sessionId, ice);
137
-
138
- /**
139
- * Release any buffered private candidates to the browser.
140
- * Called as a last resort when ICE gathering completes without producing
141
- * a public (srflx/relay) candidate — e.g. when STUN is unreachable or
142
- * the NAT is symmetric. The browser will show a PNA permission dialog,
143
- * but at least connectivity is possible.
144
- */
145
- function flushPrivateCandidates() {
146
- if (ice.sentPublic || ice.privateCandidates.length === 0) {
147
- return;
148
- }
149
- log(`[webrtc] Session ${sessionId.slice(0, 8)}: no public ICE candidate available — falling back to private (PNA dialog will appear)`);
150
- for (const { candidate, mid } of ice.privateCandidates) {
151
- sendSignal(sessionId, { type: "candidate", candidate, mid });
152
- }
153
- ice.privateCandidates = [];
154
- }
155
-
156
- // Forward our ICE candidates to the browser through the tunnel.
125
+ // Forward all ICE candidates to the browser through the tunnel.
157
126
  //
158
- // Strategy: send public (srflx / relay) candidates immediately so the
159
- // browser can connect via the proxy's public IP this never triggers
160
- // Chrome's Private Network Access (PNA) permission dialog.
161
- // Private host candidates (RFC 1918, Docker bridge IPs, ULA IPv6) are
162
- // buffered. If ICE gathering completes without a public candidate the
163
- // buffer is flushed as a fallback so the user still gets a PNA dialog
164
- // rather than a silent failure.
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.
165
132
  pc.onLocalCandidate((candidate, mid) => {
166
- if (isPrivateHostCandidate(candidate)) {
167
- log(`[webrtc] Session ${sessionId.slice(0, 8)}: buffered private host candidate`);
168
- ice.privateCandidates.push({ candidate, mid });
169
- return;
170
- }
171
- // Public candidate — send immediately and discard the private buffer.
172
- if (!ice.sentPublic) {
173
- ice.sentPublic = true;
174
- ice.privateCandidates = [];
175
- log(`[webrtc] Session ${sessionId.slice(0, 8)}: sent public ICE candidate (PNA-free path)`);
176
- }
133
+ const isPrivate = isPrivateHostCandidate(candidate);
134
+ log(`[webrtc] Session ${sessionId.slice(0, 8)}: sending ${isPrivate ? "private" : "public"} candidate`);
177
135
  sendSignal(sessionId, { type: "candidate", candidate, mid });
178
136
  });
179
137
 
180
- // When gathering finishes, flush private candidates if no public one arrived.
181
138
  pc.onGatheringStateChange((state) => {
182
139
  log(`[webrtc] Session ${sessionId.slice(0, 8)}: gathering → ${state}`);
183
- if (state === "complete") {
184
- flushPrivateCandidates();
185
- }
186
140
  });
187
141
 
188
142
  // Forward our SDP answer to the browser through the tunnel.
@@ -259,7 +213,6 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog }) {
259
213
  if (pc) {
260
214
  try { pc.close(); } catch { /* ignore */ }
261
215
  peers.delete(sessionId);
262
- iceState.delete(sessionId);
263
216
  log(`[webrtc] Session ${sessionId.slice(0, 8)}: closed`);
264
217
  }
265
218
  }