mirror of
https://github.com/baairon/torlink.git
synced 2026-07-08 18:28:22 +02:00
fix: disable NAT-PMP on macOS to prevent EADDRINUSE crash on port 5350 (#45)
macOS mDNSResponder permanently occupies UDP port 5350, which is the NAT-PMP client port that webtorrent binds when initializing its NAT traversal. The bind fails asynchronously with EADDRINUSE, and since the PMP client is a raw EventEmitter with no error listener, it surfaces as an uncaughtException that kills the app the moment a download starts. NAT-PMP can never succeed on macOS because the port is permanently taken, so disable it on darwin only and let UPnP handle NAT traversal instead. Other platforms are unaffected. Closes #22
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
|
||||
const constructorCalls: Record<string, unknown>[] = [];
|
||||
|
||||
vi.mock("webtorrent", () => {
|
||||
return {
|
||||
default: class extends EventEmitter {
|
||||
torrentPort = 6881;
|
||||
constructor(opts?: Record<string, unknown>) {
|
||||
super();
|
||||
constructorCalls.push(opts ?? {});
|
||||
}
|
||||
add(): EventEmitter {
|
||||
return new EventEmitter();
|
||||
}
|
||||
destroy(): void {}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
constructorCalls.length = 0;
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe("TorrentEngine macOS port-5350 fix (#22)", () => {
|
||||
it("passes natPmp:false on macOS so mDNSResponder's port 5350 is never bound", async () => {
|
||||
const { TorrentEngine } = await import("./engine");
|
||||
const original = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
try {
|
||||
const engine = new TorrentEngine();
|
||||
engine.add(
|
||||
"test-id",
|
||||
"magnet:?xt=urn:btih:0000000000000000000000000000000000000000",
|
||||
"/downloads",
|
||||
{},
|
||||
);
|
||||
engine.destroy();
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: original });
|
||||
}
|
||||
expect(constructorCalls).toHaveLength(1);
|
||||
expect(constructorCalls[0]).toMatchObject({ natPmp: false });
|
||||
});
|
||||
|
||||
it("does not disable natPmp on Linux (port 5350 is free)", async () => {
|
||||
const { TorrentEngine } = await import("./engine");
|
||||
const original = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
try {
|
||||
const engine = new TorrentEngine();
|
||||
engine.add(
|
||||
"test-id",
|
||||
"magnet:?xt=urn:btih:0000000000000000000000000000000000000000",
|
||||
"/downloads",
|
||||
{},
|
||||
);
|
||||
engine.destroy();
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: original });
|
||||
}
|
||||
expect(constructorCalls).toHaveLength(1);
|
||||
expect(constructorCalls[0]).not.toHaveProperty("natPmp", false);
|
||||
});
|
||||
|
||||
it("does not disable natPmp on Windows (port 5350 is free)", async () => {
|
||||
const { TorrentEngine } = await import("./engine");
|
||||
const original = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
try {
|
||||
const engine = new TorrentEngine();
|
||||
engine.add(
|
||||
"test-id",
|
||||
"magnet:?xt=urn:btih:0000000000000000000000000000000000000000",
|
||||
"/downloads",
|
||||
{},
|
||||
);
|
||||
engine.destroy();
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: original });
|
||||
}
|
||||
expect(constructorCalls).toHaveLength(1);
|
||||
expect(constructorCalls[0]).not.toHaveProperty("natPmp", false);
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,15 @@ export class TorrentEngine {
|
||||
|
||||
private ensureClient(): WebTorrent {
|
||||
if (!this.client) {
|
||||
this.client = new WebTorrent();
|
||||
// On macOS, mDNSResponder occupies UDP port 5350 — the NAT-PMP
|
||||
// client port. Binding it fails asynchronously with EADDRINUSE,
|
||||
// and since the PMP client is a raw EventEmitter with no error
|
||||
// listener, the error surfaces as an uncaughtException that kills
|
||||
// the app the moment a download starts. NAT-PMP can never succeed
|
||||
// on macOS because the port is permanently taken, so disable it
|
||||
// and let UPnP handle NAT traversal instead.
|
||||
const opts = process.platform === "darwin" ? { natPmp: false } : {};
|
||||
this.client = new WebTorrent(opts);
|
||||
this.client.on("error", () => {});
|
||||
}
|
||||
return this.client;
|
||||
|
||||
Vendored
+2
@@ -42,6 +42,8 @@ declare module "webtorrent" {
|
||||
utp?: boolean;
|
||||
tracker?: boolean;
|
||||
lsd?: boolean;
|
||||
natPmp?: boolean;
|
||||
natUpnp?: boolean | "permanent";
|
||||
}
|
||||
|
||||
class WebTorrent extends EventEmitter {
|
||||
|
||||
Reference in New Issue
Block a user