mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-09 17:25:42 +00:00
fix(network): harden TUN-aware DNS routing (#38)
- Keep hostname resolution on the active OS, TUN, proxy, or Aria2 route while blocking literal local targets.\n- Propagate and attest the Firelink Aria2 resolver and network-policy contract across normal, Torrent, and magnet paths.\n- Fence magnet probe cleanup, redirect/range policy, lifecycle races, and engine staging with regression and smoke coverage.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
export const ARIA2_FIRELINK_REVISION = 'firelink-native-dns-v1';
|
||||
export const ARIA2_DNS_RESOLVER = 'native-async';
|
||||
export const ARIA2_NETWORK_TARGET_POLICY = 'firelink-v1';
|
||||
export const ARIA2_NETWORK_TARGET_POLICY_DIGEST =
|
||||
'sha256:064503d30f1a043e79113f7e44ddfb517fbf2c578a332896355180743eaf1705';
|
||||
|
||||
export const ARIA2_ROUTE_DAEMON_ARGS = Object.freeze([
|
||||
'--async-dns=true',
|
||||
`--dns-resolver=${ARIA2_DNS_RESOLVER}`,
|
||||
`--network-target-policy=${ARIA2_NETWORK_TARGET_POLICY}`,
|
||||
]);
|
||||
|
||||
export const ARIA2_ROUTE_OPTIONS = Object.freeze({
|
||||
'async-dns': 'true',
|
||||
'dns-resolver': ARIA2_DNS_RESOLVER,
|
||||
'network-target-policy': ARIA2_NETWORK_TARGET_POLICY,
|
||||
});
|
||||
|
||||
// Local HTTP servers are used only by engine smoke tests. Product transfers
|
||||
// never apply this override; Firelink keeps the literal-target policy enabled.
|
||||
export const ARIA2_LOCAL_FIXTURE_OPTIONS = Object.freeze({
|
||||
...ARIA2_ROUTE_OPTIONS,
|
||||
'network-target-policy': 'none',
|
||||
});
|
||||
|
||||
export function assertAria2RouteCapabilities(version) {
|
||||
if (!Array.isArray(version?.enabledFeatures)
|
||||
|| !version.enabledFeatures.includes('Async DNS')) {
|
||||
throw new Error(`aria2 does not advertise asynchronous DNS: ${JSON.stringify(version)}`);
|
||||
}
|
||||
const expected = {
|
||||
firelinkRevision: ARIA2_FIRELINK_REVISION,
|
||||
firelinkDnsResolver: ARIA2_DNS_RESOLVER,
|
||||
firelinkNetworkTargetPolicyDigest: ARIA2_NETWORK_TARGET_POLICY_DIGEST,
|
||||
};
|
||||
for (const [field, value] of Object.entries(expected)) {
|
||||
if (version?.[field] !== value) {
|
||||
throw new Error(`aria2 route capability mismatch for ${field}: ${JSON.stringify(version)}`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(version.firelinkNetworkTargetPolicies)
|
||||
|| !version.firelinkNetworkTargetPolicies.includes(ARIA2_NETWORK_TARGET_POLICY)) {
|
||||
throw new Error(`aria2 does not advertise the Firelink target policy: ${JSON.stringify(version)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertAria2RouteContract(version) {
|
||||
assertAria2RouteCapabilities(version);
|
||||
const expected = {
|
||||
firelinkRevision: ARIA2_FIRELINK_REVISION,
|
||||
firelinkDnsResolver: ARIA2_DNS_RESOLVER,
|
||||
firelinkNetworkTargetPolicy: ARIA2_NETWORK_TARGET_POLICY,
|
||||
firelinkNetworkTargetPolicyDigest: ARIA2_NETWORK_TARGET_POLICY_DIGEST,
|
||||
};
|
||||
for (const [field, value] of Object.entries(expected)) {
|
||||
if (version?.[field] !== value) {
|
||||
throw new Error(`aria2 route contract mismatch for ${field}: ${JSON.stringify(version)}`);
|
||||
}
|
||||
}
|
||||
if (version.firelinkNetworkTargetPolicyEnforced !== true) {
|
||||
throw new Error(`aria2 is not enforcing the Firelink network target policy: ${JSON.stringify(version)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertAria2RouteOptions(options, context = 'aria2 transfer') {
|
||||
for (const [field, value] of Object.entries(ARIA2_ROUTE_OPTIONS)) {
|
||||
if (options?.[field] !== value) {
|
||||
throw new Error(`${context} did not retain ${field}=${value}: ${JSON.stringify(options)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function assertAria2RouteSource(source, target) {
|
||||
const contract = source?.firelinkRouteContract;
|
||||
const expected = {
|
||||
revision: ARIA2_FIRELINK_REVISION,
|
||||
dnsResolver: ARIA2_DNS_RESOLVER,
|
||||
networkTargetPolicy: ARIA2_NETWORK_TARGET_POLICY,
|
||||
networkTargetPolicyDigest: ARIA2_NETWORK_TARGET_POLICY_DIGEST,
|
||||
};
|
||||
for (const [field, value] of Object.entries(expected)) {
|
||||
if (contract?.[field] !== value) {
|
||||
throw new Error(
|
||||
`aria2c source for ${target} is not a Firelink route-contract build; `
|
||||
+ `expected firelinkRouteContract.${field}=${value}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
ARIA2_DNS_RESOLVER,
|
||||
ARIA2_FIRELINK_REVISION,
|
||||
ARIA2_NETWORK_TARGET_POLICY,
|
||||
ARIA2_NETWORK_TARGET_POLICY_DIGEST,
|
||||
assertAria2RouteCapabilities,
|
||||
assertAria2RouteContract,
|
||||
assertAria2RouteSource,
|
||||
} from './aria2-route-contract.js';
|
||||
|
||||
const secureVersion = {
|
||||
enabledFeatures: ['Async DNS'],
|
||||
firelinkRevision: ARIA2_FIRELINK_REVISION,
|
||||
firelinkDnsResolver: ARIA2_DNS_RESOLVER,
|
||||
firelinkNetworkTargetPolicies: ['none', ARIA2_NETWORK_TARGET_POLICY],
|
||||
firelinkNetworkTargetPolicy: ARIA2_NETWORK_TARGET_POLICY,
|
||||
firelinkNetworkTargetPolicyDigest: ARIA2_NETWORK_TARGET_POLICY_DIGEST,
|
||||
firelinkNetworkTargetPolicyEnforced: true,
|
||||
};
|
||||
|
||||
test('Aria2 source metadata must identify the Firelink route contract', () => {
|
||||
const source = {
|
||||
firelinkRouteContract: {
|
||||
revision: ARIA2_FIRELINK_REVISION,
|
||||
dnsResolver: ARIA2_DNS_RESOLVER,
|
||||
networkTargetPolicy: ARIA2_NETWORK_TARGET_POLICY,
|
||||
networkTargetPolicyDigest: ARIA2_NETWORK_TARGET_POLICY_DIGEST,
|
||||
},
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => assertAria2RouteSource(source, 'test-target'));
|
||||
assert.throws(
|
||||
() => assertAria2RouteSource({}, 'test-target'),
|
||||
/not a Firelink route-contract build/,
|
||||
);
|
||||
assert.throws(
|
||||
() => assertAria2RouteSource({
|
||||
firelinkRouteContract: {
|
||||
...source.firelinkRouteContract,
|
||||
networkTargetPolicyDigest: 'sha256:wrong',
|
||||
},
|
||||
}, 'test-target'),
|
||||
/networkTargetPolicyDigest/,
|
||||
);
|
||||
});
|
||||
|
||||
test('route capabilities are distinct from the active local-fixture policy', () => {
|
||||
assert.doesNotThrow(() => assertAria2RouteCapabilities({
|
||||
...secureVersion,
|
||||
firelinkNetworkTargetPolicy: 'none',
|
||||
firelinkNetworkTargetPolicyEnforced: false,
|
||||
}));
|
||||
assert.doesNotThrow(() => assertAria2RouteContract(secureVersion));
|
||||
assert.throws(
|
||||
() => assertAria2RouteContract({
|
||||
...secureVersion,
|
||||
firelinkNetworkTargetPolicy: 'none',
|
||||
firelinkNetworkTargetPolicyEnforced: false,
|
||||
}),
|
||||
/route contract mismatch for firelinkNetworkTargetPolicy/,
|
||||
);
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
removeOrphanedProvisioningDirectories,
|
||||
removePathWithRetry,
|
||||
} from './engine-payload-promotion.js';
|
||||
import { assertAria2RouteSource } from './aria2-route-contract.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
@@ -39,6 +40,17 @@ if (!targetSources) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Firelink passes route-contract options to every production Aria2 daemon.
|
||||
// A stock archive would either reject those options or silently omit the
|
||||
// literal-target policy on non-macOS targets, so fail before downloading or
|
||||
// staging an unusable payload.
|
||||
assertAria2RouteSource(targetSources.aria2c, target);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const destination = path.join(repoRoot, 'src-tauri', 'provisioned-engines', target);
|
||||
const isWindows = target.includes('windows');
|
||||
const executableSuffix = isWindows ? '.exe' : '';
|
||||
@@ -134,7 +146,10 @@ function writePayloadManifest() {
|
||||
{
|
||||
version: source.version,
|
||||
url: source.url || source.sourceUrl,
|
||||
sha256: source.sha256 || source.sourceSha256
|
||||
sha256: source.sha256 || source.sourceSha256,
|
||||
...(name === 'aria2c' && source.firelinkRouteContract
|
||||
? { firelinkRouteContract: source.firelinkRouteContract }
|
||||
: {})
|
||||
}
|
||||
])
|
||||
),
|
||||
|
||||
@@ -8,6 +8,12 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
ARIA2_ROUTE_DAEMON_ARGS,
|
||||
ARIA2_ROUTE_OPTIONS,
|
||||
assertAria2RouteContract,
|
||||
assertAria2RouteOptions,
|
||||
} from './aria2-route-contract.js';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const arch = { x64: 'x86_64', arm64: 'aarch64' }[os.arch()];
|
||||
@@ -195,7 +201,7 @@ const child = spawn(binaryPath, [
|
||||
`--dir=${tempRoot}`,
|
||||
'--file-allocation=none',
|
||||
'--enable-dht=false',
|
||||
'--async-dns=true',
|
||||
...ARIA2_ROUTE_DAEMON_ARGS,
|
||||
'--console-log-level=error',
|
||||
'--quiet=true',
|
||||
], { env: environment, stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
@@ -204,18 +210,17 @@ child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
||||
|
||||
try {
|
||||
const version = await waitForRpc(rpcPort, secret);
|
||||
const features = Array.isArray(version.enabledFeatures) ? version.enabledFeatures : [];
|
||||
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`);
|
||||
assertAria2RouteContract(version);
|
||||
console.log(`[INFO] aria2 ${version.version || 'unknown'}; Firelink route contract verified`);
|
||||
|
||||
const uriResult = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${contentPort}/file`], {
|
||||
...ARIA2_ROUTE_OPTIONS,
|
||||
'network-target-policy': 'none',
|
||||
out: 'resolver-normal.bin',
|
||||
}]);
|
||||
const uriOptions = await rpc(rpcPort, secret, 'aria2.getOption', [uriResult]);
|
||||
if (uriOptions['async-dns'] === 'false') {
|
||||
throw new Error(`direct aria2.addUri unexpectedly disabled asynchronous DNS: ${JSON.stringify(uriOptions)}`);
|
||||
if (uriOptions['async-dns'] !== 'true' || uriOptions['dns-resolver'] !== 'native-async') {
|
||||
throw new Error(`direct fixture transfer did not retain native asynchronous DNS: ${JSON.stringify(uriOptions)}`);
|
||||
}
|
||||
|
||||
const torrent = bencode({
|
||||
@@ -227,16 +232,16 @@ try {
|
||||
},
|
||||
}).toString('base64');
|
||||
const torrentResult = await rpc(rpcPort, secret, 'aria2.addTorrent', [torrent, [], {
|
||||
...ARIA2_ROUTE_OPTIONS,
|
||||
dir: tempRoot,
|
||||
}]);
|
||||
const torrentOptions = await rpc(rpcPort, secret, 'aria2.getOption', [torrentResult]);
|
||||
if (torrentOptions['async-dns'] === 'false') {
|
||||
throw new Error(`direct aria2.addTorrent unexpectedly disabled asynchronous DNS: ${JSON.stringify(torrentOptions)}`);
|
||||
}
|
||||
assertAria2RouteOptions(torrentOptions, 'direct aria2.addTorrent');
|
||||
|
||||
const proxyRoute = 'http://127.0.0.1:9';
|
||||
const normalizedProxyRoute = new URL(proxyRoute).toString();
|
||||
const proxiedUriResult = await rpc(rpcPort, secret, 'aria2.addUri', [['https://route-owned.invalid/file'], {
|
||||
...ARIA2_ROUTE_OPTIONS,
|
||||
'all-proxy': proxyRoute,
|
||||
pause: 'true',
|
||||
out: 'resolver-proxied.bin',
|
||||
@@ -245,11 +250,10 @@ try {
|
||||
if (proxiedUriOptions['all-proxy'] !== normalizedProxyRoute) {
|
||||
throw new Error(`aria2.addUri did not retain the configured proxy route: ${JSON.stringify(proxiedUriOptions)}`);
|
||||
}
|
||||
if (proxiedUriOptions['async-dns'] === 'false') {
|
||||
throw new Error(`proxied aria2.addUri unexpectedly forced system DNS resolution: ${JSON.stringify(proxiedUriOptions)}`);
|
||||
}
|
||||
assertAria2RouteOptions(proxiedUriOptions, 'proxied aria2.addUri');
|
||||
|
||||
const proxiedTorrentResult = await rpc(rpcPort, secret, 'aria2.addTorrent', [torrent, [], {
|
||||
...ARIA2_ROUTE_OPTIONS,
|
||||
'all-proxy': proxyRoute,
|
||||
pause: 'true',
|
||||
dir: tempRoot,
|
||||
@@ -258,9 +262,7 @@ try {
|
||||
if (proxiedTorrentOptions['all-proxy'] !== normalizedProxyRoute) {
|
||||
throw new Error(`aria2.addTorrent did not retain the configured proxy route: ${JSON.stringify(proxiedTorrentOptions)}`);
|
||||
}
|
||||
if (proxiedTorrentOptions['async-dns'] === 'false') {
|
||||
throw new Error(`proxied aria2.addTorrent unexpectedly forced system DNS resolution: ${JSON.stringify(proxiedTorrentOptions)}`);
|
||||
}
|
||||
assertAria2RouteOptions(proxiedTorrentOptions, 'proxied aria2.addTorrent');
|
||||
await forceRemoveIfPresent(rpcPort, secret, proxiedUriResult);
|
||||
await forceRemoveIfPresent(rpcPort, secret, proxiedTorrentResult);
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
ARIA2_ROUTE_DAEMON_ARGS,
|
||||
assertAria2RouteContract,
|
||||
} from './aria2-route-contract.js';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const arch = { x64: 'x86_64', arm64: 'aarch64' }[os.arch()];
|
||||
@@ -326,6 +331,7 @@ const child = spawn(binaryPath, [
|
||||
'--enable-dht=false',
|
||||
'--console-log-level=error',
|
||||
'--quiet=true',
|
||||
...ARIA2_ROUTE_DAEMON_ARGS,
|
||||
`--server-stat-if=${serverStatPath}`,
|
||||
`--server-stat-of=${serverStatPath}`,
|
||||
], { env: environment, stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
@@ -334,9 +340,11 @@ child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
||||
|
||||
try {
|
||||
const version = await waitForRpc(rpcPort, secret);
|
||||
assertAria2RouteContract(version);
|
||||
console.log(`[INFO] aria2 ${version.version || 'unknown'} normal-transfer smoke`);
|
||||
|
||||
const rangeGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/range`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'range.bin', split: '4', 'max-connection-per-server': '4', 'min-split-size': '1M',
|
||||
}]);
|
||||
const rangeStatus = await waitForTerminal(rpcPort, secret, rangeGid);
|
||||
@@ -345,6 +353,7 @@ try {
|
||||
}
|
||||
|
||||
const noRangeGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/no-range`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'no-range.bin', split: '1', 'max-connection-per-server': '1',
|
||||
}]);
|
||||
if ((await waitForTerminal(rpcPort, secret, noRangeGid)).status !== 'complete') {
|
||||
@@ -352,6 +361,7 @@ try {
|
||||
}
|
||||
|
||||
const authenticatedGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/authenticated`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'authenticated.bin', 'http-user': 'fixture-user', 'http-passwd': 'fixture-password',
|
||||
header: ['Cookie: fixture-cookie=present', 'X-Firelink-Auth: present'],
|
||||
}]);
|
||||
@@ -360,6 +370,7 @@ try {
|
||||
}
|
||||
|
||||
const resumeGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/throttled`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'resume.bin', split: '1', continue: 'true',
|
||||
}]);
|
||||
await waitForProgress(rpcPort, secret, resumeGid);
|
||||
@@ -374,6 +385,7 @@ try {
|
||||
}
|
||||
|
||||
const cancelGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/throttled`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'cancel.bin', split: '1',
|
||||
}]);
|
||||
await waitForProgress(rpcPort, secret, cancelGid);
|
||||
@@ -386,17 +398,19 @@ try {
|
||||
const mirrorGid = await rpc(rpcPort, secret, 'aria2.addUri', [[
|
||||
`http://127.0.0.1:${fixturePort}/missing`,
|
||||
`http://127.0.0.1:${fixturePort}/range`,
|
||||
], { out: 'mirror.bin', split: '1', 'max-tries': '1', 'uri-selector': 'adaptive' }]);
|
||||
], { ...ARIA2_LOCAL_FIXTURE_OPTIONS, out: 'mirror.bin', split: '1', 'max-tries': '1', 'uri-selector': 'adaptive' }]);
|
||||
const mirrorStatus = await waitForTerminal(rpcPort, secret, mirrorGid);
|
||||
if (mirrorStatus.status !== 'complete') throw new Error(`adaptive mirror failover failed: ${JSON.stringify(mirrorStatus)}`);
|
||||
|
||||
const checksumGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/range`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'checksum.bin', checksum: `sha-256=${checksum}`, 'check-integrity': 'true',
|
||||
}]);
|
||||
if ((await waitForTerminal(rpcPort, secret, checksumGid)).status !== 'complete') {
|
||||
throw new Error('valid checksum transfer did not complete');
|
||||
}
|
||||
const mismatchGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/range`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'checksum-mismatch.bin', checksum: `sha-256=${'0'.repeat(64)}`, 'check-integrity': 'true',
|
||||
}]);
|
||||
const mismatchStatus = await waitForTerminal(rpcPort, secret, mismatchGid);
|
||||
@@ -423,6 +437,7 @@ try {
|
||||
if (!redirectLocation) throw new Error('redirect preflight returned no Location header');
|
||||
const resolvedRedirect = new URL(redirectLocation, redirectProbe.url);
|
||||
const redirectGid = await rpc(rpcPort, secret, 'aria2.addUri', [[resolvedRedirect.toString()], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'redirect.bin',
|
||||
}]);
|
||||
const redirectStatus = await waitForTerminal(rpcPort, secret, redirectGid);
|
||||
@@ -431,6 +446,7 @@ try {
|
||||
}
|
||||
|
||||
const missingGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/missing`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'missing.bin', 'max-tries': '1',
|
||||
}]);
|
||||
const missingStatus = await waitForTerminal(rpcPort, secret, missingGid);
|
||||
@@ -439,6 +455,7 @@ try {
|
||||
}
|
||||
|
||||
const lowSpeedGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/slow`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'low-speed.bin', 'max-tries': '1', 'lowest-speed-limit': '1M', timeout: '20',
|
||||
}]);
|
||||
const lowSpeedStatus = await waitForTerminal(rpcPort, secret, lowSpeedGid, 25000);
|
||||
@@ -447,6 +464,7 @@ try {
|
||||
}
|
||||
|
||||
const malformedGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/malformed`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'malformed.bin', 'max-tries': '1',
|
||||
}]);
|
||||
if ((await waitForTerminal(rpcPort, secret, malformedGid)).status !== 'error') {
|
||||
@@ -454,6 +472,7 @@ try {
|
||||
}
|
||||
|
||||
const proxyGid = await rpc(rpcPort, secret, 'aria2.addUri', [[`http://127.0.0.1:${fixturePort}/range`], {
|
||||
...ARIA2_LOCAL_FIXTURE_OPTIONS,
|
||||
out: 'proxy.bin', 'all-proxy': `http://127.0.0.1:${unavailableProxyPort}`, 'max-tries': '1',
|
||||
}]);
|
||||
if ((await waitForTerminal(rpcPort, secret, proxyGid)).status !== 'error') {
|
||||
|
||||
@@ -8,6 +8,10 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
ARIA2_DNS_RESOLVER,
|
||||
assertAria2RouteCapabilities,
|
||||
} from './aria2-route-contract.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
@@ -68,6 +72,14 @@ let signalTerminationRequested = false;
|
||||
|
||||
class DaemonExitedError extends Error {}
|
||||
|
||||
function assertFixtureRouteOptions(options, context) {
|
||||
if (options['async-dns'] !== 'true'
|
||||
|| options['dns-resolver'] !== ARIA2_DNS_RESOLVER
|
||||
|| options['network-target-policy'] !== 'none') {
|
||||
throw new Error(`${context} did not retain the local-fixture resolver contract: ${JSON.stringify(options)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function childExited(child) {
|
||||
return child.exitCode !== null || child.signalCode !== null;
|
||||
}
|
||||
@@ -309,6 +321,8 @@ async function startDaemon({ name, rpcPort, listenPort, directory, extraArgs = [
|
||||
'--enable-peer-exchange=false',
|
||||
'--bt-enable-lpd=false',
|
||||
'--async-dns=true',
|
||||
`--dns-resolver=${ARIA2_DNS_RESOLVER}`,
|
||||
'--network-target-policy=none',
|
||||
'--console-log-level=error',
|
||||
'--quiet=true',
|
||||
...(selectedListenPort ? [`--listen-port=${selectedListenPort}`] : []),
|
||||
@@ -330,15 +344,15 @@ async function startDaemon({ name, rpcPort, listenPort, directory, extraArgs = [
|
||||
};
|
||||
activeDaemons.add(daemon);
|
||||
try {
|
||||
await waitFor(`${name} Aria2 RPC`, async () => {
|
||||
const version = await waitFor(`${name} Aria2 RPC`, async () => {
|
||||
if (exit) throw new DaemonExitedError(`${name} exited: ${exit.error?.message || `${exit.code}/${exit.signal}`}`);
|
||||
try {
|
||||
await rpc(selectedRpcPort, secret, 'aria2.getVersion');
|
||||
return true;
|
||||
return await rpc(selectedRpcPort, secret, 'aria2.getVersion');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 10000);
|
||||
assertAria2RouteCapabilities(version);
|
||||
return daemon;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -770,7 +784,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', 'fresh direct Torrent unexpectedly disabled asynchronous DNS');
|
||||
assertFixtureRouteOptions(directOptions, 'fresh direct Torrent');
|
||||
await rpc(client.rpcPort, client.secret, 'aria2.removeDownloadResult', [directGid]);
|
||||
try {
|
||||
await rpc(client.rpcPort, client.secret, 'aria2.removeDownloadResult', [directHandoff.childGid]);
|
||||
@@ -800,7 +814,7 @@ async function main() {
|
||||
'auto-file-renaming': 'false',
|
||||
}]);
|
||||
const probeOptions = await rpc(client.rpcPort, client.secret, 'aria2.getOption', [probeGid]);
|
||||
assert(probeOptions['async-dns'] !== 'false', 'fresh direct magnet probe unexpectedly disabled asynchronous DNS');
|
||||
assertFixtureRouteOptions(probeOptions, 'fresh direct magnet probe');
|
||||
const probeStatus = await waitForTerminal(client, probeGid, 30000);
|
||||
assert(probeStatus.status === 'complete', 'magnet metadata probe did not complete');
|
||||
const savedTorrentPaths = fs.readdirSync(probeDir)
|
||||
@@ -858,7 +872,7 @@ async function main() {
|
||||
}]);
|
||||
const proxiedProbeOptions = await rpc(client.rpcPort, client.secret, 'aria2.getOption', [proxiedProbeGid]);
|
||||
assert(proxiedProbeOptions['all-proxy'] === normalizedProxiedProbeRoute, 'proxied magnet metadata probe did not retain the configured proxy route');
|
||||
assert(proxiedProbeOptions['async-dns'] !== 'false', 'proxied magnet metadata probe unexpectedly forced system DNS resolution');
|
||||
assertFixtureRouteOptions(proxiedProbeOptions, 'proxied magnet metadata probe');
|
||||
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 probes kept asynchronous DNS');
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
resolveOutputRoot,
|
||||
resolveTargetTriple,
|
||||
} from './engine-workspace.js';
|
||||
import { assertAria2RouteSource } from './aria2-route-contract.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
@@ -45,6 +46,13 @@ if (!source) {
|
||||
}
|
||||
|
||||
if (targetLock) {
|
||||
try {
|
||||
assertAria2RouteSource(targetLock.engines?.aria2c, target);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const engine of engines) {
|
||||
const name = `${engine}-${target}${suffix}`;
|
||||
const expected = targetLock.engines?.[engine]?.sha256;
|
||||
@@ -78,6 +86,12 @@ if (targetLock) {
|
||||
console.error(`Payload manifest target mismatch: ${manifest.target}`);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
assertAria2RouteSource(manifest.generatedFrom?.aria2c, target);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
for (const [relative, expected] of Object.entries(manifest.files || {})) {
|
||||
const file = path.join(source, relative);
|
||||
if (!fs.existsSync(file) || sha256(file) !== expected) {
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
resolveOutputRoot,
|
||||
resolveTargetTriple,
|
||||
} from './engine-workspace.js';
|
||||
import {
|
||||
ARIA2_ROUTE_DAEMON_ARGS,
|
||||
assertAria2RouteContract,
|
||||
} from './aria2-route-contract.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -469,6 +473,7 @@ if (canExecuteTarget) {
|
||||
'--quiet',
|
||||
'--console-log-level=error',
|
||||
'--rpc-listen-all=false',
|
||||
...ARIA2_ROUTE_DAEMON_ARGS,
|
||||
], {
|
||||
env: engineEnv('aria2c'),
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
@@ -563,6 +568,7 @@ if (canExecuteTarget) {
|
||||
try {
|
||||
const resp = JSON.parse(result.data);
|
||||
if (resp?.result?.version) {
|
||||
assertAria2RouteContract(resp.result);
|
||||
ok(`aria2 RPC version: ${resp.result.version}`);
|
||||
} else {
|
||||
fail(`aria2 RPC unexpected response: ${result.data}`);
|
||||
|
||||
Reference in New Issue
Block a user