diff --git a/.gitignore b/.gitignore index ee1effb3fb..66f9af5952 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules_bkp .sass-cache coverage/* build/curve25519_compiled.js +build/dns-fallback.json stylesheets/*.css.map /dist .DS_Store diff --git a/app/dns-fallback.ts b/app/dns-fallback.ts new file mode 100644 index 0000000000..6e5e596a59 --- /dev/null +++ b/app/dns-fallback.ts @@ -0,0 +1,33 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { join } from 'path'; +import { readFile } from 'fs/promises'; +import { DNSFallbackSchema } from '../ts/types/DNSFallback'; +import type { DNSFallbackType } from '../ts/types/DNSFallback'; + +let cached: DNSFallbackType | undefined; + +export async function getDNSFallback(): Promise { + if (cached != null) { + return cached; + } + + const configPath = join(__dirname, '..', 'build', 'dns-fallback.json'); + let str: string; + try { + str = await readFile(configPath, 'utf8'); + } catch (error) { + console.error( + 'Warning: build/dns-fallback.json not build, run `yarn generate`' + ); + cached = []; + return cached; + } + + const json = JSON.parse(str); + + const result = DNSFallbackSchema.parse(json); + cached = result; + return result; +} diff --git a/app/main.ts b/app/main.ts index 9463bc972a..f87d8de70c 100644 --- a/app/main.ts +++ b/app/main.ts @@ -36,6 +36,7 @@ import packageJson from '../package.json'; import * as GlobalErrors from './global_errors'; import { setup as setupCrashReports } from './crashReports'; import { setup as setupSpellChecker } from './spell_check'; +import { getDNSFallback } from './dns-fallback'; import { redactAll, addSensitivePath } from '../ts/util/privacy'; import { createSupportUrl } from '../ts/util/createSupportUrl'; import { missingCaseError } from '../ts/util/missingCaseError'; @@ -52,7 +53,6 @@ import { explodePromise } from '../ts/util/explodePromise'; import './startup_config'; -import type { ConfigType } from './config'; import type { RendererConfigType } from '../ts/types/RendererConfig'; import { directoryConfigSchema, @@ -112,6 +112,7 @@ import { HourCyclePreference } from '../ts/types/I18N'; import { DBVersionFromFutureError } from '../ts/sql/migrations'; import type { ParsedSignalRoute } from '../ts/util/signalRoutes'; import { parseSignalRoute } from '../ts/util/signalRoutes'; +import * as dns from '../ts/util/dns'; import { ZoomFactorService } from '../ts/services/ZoomFactorService'; const STICKER_CREATOR_PARTITION = 'sticker-creator'; @@ -1734,6 +1735,8 @@ if (DISABLE_GPU) { // Some APIs can only be used after this event occurs. let ready = false; app.on('ready', async () => { + dns.setFallback(await getDNSFallback()); + const [userDataPath, crashDumpsPath, installPath] = await Promise.all([ realpath(app.getPath('userData')), realpath(app.getPath('crashDumps')), @@ -2449,15 +2452,17 @@ ipc.on('get-config', async event => { updatesUrl: config.get('updatesUrl'), resourcesUrl: config.get('resourcesUrl'), artCreatorUrl: config.get('artCreatorUrl'), - cdnUrl0: config.get('cdn').get('0'), - cdnUrl2: config.get('cdn').get('2'), - cdnUrl3: config.get('cdn').get('3'), + cdnUrl0: config.get('cdn.0'), + cdnUrl2: config.get('cdn.2'), + cdnUrl3: config.get('cdn.3'), certificateAuthority: config.get('certificateAuthority'), environment: !isTestEnvironment(getEnvironment()) && ciMode ? Environment.Production : getEnvironment(), ciMode, + // Should be already computed and cached at this point + dnsFallback: await getDNSFallback(), nodeVersion: process.versions.node, hostname: os.hostname(), osRelease: os.release(), diff --git a/package.json b/package.json index ef30b1c60c..77ba9eea19 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "postinstall": "yarn build:acknowledgments && patch-package && yarn electron:install-app-deps", "postuninstall": "yarn build:acknowledgments", "start": "electron .", - "generate": "npm-run-all build-protobuf build:esbuild sass get-expire-time copy-components", + "generate": "npm-run-all build-protobuf build:esbuild build:dns-fallback sass get-expire-time copy-components", "build-release": "yarn run build", "sign-release": "node ts/updater/generateSignature.js", "notarize": "echo 'No longer necessary'", @@ -75,6 +75,7 @@ "build": "run-s --print-label generate build:esbuild:prod build:release", "build-linux": "yarn generate && yarn build:esbuild:prod && yarn build:release -- --publish=never", "build:acknowledgments": "node scripts/generate-acknowledgments.js", + "build:dns-fallback": "node ts/scripts/generate-dns-fallback.js", "build:dev": "run-s --print-label generate build:esbuild:prod", "build:esbuild": "node scripts/esbuild.js", "build:esbuild:prod": "node scripts/esbuild.js --prod", @@ -501,6 +502,7 @@ "build/available-locales.json", "build/locale-display-names.json", "build/country-display-names.json", + "build/dns-fallback.json", "node_modules/**", "!node_modules/underscore/**", "!node_modules/emoji-datasource/emoji_pretty.json", diff --git a/ts/scripts/generate-dns-fallback.ts b/ts/scripts/generate-dns-fallback.ts new file mode 100644 index 0000000000..dec7023df3 --- /dev/null +++ b/ts/scripts/generate-dns-fallback.ts @@ -0,0 +1,71 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { join } from 'path'; +import { lookup as lookupCb } from 'dns'; +import { writeFile } from 'fs/promises'; +import { promisify } from 'util'; +import type { ResolvedEndpoint } from 'electron'; + +import { isNotNil } from '../util/isNotNil'; + +const lookup = promisify(lookupCb); + +const FALLBACK_DOMAINS = [ + 'chat.signal.org', + 'storage.signal.org', + 'cdsi.signal.org', + 'cdn.signal.org', + 'cdn2.signal.org', + 'cdn3.signal.org', + 'updates2.signal.org', + 'sfu.voip.signal.org', + 'create.signal.art', +]; + +async function main() { + const config = await Promise.all( + FALLBACK_DOMAINS.sort().map(async domain => { + const addresses = await lookup(domain, { all: true }); + + const endpoints = addresses + .map(({ address, family }): ResolvedEndpoint | null => { + if (family === 4) { + return { family: 'ipv4', address }; + } + if (family === 6) { + return { family: 'ipv6', address }; + } + return null; + }) + .filter(isNotNil) + .sort((a, b) => { + if (a.family < b.family) { + return -1; + } + if (a.family > b.family) { + return 1; + } + + if (a.address < b.address) { + return -1; + } + if (a.address > b.address) { + return 1; + } + return 0; + }); + + return { domain, endpoints }; + }) + ); + + const outPath = join(__dirname, '../../build/dns-fallback.json'); + + await writeFile(outPath, `${JSON.stringify(config, null, 2)}\n`); +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/ts/types/DNSFallback.ts b/ts/types/DNSFallback.ts new file mode 100644 index 0000000000..aad55d100a --- /dev/null +++ b/ts/types/DNSFallback.ts @@ -0,0 +1,18 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import z from 'zod'; + +export const DNSFallbackSchema = z.array( + z.object({ + domain: z.string(), + endpoints: z.array( + z.object({ + family: z.enum(['ipv4', 'ipv6']), + address: z.string(), + }) + ), + }) +); + +export type DNSFallbackType = z.infer; diff --git a/ts/types/RendererConfig.ts b/ts/types/RendererConfig.ts index 92c75c39a8..4d188c08de 100644 --- a/ts/types/RendererConfig.ts +++ b/ts/types/RendererConfig.ts @@ -6,6 +6,7 @@ import { z } from 'zod'; import { Environment } from '../environment'; import { themeSettingSchema } from './StorageUIKeys'; import { HourCyclePreferenceSchema } from './I18N'; +import { DNSFallbackSchema } from './DNSFallback'; const environmentSchema = z.nativeEnum(Environment); @@ -40,6 +41,7 @@ export const rendererConfigSchema = z.object({ contentProxyUrl: configRequiredStringSchema, crashDumpsPath: configRequiredStringSchema, ciMode: z.enum(['full', 'benchmark']).or(z.literal(false)), + dnsFallback: DNSFallbackSchema, environment: environmentSchema, homePath: configRequiredStringSchema, hostname: configRequiredStringSchema, diff --git a/ts/util/createHTTPSAgent.ts b/ts/util/createHTTPSAgent.ts index eef8c848fb..986b26f828 100644 --- a/ts/util/createHTTPSAgent.ts +++ b/ts/util/createHTTPSAgent.ts @@ -40,6 +40,7 @@ const HOST_LOG_ALLOWLIST = new Set([ 'cdsi.signal.org', 'cdn.signal.org', 'cdn2.signal.org', + 'cdn3.signal.org', 'create.signal.art', // Staging diff --git a/ts/util/dns.ts b/ts/util/dns.ts index 69e2c76773..83ae342fb7 100644 --- a/ts/util/dns.ts +++ b/ts/util/dns.ts @@ -12,35 +12,16 @@ import type { ResolvedHost, ResolvedEndpoint } from 'electron'; import { strictAssert } from './assert'; import { drop } from './drop'; +import type { DNSFallbackType } from '../types/DNSFallback'; -const FALLBACK_ADDRS: ReadonlyMap< - string, - ReadonlyArray -> = new Map([ - [ - 'cdsi.signal.org', - [ - { family: 'ipv4', address: '40.122.45.194' }, - { family: 'ipv6', address: '2603:1030:7::1' }, - ], - ], - [ - 'chat.signal.org', - [ - { family: 'ipv4', address: '13.248.212.111' }, - { family: 'ipv4', address: '76.223.92.165' }, - { family: 'ipv6', address: '2600:9000:a507:ab6d:4ce3:2f58:25d7:9cbf' }, - { family: 'ipv6', address: '2600:9000:a61f:527c:d5eb:a431:5239:3232' }, - ], - ], - [ - 'sfu.voip.signal.org', - [ - { family: 'ipv4', address: '34.49.5.136' }, - { family: 'ipv6', address: '2600:1901:0:9c39::' }, - ], - ], -]); +const fallbackAddrs = new Map>(); + +export function setFallback(dnsFallback: DNSFallbackType): void { + fallbackAddrs.clear(); + for (const { domain, endpoints } of dnsFallback) { + fallbackAddrs.set(domain, endpoints); + } +} function lookupAll( hostname: string, @@ -80,7 +61,7 @@ function lookupAll( ); } } catch (error) { - const fallback = FALLBACK_ADDRS.get(hostname); + const fallback = fallbackAddrs.get(hostname); if (fallback) { result = { endpoints: fallback.slice() }; } else { diff --git a/ts/windows/main/phase2-dependencies.ts b/ts/windows/main/phase2-dependencies.ts index 0ae3720ffc..33b14fb6fe 100644 --- a/ts/windows/main/phase2-dependencies.ts +++ b/ts/windows/main/phase2-dependencies.ts @@ -12,6 +12,7 @@ import { textsecure } from '../../textsecure'; import * as Attachments from '../attachments'; import { setup } from '../../signal'; import { addSensitivePath } from '../../util/privacy'; +import * as dns from '../../util/dns'; import * as log from '../../logging/log'; import { SignalContext } from '../context'; @@ -72,6 +73,8 @@ if (config.crashDumpsPath) { addSensitivePath(config.crashDumpsPath); } +dns.setFallback(SignalContext.config.dnsFallback); + window.Signal = setup({ Attachments, getRegionCode: () => window.storage.get('regionCode'),