@torrent-tv/proxy 2.9.16 → 2.9.17

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.17
2
+
3
+ - **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.
4
+
1
5
  ## 2.9.16
2
6
 
3
7
  - **New**: Automatic port mapping (`services/port-mapper.js`). At startup the proxy asks the home router to open its local port (default TCP 9090) via UPnP IGD / NAT-PMP using `@silentbot1/nat-api` (the same library WebTorrent already uses for the torrent port — no new host dependency). The mapping uses a 2 h lease auto-renewed while running and is removed on graceful shutdown (wired into the `cli.js` shutdown path; lease expiry is the backstop on a hard kill). Strictly best-effort: a router without UPnP/NAT-PMP is a normal case — it is logged and the proxy continues. Bounded by start/stop timeouts so a non-responding gateway never delays startup or hangs shutdown. Disable with `--no-port-mapping`. The discovered external endpoint is exposed via `getMappedEndpoint()` for the upcoming server-side reachability probe (not yet reported). `@silentbot1/nat-api` is now a direct dependency (was transitive via WebTorrent).
package/CLAUDE.md CHANGED
@@ -56,24 +56,29 @@ Decided direction — full plan in the parent `../CLAUDE.md`. Proxy-side pieces:
56
56
  same lib WebTorrent uses for the torrent port). Maps TCP 9090 with a 2 h
57
57
  auto-renewed lease, removed on shutdown (lease expiry covers hard kills).
58
58
  Best-effort + start/stop timeouts; `--no-port-mapping` opts out;
59
- `getMappedEndpoint()` exposes the external endpoint for the reachability
60
- probe. NOT yet done: mapping the **UDP** port WebRTC actually uses (it binds
61
- ephemeral UDP ports, so this TCP mapping does not yet help WebRTC — see the
62
- NAT-traversal toolbox in the parent CLAUDE.md), and reporting the endpoint
63
- to the server.
64
- - Report the mapped external endpoint (and local addresses) to the server over
65
- the tunnel; the server dial-back-verifies reachability before use.
59
+ `getMappedEndpoint()` exposes the external endpoint. NOT yet done: mapping the
60
+ **UDP** port WebRTC actually uses (it binds ephemeral UDP ports, so this TCP
61
+ mapping does not yet help WebRTC — roadmap step 3 in the parent CLAUDE.md).
62
+ Also pending (next iteration): a success log line in `port-mapper.js` `stop()`
63
+ (`removed mapping for TCP <port>`) — today stop() only logs on failure, so a
64
+ clean unmap on shutdown is silent.
65
+ - **Report endpoint to server** DONE (proxy 2.9.17). The mapped endpoint
66
+ is sent over the tunnel (`tunnel-client.sendEndpoint` → `proxy-endpoint`) on
67
+ mapping success and on every tunnel (re)connect; the server dial-back-verifies
68
+ reachability (server 0.8.22, roadmap step 2).
66
69
  - **HTTPS listener**: serve the existing routes over TLS with a per-proxy
67
70
  certificate delivered by the server through the tunnel (persist cert+key
68
71
  locally; ~90-day renewals are pushed the same way). Add CORS headers for the
69
72
  web-app origin so hls.js / `<video>` can fetch cross-origin.
70
73
  - Plain HTTPS becomes the preferred video transport; WebRTC data channel stays
71
74
  as fallback for hosts where no port could be opened.
72
- - **NAT-traversal toolbox** (staged, see parent `../CLAUDE.md`): birthday-
73
- paradox port prediction (open ~256 UDP sockets, inject predicted-port ICE
74
- candidates) for symmetric NAT; IPv6-first (audit the candidate filter — do
75
- not drop *global* v6); STUN-based NAT pre-classification at startup reported
76
- to the registry; relay-then-upgrade later.
75
+ - **Later roadmap steps** (single staged roadmap in parent `../CLAUDE.md`,
76
+ WebRTC-first ordering): step 3 map the WebRTC UDP port (fixed
77
+ `portRangeBegin`/`End` + UPnP-map UDP); step 4 birthday-paradox port
78
+ prediction (open ~256 UDP sockets, inject predicted-port ICE candidates) for
79
+ symmetric NAT; step 5 IPv6-first (audit the candidate filter — do not drop
80
+ *global* v6) + STUN NAT pre-classification reported to the registry; then
81
+ the DNS+TLS path (steps 6–7); step 8 relay-then-upgrade (deferred).
77
82
  - Future: ed25519 proxy identity (sign announcements), BEP 44 endpoint
78
83
  announcements via the `bittorrent-dht` already bundled with WebTorrent.
79
84
 
@@ -82,17 +87,16 @@ All of this must stay deployment-agnostic (HA addon, bare npm, Docker).
82
87
  ## Disk hygiene (open item — torrent data is NOT cleaned up today)
83
88
 
84
89
  HLS segments are handled (`hls-session-manager.js`: idle TTL, `disposeSession`,
85
- `disposeAll`). Torrent data is NOT: `new WebTorrent()` in `torrent-pool.js` uses
86
- the default FS store under `os.tmpdir()`, there is no `client.remove()` /
87
- `torrent.destroy()`, `deselect()` only stops further download, nothing sweeps
88
- orphans at startup, and `TorrentPool` is not wired into the `onClose` shutdown
89
- hook (`server.js` only disposes HLS sessions on close — see `cli.js` shutdown →
90
- `app.close()` → `onClose`).
91
-
92
- Level 1 (do first): `client.remove(torrent, { destroyStore: true })` on last-
93
- file refcount 0 + idle TTL; startup sweep of orphaned store dirs; wire
94
- `TorrentPool` teardown into the `onClose` hook so closing the proxy cleans up
95
- (not only startup); global disk cap with LRU eviction of whole torrents.
90
+ `disposeAll`). Torrent data is **partially** handled: shutdown cleanup is done
91
+ (`TorrentPool.destroyAll()` with `destroyStore: true`, wired into the `onClose`
92
+ hook — proxy 2.9.15), but `deselect()` only stops further download and nothing
93
+ removes a torrent's data **while the proxy keeps running**, nor sweeps orphans
94
+ left by a previous hard kill at startup.
95
+
96
+ Level 1 — remaining: `client.remove(torrent, { destroyStore: true })` on last-
97
+ file refcount 0 + idle TTL (mirror the HLS session model); startup sweep of
98
+ orphaned store dirs under `os.tmpdir()`; global disk cap with LRU eviction of
99
+ whole torrents. (Shutdown teardown done.)
96
100
  Level 2 (research): sliding-window chunk store. Full rationale in the parent
97
101
  `../CLAUDE.md` "Disk hygiene" section.
98
102
 
package/bin/cli.js CHANGED
@@ -234,10 +234,20 @@ try {
234
234
  // delay tunnel connect / registration, so we do not await it.
235
235
  if (portMappingEnabled) {
236
236
  portMapper = createPortMapper({ port: actualPort, protocol: "TCP" });
237
- void portMapper.start().catch((error) => {
238
- const message = error instanceof Error ? error.message : String(error);
239
- logger.warn(`Port mapping failed to start: ${message}`);
240
- });
237
+ void portMapper
238
+ .start()
239
+ .then(() => {
240
+ // Mapping may finish after the tunnel is already connected; report the
241
+ // endpoint now. If the tunnel is not open yet, onConnect re-sends it.
242
+ const endpoint = portMapper?.getMappedEndpoint();
243
+ if (endpoint) {
244
+ tunnelClient?.sendEndpoint(endpoint);
245
+ }
246
+ })
247
+ .catch((error) => {
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ logger.warn(`Port mapping failed to start: ${message}`);
250
+ });
241
251
  } else {
242
252
  logger.info("Automatic port mapping is disabled (--no-port-mapping).");
243
253
  }
@@ -270,6 +280,13 @@ try {
270
280
  const message = error instanceof Error ? error.message : String(error);
271
281
  logger.error(`Re-registration after tunnel connect failed: ${message}`);
272
282
  });
283
+ // Re-report the mapped endpoint on every (re)connect — the server's
284
+ // in-memory reachability state resets on restart, and the mapping may
285
+ // have completed before this connection existed.
286
+ const endpoint = portMapper?.getMappedEndpoint();
287
+ if (endpoint) {
288
+ tunnelClient?.sendEndpoint(endpoint);
289
+ }
273
290
  },
274
291
  onLog: (message) => logger.info(message)
275
292
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.16",
3
+ "version": "2.9.17",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -72,6 +72,9 @@ import { WebSocket } from "ws";
72
72
  * @property {() => void} disconnect - Close the tunnel; suppresses reconnects.
73
73
  * @property {(sessionId: string, signal: WebRtcSignal) => void} sendSignal
74
74
  * Send a WebRTC signal (answer / candidate) back to the browser.
75
+ * @property {(endpoint: { externalIp: string | null, externalPort: number, protocol: string }) => void} sendEndpoint
76
+ * Report this proxy's UPnP-mapped external endpoint to the server so it can
77
+ * dial back and verify reachability.
75
78
  */
76
79
 
77
80
  const RECONNECT_DELAY_MS = 5_000;
@@ -312,6 +315,18 @@ export function createTunnelClient({ serverUrl, proxyId, token, proxyPort, onSig
312
315
  */
313
316
  sendSignal(sessionId, signal) {
314
317
  send({ type: "signal", sessionId, signal });
318
+ },
319
+
320
+ /**
321
+ * Report this proxy's UPnP-mapped external endpoint to the server.
322
+ * No-op if the tunnel is not currently open (the caller re-sends on
323
+ * connect / reconnect).
324
+ *
325
+ * @param {{ externalIp: string | null, externalPort: number, protocol: string }} endpoint
326
+ * @returns {void}
327
+ */
328
+ sendEndpoint(endpoint) {
329
+ send({ type: "proxy-endpoint", endpoint });
315
330
  }
316
331
  };
317
332
  }