fix(network): harden route-aware torrent resolution

- keep reqwest metadata on its supported proxy route and require Aria2 asynchronous DNS
- fence magnet probe ownership and delayed RPC cleanup without cross-transfer removal
- validate Torrent DHT nodes, web seeds, proxy inputs, and literal IPv4/IPv6 targets
- extend regression and smoke coverage for issue #38

Refs: #38
This commit is contained in:
NimBold
2026-08-30 14:58:01 +03:30
parent 1d629873b5
commit b564e92532
8 changed files with 918 additions and 709 deletions
+10 -8
View File
@@ -189,6 +189,7 @@ const child = spawn(binaryPath, [
`--dir=${tempRoot}`,
'--file-allocation=none',
'--enable-dht=false',
'--async-dns=true',
'--console-log-level=error',
'--quiet=true',
], { env: environment, stdio: ['ignore', 'ignore', 'pipe'] });
@@ -198,15 +199,17 @@ child.stderr.on('data', chunk => { stderr += chunk.toString(); });
try {
const version = await waitForRpc(rpcPort, secret);
const features = Array.isArray(version.enabledFeatures) ? version.enabledFeatures : [];
console.log(`[INFO] aria2 ${version.version || 'unknown'}; Async DNS: ${features.includes('Async DNS') ? 'supported' : 'not advertised'}`);
if (!features.includes('Async DNS')) {
throw new Error(`packaged aria2 must advertise Async DNS for route-safe transfers: ${JSON.stringify(version)}`);
}
console.log(`[INFO] aria2 ${version.version || 'unknown'}; Async DNS: supported`);
const uriResult = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${contentPort}/file`], {
'async-dns': 'false',
out: 'resolver-normal.bin',
}]);
const uriOptions = await rpc(rpcPort, secret, 'aria2.getOption', [uriResult]);
if (uriOptions['async-dns'] !== 'false') {
throw new Error(`aria2.addUri did not retain async-dns=false: ${JSON.stringify(uriOptions)}`);
if (uriOptions['async-dns'] === 'false') {
throw new Error(`direct aria2.addUri unexpectedly disabled asynchronous DNS: ${JSON.stringify(uriOptions)}`);
}
const torrent = bencode({
@@ -218,12 +221,11 @@ try {
},
}).toString('base64');
const torrentResult = await rpc(rpcPort, secret, 'aria2.addTorrent', [torrent, [], {
'async-dns': 'false',
dir: tempRoot,
}]);
const torrentOptions = await rpc(rpcPort, secret, 'aria2.getOption', [torrentResult]);
if (torrentOptions['async-dns'] !== 'false') {
throw new Error(`aria2.addTorrent did not retain async-dns=false: ${JSON.stringify(torrentOptions)}`);
if (torrentOptions['async-dns'] === 'false') {
throw new Error(`direct aria2.addTorrent unexpectedly disabled asynchronous DNS: ${JSON.stringify(torrentOptions)}`);
}
const proxyRoute = 'http://127.0.0.1:9';
@@ -256,7 +258,7 @@ try {
await forceRemoveIfPresent(rpcPort, secret, proxiedUriResult);
await forceRemoveIfPresent(rpcPort, secret, proxiedTorrentResult);
console.log('[PASS] Aria2 retained system-resolver mode for direct normal/Torrent transfers and configured proxy mode for proxied transfers');
console.log('[PASS] Aria2 kept asynchronous DNS for fresh direct and proxied normal/Torrent transfers');
} catch (error) {
const detail = stderr.trim();
throw new Error(`${error.message}${detail ? `\n${detail}` : ''}`);
+4 -5
View File
@@ -305,6 +305,7 @@ async function startDaemon({ name, rpcPort, listenPort, directory, extraArgs = [
'--enable-dht=false',
'--enable-peer-exchange=false',
'--bt-enable-lpd=false',
'--async-dns=true',
'--console-log-level=error',
'--quiet=true',
...(selectedListenPort ? [`--listen-port=${selectedListenPort}`] : []),
@@ -720,7 +721,6 @@ async function main() {
'bt-metadata-only': 'false',
'bt-save-metadata': 'false',
'follow-torrent': 'false',
'async-dns': 'false',
'max-tries': '3',
'retry-wait': '2',
'connect-timeout': '20',
@@ -767,7 +767,7 @@ async function main() {
assert(directHandoff.parent.status === 'complete', `normal magnet parent did not complete metadata: ${JSON.stringify(directHandoff.parent)}`);
assert(directHandoff.parent.files?.some(file => String(file.path).startsWith('[METADATA]')), 'normal magnet parent did not expose a metadata file');
assert(directOptions['bt-metadata-only'] === 'false', 'normal magnet child did not retain payload mode');
assert(directOptions['async-dns'] === 'false', 'direct Torrent did not retain system DNS resolution');
assert(directOptions['async-dns'] !== 'false', 'fresh direct Torrent unexpectedly disabled asynchronous DNS');
await rpc(client.rpcPort, client.secret, 'aria2.removeDownloadResult', [directGid]);
try {
await rpc(client.rpcPort, client.secret, 'aria2.removeDownloadResult', [directHandoff.childGid]);
@@ -790,7 +790,6 @@ async function main() {
dir: probeDir,
'bt-metadata-only': 'true',
'bt-save-metadata': 'true',
'async-dns': 'false',
'max-tries': '3',
'retry-wait': '1',
'connect-timeout': '5',
@@ -798,7 +797,7 @@ async function main() {
'auto-file-renaming': 'false',
}]);
const probeOptions = await rpc(client.rpcPort, client.secret, 'aria2.getOption', [probeGid]);
assert(probeOptions['async-dns'] === 'false', 'direct magnet metadata probe did not retain system DNS resolution');
assert(probeOptions['async-dns'] !== 'false', 'fresh direct magnet probe unexpectedly disabled asynchronous DNS');
const probeStatus = await waitForTerminal(client, probeGid, 30000);
assert(probeStatus.status === 'complete', 'magnet metadata probe did not complete');
const savedTorrentPaths = fs.readdirSync(probeDir)
@@ -859,7 +858,7 @@ async function main() {
assert(proxiedProbeOptions['async-dns'] !== 'false', 'proxied magnet metadata probe unexpectedly forced system DNS resolution');
assert(await forceRemoveIfPresent(client, proxiedProbeGid), 'proxied magnet metadata probe was not removable');
await waitForRemoved(client, proxiedProbeGid);
console.log('[OK] metadata probe was removed after resolution; direct and proxied resolver modes were retained');
console.log('[OK] metadata probe was removed after resolution; direct and proxied probes kept asynchronous DNS');
const finalGid = await rpc(client.rpcPort, client.secret, 'aria2.addTorrent', [
trackerlessTorrentBytes.toString('base64'),