// https://reserve-5a846.firebaseio.com/ipconfigv3.json // apv3.stel.com tapv3.stel.com import { Buffer } from 'node:buffer'; import crypto from 'node:crypto'; import { Api, extensions as TgExtensions } from 'telegram'; import { bigint2ip } from 'fast-cidr-tools'; import { base64ToUint8Array, concatUint8Arrays } from 'foxts/uint8array-utils'; import Worktank from 'worktank'; import { wait } from 'foxts/wait'; const mtptoto_public_rsa = `-----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEAyr+18Rex2ohtVy8sroGP BwXD3DOoKCSpjDqYoXgCqB7ioln4eDCFfOBUlfXUEvM/fnKCpF46VkAftlb4VuPD eQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd192xRGreuXIaUKmkwlM9JID9WS2jUsTp zQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c9G5IzzBm+otMS/YKwmR1olzRCyEkyAEj XWqBI9Ftv5eG8m0VkBzOG655WIYdyV0HfDK/NWcvGqa0w/nriMD6mDjKOryamw0O P9QuYgMN0C9xMW9y8SmP4h92OAWodTYgY1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWY xwIDAQAB -----END RSA PUBLIC KEY----- `; export function getTelegramBackupIPFromBase64(base64: string) { // 1. Check base64 size if (base64.length !== 344) { throw new TypeError('Invalid base64 length'); } // 2. Filter to base64 and check length // Not needed with base64ToUint8Array, it has built-in base64-able checking // 3. Decode base64 to Buffer const decoded = base64ToUint8Array(base64); if (decoded.length !== 256) { throw new TypeError('Decoded buffer length is not 344 bytes, received ' + decoded.length); } // 4. RSA decrypt (public key, "decrypt signature" - usually means "verify and extract") // In Node.js, publicDecrypt is used for signature verification (Note that no padding is needed) const publicKey = crypto.createPublicKey(mtptoto_public_rsa); const decrypted = crypto.publicDecrypt( { key: publicKey, padding: crypto.constants.RSA_NO_PADDING }, decoded ); // 5. Extract AES key/iv and encrypted payload const key = decrypted.subarray(0, 32); const iv = decrypted.subarray(16, 32); const dataCbc = decrypted.subarray(32); // 224 bytes if (dataCbc.length !== 224) { throw new Error(`Invalid AES payload length: ${dataCbc.length}`); } // 6. AES-CBC decrypt const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); decipher.setAutoPadding(false); const decryptedCbc = concatUint8Arrays([decipher.update(dataCbc), decipher.final()]); if (decryptedCbc.length !== 224) { throw new Error(`Decrypted AES payload length is not 224 bytes, received ${decryptedCbc.length}`); } // 7. SHA256 check const currentHash = crypto .createHash('sha256') .update(decryptedCbc.subarray(0, 208)) .digest() .subarray(0, 16); const expectedHash = decryptedCbc.subarray(208, 224); // check if hash matches if (!currentHash.equals(expectedHash)) { throw new Error('SHA256 hash mismatch'); } const parser = new TgExtensions.BinaryReader(Buffer.from(decryptedCbc.buffer, decryptedCbc.byteOffset, decryptedCbc.byteLength)); const len = parser.readInt(); if (len < 8 || len > 208) throw new Error(`Invalid TL data length: ${len}`); const constructorId = parser.readInt(); if (constructorId !== Api.help.ConfigSimple.CONSTRUCTOR_ID) { throw new Error(`Invalid constructor ID: ${constructorId.toString(16)}`); } const payload = decryptedCbc.subarray(8, len); const configSimple = Api.help.ConfigSimple.fromReader(new TgExtensions.BinaryReader(Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength))); return configSimple.rules.flatMap(rule => rule.ips.map(ip => { switch (ip.CONSTRUCTOR_ID) { case Api.IpPort.CONSTRUCTOR_ID: case Api.IpPortSecret.CONSTRUCTOR_ID: return { ip: bigint2ip( ip.ipv4 > 0 ? BigInt(ip.ipv4) : (2n ** 32n) + BigInt(ip.ipv4), 4 ), port: ip.port }; default: throw new TypeError(`Unknown IP type: 0x${ip.CONSTRUCTOR_ID.toString(16)}`); } })); } const pool = new Worktank({ pool: { name: 'get-telegram-backup-ips', size: 1 // The number of workers to keep in the pool, if more workers are needed they will be spawned up to this limit }, worker: { autoAbort: 10000, autoTerminate: 30000, // The interval of milliseconds at which to check if the pool can be automatically terminated, to free up resources, workers will be spawned up again if needed autoInstantiate: true, methods: { // eslint-disable-next-line object-shorthand -- workertank getTelegramBackupIPs: async function (__filename: string): Promise<{ timestamp: number, ipcidr: string[], ipcidr6: string[] }> { // TODO: createRequire is a temporary workaround for https://github.com/nodejs/node/issues/51956 const { default: module } = await import('node:module'); const __require = module.createRequire(__filename); const picocolors = __require('picocolors') as typeof import('picocolors'); const { fetch } = __require('./fetch-retry') as typeof import('./fetch-retry'); const DNS2 = __require('dns2') as typeof import('dns2'); const { createReadlineInterfaceFromResponse } = __require('./fetch-text-by-line') as typeof import('./fetch-text-by-line'); const { getTelegramBackupIPFromBase64 } = __require('./get-telegram-backup-ip') as typeof import('./get-telegram-backup-ip'); const { fastIpVersion } = __require('foxts/fast-ip-version') as typeof import('foxts/fast-ip-version'); const resp = await fetch('https://core.telegram.org/resources/cidr.txt'); const lastModified = resp.headers.get('last-modified'); const date = lastModified ? new Date(lastModified) : new Date(); const ipcidr: string[] = [ // Unused secret Telegram backup CIDR, announced by AS62041 '95.161.64.0/20' ]; const ipcidr6: string[] = []; for await (const cidr of createReadlineInterfaceFromResponse(resp, true)) { const v = fastIpVersion(cidr); if (v === 4) { ipcidr.push(cidr); } else if (v === 6) { ipcidr6.push(cidr); } } const backupIPs = new Set(); // https://github.com/tdlib/td/blob/master/td/telegram/ConfigManager.cpp // Backup IP Source 1 (DoH) await Promise.all([ DNS2.DOHClient({ dns: 'https://8.8.4.4/dns-query?dns={query}' }), DNS2.DOHClient({ dns: 'https://1.0.0.1/dns-query?dns={query}' }) ].flatMap( (client) => [ 'apv3.stel.com', // prod 'tapv3.stel.com' // test ].map(async (domain) => { try { // tapv3.stel.com was for testing server const resp = await client(domain, 'TXT'); const strings = resp.answers.map(i => i.data); const str = strings[0]!.length > strings[1]!.length ? strings[0]! + strings[1]! : strings[1]! + strings[0]!; const ips = getTelegramBackupIPFromBase64(str); ips.forEach(i => backupIPs.add(i.ip)); console.log('[telegram backup ip]', picocolors.green('DoH TXT'), { domain, ips }); } catch (e) { console.error('[telegram backup ip]', picocolors.red('DoH TXT error'), { domain }, e); } }) )); // Backup IP Source 2: Firebase Realtime Database (test server not supported) try { const text = await (await fetch('https://reserve-5a846.firebaseio.com/ipconfigv3.json')).json(); if (typeof text === 'string' && text.length === 344) { const ips = getTelegramBackupIPFromBase64(text); ips.forEach(i => backupIPs.add(i.ip)); console.log('[telegram backup ip]', picocolors.green('Firebase Realtime DB'), { ips }); } } catch (e) { console.error('[telegram backup ip]', picocolors.red('Firebase Realtime DB error'), e); // ignore all errors } // Backup IP Source 3: Firebase Value Store (test server not supported) try { const json = await (await fetch('https://firestore.googleapis.com/v1/projects/reserve-5a846/databases/(default)/documents/ipconfig/v3', { headers: { Accept: '*/*', Origin: undefined // Without this line, Google API will return "Bad request: Origin doesn't match Host for XD3.". Probably have something to do with sqlite cache store } })).json(); // const json = await resp.json(); if ( json && typeof json === 'object' && 'fields' in json && typeof json.fields === 'object' && json.fields && 'data' in json.fields && typeof json.fields.data === 'object' && json.fields.data && 'stringValue' in json.fields.data && typeof json.fields.data.stringValue === 'string' && json.fields.data.stringValue.length === 344 ) { const ips = getTelegramBackupIPFromBase64(json.fields.data.stringValue); ips.forEach(i => backupIPs.add(i.ip)); console.log('[telegram backup ip]', picocolors.green('Firebase Value Store'), { ips }); } else { console.error('[telegram backup ip]', picocolors.red('Firebase Value Store data format invalid'), { json }); } } catch (e) { console.error('[telegram backup ip]', picocolors.red('Firebase Value Store error'), e); } // Backup IP Source 4: Google App Engine await Promise.all([ 'https://dns-telegram.appspot.com', 'https://dns-telegram.appspot.com/test' ].map(async (url) => { try { const text = await (await fetch(url)).text(); if (text.length === 344) { const ips = getTelegramBackupIPFromBase64(text); ips.forEach(i => backupIPs.add(i.ip)); console.log('[telegram backup ip]', picocolors.green('Google App Engine'), { url, ips }); } } catch (e) { console.error('[telegram backup ip]', picocolors.red('Google App Engine error'), { url }, e); } })); // tcdnb.azureedge.net no longer works console.log('[telegram backup ip]', `Found ${backupIPs.size} backup IPs:`, backupIPs); ipcidr.push(...Array.from(backupIPs).map(i => i + '/32')); return { timestamp: date.getTime(), ipcidr, ipcidr6 }; } } } }); export const getTelegramCIDRPromise = wait(0).then(() => pool.exec( 'getTelegramBackupIPs', [__filename] )).finally(() => pool.terminate());