@torrent-tv/proxy 2.9.17 → 2.9.18

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.9.18
2
+
3
+ - **New**: WebRTC is now reachable behind NAT via a static UDP port mapping. All sessions are pinned to a single UDP port (same number as the HTTP port, default 9090) and multiplexed over it (`enableIceUdpMux` + `portRangeBegin`/`portRangeEnd` in `webrtc-manager.js`), and that UDP port is UPnP/NAT-PMP-mapped at startup (a second `port-mapper.js` instance, protocol UDP, removed on shutdown). Because the socket is bound to a fixed, statically-mapped port, the proxy's `srflx` ICE candidate now carries `publicIP:9090` — reachable from the browser even behind symmetric NAT for that port (previously WebRTC used an ephemeral UDP port that UPnP could not map). Verified: two PeerConnections share the one UDP port with no bind conflict; host + srflx (v4 and global v6) candidates all carry the fixed port. The UDP endpoint is not reported to the server (the browser learns it via ICE, not the TCP dial-back probe).
4
+
1
5
  ## 2.9.17
2
6
 
3
7
  - **New**: The proxy reports its UPnP-mapped external endpoint to the server over the tunnel (new `proxy-endpoint` message: `{ externalIp, externalPort, protocol }` from `port-mapper.getMappedEndpoint()`). Sent when the mapping completes and re-sent on every tunnel (re)connect, so the server can dial back and verify the proxy is reachable from the internet (server 0.8.22). No effect if port mapping is disabled or failed.
package/bin/cli.js CHANGED
@@ -122,6 +122,9 @@ let tunnelClient = null;
122
122
  /** @type {ReturnType<typeof createPortMapper> | null} */
123
123
  let portMapper = null;
124
124
 
125
+ /** @type {ReturnType<typeof createPortMapper> | null} UDP mapping for the WebRTC port. */
126
+ let udpPortMapper = null;
127
+
125
128
 
126
129
  /**
127
130
  * Register this proxy with the registry server.
@@ -190,12 +193,16 @@ async function shutdown(signal) {
190
193
  }
191
194
  logger.warn(`Received ${signal}, shutting down...`);
192
195
  try {
193
- // Remove the router port mapping before exiting (lease expiry is the
196
+ // Remove the router port mappings before exiting (lease expiry is the
194
197
  // backstop if this is skipped on a hard kill).
195
198
  if (portMapper) {
196
199
  await portMapper.stop();
197
200
  portMapper = null;
198
201
  }
202
+ if (udpPortMapper) {
203
+ await udpPortMapper.stop();
204
+ udpPortMapper = null;
205
+ }
199
206
  if (app) {
200
207
  await app.close();
201
208
  }
@@ -248,6 +255,21 @@ try {
248
255
  const message = error instanceof Error ? error.message : String(error);
249
256
  logger.warn(`Port mapping failed to start: ${message}`);
250
257
  });
258
+
259
+ // Also map the WebRTC UDP port (same number, different protocol). All
260
+ // WebRTC sessions are multiplexed onto this single UDP port (ICE UDP mux),
261
+ // so a static mapping makes the proxy's WebRTC path reachable even behind
262
+ // symmetric NAT. This endpoint is NOT reported to the server: the browser
263
+ // discovers it via ICE (srflx) candidates, not the TCP dial-back probe.
264
+ udpPortMapper = createPortMapper({
265
+ port: actualPort,
266
+ protocol: "UDP",
267
+ description: "torrent-tv proxy (WebRTC)"
268
+ });
269
+ void udpPortMapper.start().catch((error) => {
270
+ const message = error instanceof Error ? error.message : String(error);
271
+ logger.warn(`UDP port mapping failed to start: ${message}`);
272
+ });
251
273
  } else {
252
274
  logger.info("Automatic port mapping is disabled (--no-port-mapping).");
253
275
  }
@@ -297,6 +319,9 @@ try {
297
319
  });
298
320
 
299
321
  webRtcManager = createWebRtcManager({
322
+ // Pin all WebRTC sessions to this single UDP port (multiplexed via ICE UDP
323
+ // mux) so the UPnP UDP mapping above makes the WebRTC path reachable.
324
+ udpPort: actualPort,
300
325
  sendSignal(sessionId, signal) {
301
326
  tunnelClient?.sendSignal(sessionId, signal);
302
327
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.17",
3
+ "version": "2.9.18",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -70,6 +70,11 @@ function isPrivateHostCandidate(candidate) {
70
70
  * `node-datachannel` `DataChannel` object; hand it to `createDataChannelHandler`.
71
71
  * @property {(message: string) => void} [onLog]
72
72
  * Optional log sink.
73
+ * @property {number} [udpPort]
74
+ * When set, every PeerConnection is pinned to this single UDP port and ICE
75
+ * UDP multiplexing is enabled, so all sessions share one port that can be
76
+ * statically UPnP-mapped (makes the WebRTC path reachable behind NAT). When
77
+ * omitted, node-datachannel uses an ephemeral UDP port (previous behaviour).
73
78
  */
74
79
 
75
80
  /**
@@ -89,10 +94,20 @@ function isPrivateHostCandidate(candidate) {
89
94
  * @param {WebRtcManagerOptions} options
90
95
  * @returns {WebRtcManager}
91
96
  */
92
- export function createWebRtcManager({ sendSignal, onDataChannel, onLog }) {
97
+ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort }) {
93
98
  /** @type {Map<string, import("node-datachannel").PeerConnection>} */
94
99
  const peers = new Map();
95
100
 
101
+ // Base PeerConnection config shared by every session. When a UDP port is
102
+ // configured, pin all sessions to it and enable ICE UDP mux so they share the
103
+ // single (UPnP-mapped) port; otherwise fall back to an ephemeral UDP port.
104
+ const pcConfig = { iceServers: ICE_SERVERS };
105
+ if (Number.isInteger(udpPort) && udpPort > 0 && udpPort <= 65535) {
106
+ pcConfig.enableIceUdpMux = true;
107
+ pcConfig.portRangeBegin = udpPort;
108
+ pcConfig.portRangeEnd = udpPort;
109
+ }
110
+
96
111
  /**
97
112
  * @param {string} message
98
113
  * @returns {void}
@@ -118,9 +133,7 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog }) {
118
133
  return existing;
119
134
  }
120
135
 
121
- const pc = new nodeDataChannel.PeerConnection(`proxy-${sessionId.slice(0, 8)}`, {
122
- iceServers: ICE_SERVERS
123
- });
136
+ const pc = new nodeDataChannel.PeerConnection(`proxy-${sessionId.slice(0, 8)}`, pcConfig);
124
137
 
125
138
  // Forward all ICE candidates to the browser through the tunnel.
126
139
  //