// Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { LookupOneOptions, LookupAllOptions, LookupAddress } from 'dns'; import { lookup as nodeLookup } from 'dns'; import { ipcRenderer, net } from 'electron'; import type { ResolvedHost } from 'electron'; import { strictAssert } from './assert'; import { drop } from './drop'; const HOST_ALLOWLIST = new Set([ // Production 'chat.signal.org', 'storage.signal.org', 'cdsi.signal.org', 'cdn.signal.org', 'cdn2.signal.org', 'create.signal.art', // Staging 'chat.staging.signal.org', 'storage-staging.signal.org', 'cdsi.staging.signal.org', 'cdn-staging.signal.org', 'cdn2-staging.signal.org', 'create.staging.signal.art', // Common 'updates2.signal.org', 'sfu.voip.signal.org', ]); function lookupAll( hostname: string, opts: LookupOneOptions | LookupAllOptions, callback: ( err: NodeJS.ErrnoException | null, addresses: string | Array, family?: number ) => void ): void { if (!HOST_ALLOWLIST.has(hostname)) { nodeLookup(hostname, opts, callback); return; } // Node.js support various signatures, but we only support one. strictAssert(typeof opts === 'object', 'missing options'); strictAssert(typeof callback === 'function', 'missing callback'); async function run() { let result: ResolvedHost; try { let queryType: 'A' | 'AAAA' | undefined; if (opts.family === 4) { queryType = 'A'; } else if (opts.family === 6) { queryType = 'AAAA'; } if (net) { // Main process result = await net.resolveHost(hostname, { queryType, }); } else { // Renderer result = await ipcRenderer.invoke( 'net.resolveHost', hostname, queryType ); } const addresses = result.endpoints.map(({ address, family }) => { let numericFamily = -1; if (family === 'ipv4') { numericFamily = 4; } else if (family === 'ipv6') { numericFamily = 6; } return { address, family: numericFamily, }; }); if (!opts.all) { const random = addresses.at( Math.floor(Math.random() * addresses.length) ); if (random === undefined) { callback( new Error(`Hostname: ${hostname} cannot be resolved`), '', -1 ); return; } callback(null, random.address, random.family); return; } callback(null, addresses); } catch (error) { callback(error, []); } } drop(run()); } export function interleaveAddresses( addresses: ReadonlyArray ): Array { const firstAddr = addresses.find( ({ family }) => family === 4 || family === 6 ); if (!firstAddr) { throw new Error('interleaveAddresses: no addresses to interleave'); } const v4 = addresses.filter(({ family }) => family === 4); const v6 = addresses.filter(({ family }) => family === 6); // Interleave addresses for Happy Eyeballs, but keep the first address // type from the DNS response first in the list. const interleaved = new Array(); while (v4.length !== 0 || v6.length !== 0) { const v4Entry = v4.pop(); const v6Entry = v6.pop(); if (firstAddr.family === 4) { if (v4Entry !== undefined) { interleaved.push(v4Entry); } if (v6Entry !== undefined) { interleaved.push(v6Entry); } } else { if (v6Entry !== undefined) { interleaved.push(v6Entry); } if (v4Entry !== undefined) { interleaved.push(v4Entry); } } } return interleaved; } // Note: `nodeLookup` has a complicated type due to compatibility requirements. export const electronLookup = lookupAll as typeof nodeLookup;