import { Buffer } from 'node:buffer'; import { stableHash } from 'stable-hash'; import type { Api as TgApi } from 'telegram'; import type { TelegramBackupEndpoint } from './get-telegram-backup-ip'; import { setBit, getBit } from 'foxts/bitwise'; import { bigint2ip, ip2bigint } from 'fast-cidr-tools'; import { isProbablyIpv6 } from 'foxts/is-probably-ip'; import { fastIpVersion } from 'foxts/fast-ip-version'; export const DC_OPTION_FLAG_IPV6 = 1 << 0; export const DC_OPTION_FLAG_MEDIA_ONLY = 1 << 1; export const DC_OPTION_FLAG_TCPO_ONLY = 1 << 2; export const DC_OPTION_FLAG_CDN = 1 << 3; export const DC_OPTION_FLAG_STATIC = 1 << 4; export const DC_OPTION_FLAG_THIS_PORT_ONLY = 1 << 5; export const DC_OPTION_FLAG_SECRET = 1 << 10; export interface MTProtoDCConfigOption { id: number, ip: string, port: number, flags: number, secret?: string } export interface MTProtoDCConfig { version: 1, date: number, expires: number, this_dc: number, options: MTProtoDCConfigOption[] } export interface MTProtoEndpoint { dcId: number, ip: string, port: number, secret?: Uint8Array } export const TELEGRAM_BOOTSTRAP_ENDPOINTS: readonly MTProtoEndpoint[] = [ { dcId: 1, ip: '149.154.175.50', port: 443 }, { dcId: 1, ip: '2001:b28:f23d:f001::a', port: 443 }, { dcId: 2, ip: '149.154.167.50', port: 443 }, { dcId: 2, ip: '149.154.167.51', port: 443 }, { dcId: 2, ip: '95.161.76.100', port: 443 }, { dcId: 2, ip: '2001:67c:4e8:f002::a', port: 443 }, { dcId: 3, ip: '149.154.175.100', port: 443 }, { dcId: 3, ip: '2001:b28:f23d:f003::a', port: 443 }, { dcId: 4, ip: '149.154.167.91', port: 443 }, { dcId: 4, ip: '2001:67c:4e8:f004::a', port: 443 }, { dcId: 5, ip: '149.154.171.5', port: 443 }, { dcId: 5, ip: '2001:b28:f23f:f005::a', port: 443 } ]; type DcOptionWithFlags = TgApi.DcOption & { flags?: number }; /** * help.getConfig returns IPv6 addresses fully expanded (leading zeros in every * hextet, no "::"), while TELEGRAM_BOOTSTRAP_ENDPOINTS is written in the * canonical RFC 5952 form. Without canonicalizing, mergeEndpoint's string * compare never matches the two spellings of the same address, so every IPv6 * bootstrap gets appended as a duplicate option and the live option never * receives DC_OPTION_FLAG_STATIC. IPv4 has a single spelling and is passed * through untouched. */ function canonicalizeIp(ip: string) { if (!isProbablyIpv6(ip)) return ip; return bigint2ip(ip2bigint(ip, 6), 6, true); } function deriveDcOptionFlags(option: TgApi.DcOption) { let flags = 0; if (option.ipv6) flags = setBit(flags, DC_OPTION_FLAG_IPV6); if (option.mediaOnly) flags = setBit(flags, DC_OPTION_FLAG_MEDIA_ONLY); if (option.tcpoOnly) flags = setBit(flags, DC_OPTION_FLAG_TCPO_ONLY); if (option.cdn) flags = setBit(flags, DC_OPTION_FLAG_CDN); if (option.static) flags = setBit(flags, DC_OPTION_FLAG_STATIC); if (option.thisPortOnly) flags = setBit(flags, DC_OPTION_FLAG_THIS_PORT_ONLY); if (option.secret) flags = setBit(flags, DC_OPTION_FLAG_SECRET); return flags; } export function normalizeTelegramConfig(config: TgApi.Config): MTProtoDCConfig { return { version: 1, date: config.date, expires: config.expires, this_dc: config.thisDc, options: config.dcOptions.map((rawOption) => { const option = rawOption as DcOptionWithFlags; const normalized: MTProtoDCConfigOption = { id: option.id, ip: canonicalizeIp(option.ipAddress), port: option.port, flags: Number.isSafeInteger(option.flags) ? option.flags! : deriveDcOptionFlags(option) }; if (option.secret) { normalized.secret = Buffer.from(option.secret).toString('base64'); } return normalized; }) }; } function fallbackFlags(ip: string, hasSecret: boolean) { let flags = DC_OPTION_FLAG_STATIC; if (ip.includes(':')) flags = setBit(flags, DC_OPTION_FLAG_IPV6); if (hasSecret) flags = setBit(flags, DC_OPTION_FLAG_SECRET); return flags; } function mergeEndpoint(config: MTProtoDCConfig, endpoint: MTProtoEndpoint) { const secret = endpoint.secret ? Buffer.from(endpoint.secret).toString('base64') : undefined; const ip = canonicalizeIp(endpoint.ip); const functionalFlags = setBit(setBit(DC_OPTION_FLAG_MEDIA_ONLY, DC_OPTION_FLAG_TCPO_ONLY), DC_OPTION_FLAG_CDN); let matched = false; for (let i = 0, len = config.options.length; i < len; i++) { const option = config.options[i]; if ( option.id !== endpoint.dcId || option.ip !== ip || option.port !== endpoint.port || getBit(option.flags, functionalFlags) || option.secret !== secret ) { continue; } option.flags = setBit(option.flags, fallbackFlags(ip, secret !== undefined)); matched = true; } if (!matched) { config.options.push({ id: endpoint.dcId, ip, port: endpoint.port, flags: fallbackFlags(ip, secret !== undefined), ...(!(secret === undefined) && { secret }) }); } return !matched; } /** * The output order is otherwise a mix of whatever help.getConfig returned and * the order the concurrent backup-endpoint lookups happened to resolve in, so * two builds over identical upstream data can produce a spurious diff. Sort on * the full option identity to keep the committed JSON reviewable. */ function sortOptions(config: MTProtoDCConfig) { config.options.sort((a, b) => { if (a.id !== b.id) return a.id - b.id; // IPv4 before IPv6, each ordered numerically rather than lexically so // 149.154.167.51 sorts before 149.154.167.222. const aVersion = fastIpVersion(a.ip); const bVersion = fastIpVersion(b.ip); if (aVersion !== bVersion) return aVersion - bVersion; if (aVersion === 0 || bVersion === 0) return 0; // how is invalid ip even here? just leave them in their original order const aIp = ip2bigint(a.ip, aVersion); const bIp = ip2bigint(b.ip, bVersion); if (aIp !== bIp) return aIp < bIp ? -1 : 1; if (a.port !== b.port) return a.port - b.port; if (a.flags !== b.flags) return a.flags - b.flags; // Secrets are base64, so a plain code-unit compare is stable and locale-free. const aSecret = a.secret ?? ''; const bSecret = b.secret ?? ''; if (aSecret === bSecret) return 0; return aSecret < bSecret ? -1 : 1; }); } function deduplicateOptions(config: MTProtoDCConfig) { const previousCount = config.options.length; const seen = new Set(); config.options = config.options.filter((option) => { const key = stableHash(option); if (seen.has(key)) return false; seen.add(key); return true; }); return previousCount - config.options.length; } export function mergeFallbackEndpoints( config: MTProtoDCConfig, backupEndpoints: readonly TelegramBackupEndpoint[] ) { // mergeEndpoint compares IP strings, so both sides must already be canonical. // normalizeTelegramConfig canonicalizes the help.getConfig response, but the // merge cannot assume its input came from there. for (let i = 0, len = config.options.length; i < len; i++) { const option = config.options[i]; option.ip = canonicalizeIp(option.ip); } let backupAdded = 0; for (let i = 0, len = backupEndpoints.length; i < len; i++) { const endpoint = backupEndpoints[i]; if (mergeEndpoint(config, endpoint)) backupAdded++; } let bootstrapAdded = 0; for (let i = 0, len = TELEGRAM_BOOTSTRAP_ENDPOINTS.length; i < len; i++) { const endpoint = TELEGRAM_BOOTSTRAP_ENDPOINTS[i]; if (mergeEndpoint(config, endpoint)) bootstrapAdded++; } const duplicatesRemoved = deduplicateOptions(config); sortOptions(config); return { backupAdded, bootstrapAdded, duplicatesRemoved }; }