get-telegram-backup-ip.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. // https://reserve-5a846.firebaseio.com/ipconfigv3.json
  2. // apv3.stel.com tapv3.stel.com
  3. import { Buffer } from 'node:buffer';
  4. import crypto from 'node:crypto';
  5. import { Api, extensions as TgExtensions } from 'telegram';
  6. import { bigint2ip } from 'fast-cidr-tools';
  7. import { base64ToUint8Array, concatUint8Arrays } from 'foxts/uint8array-utils';
  8. import Worktank from 'worktank';
  9. import { wait } from 'foxts/wait';
  10. import { once } from 'foxts/once';
  11. const mtptoto_public_rsa = `-----BEGIN RSA PUBLIC KEY-----
  12. MIIBCgKCAQEAyr+18Rex2ohtVy8sroGP
  13. BwXD3DOoKCSpjDqYoXgCqB7ioln4eDCFfOBUlfXUEvM/fnKCpF46VkAftlb4VuPD
  14. eQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd192xRGreuXIaUKmkwlM9JID9WS2jUsTp
  15. zQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c9G5IzzBm+otMS/YKwmR1olzRCyEkyAEj
  16. XWqBI9Ftv5eG8m0VkBzOG655WIYdyV0HfDK/NWcvGqa0w/nriMD6mDjKOryamw0O
  17. P9QuYgMN0C9xMW9y8SmP4h92OAWodTYgY1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWY
  18. xwIDAQAB
  19. -----END RSA PUBLIC KEY-----
  20. `;
  21. export function getTelegramBackupIPFromBase64(base64: string) {
  22. // 1. Check base64 size
  23. if (base64.length !== 344) {
  24. throw new TypeError('Invalid base64 length');
  25. }
  26. // 2. Filter to base64 and check length
  27. // Not needed with base64ToUint8Array, it has built-in base64-able checking
  28. // 3. Decode base64 to Buffer
  29. const decoded = base64ToUint8Array(base64);
  30. if (decoded.length !== 256) {
  31. throw new TypeError('Decoded buffer length is not 344 bytes, received ' + decoded.length);
  32. }
  33. // 4. RSA decrypt (public key, "decrypt signature" - usually means "verify and extract")
  34. // In Node.js, publicDecrypt is used for signature verification (Note that no padding is needed)
  35. const publicKey = crypto.createPublicKey(mtptoto_public_rsa);
  36. const decrypted = crypto.publicDecrypt(
  37. {
  38. key: publicKey,
  39. padding: crypto.constants.RSA_NO_PADDING
  40. },
  41. decoded
  42. );
  43. // 5. Extract AES key/iv and encrypted payload
  44. const key = decrypted.subarray(0, 32);
  45. const iv = decrypted.subarray(16, 32);
  46. const dataCbc = decrypted.subarray(32); // 224 bytes
  47. if (dataCbc.length !== 224) {
  48. throw new Error(`Invalid AES payload length: ${dataCbc.length}`);
  49. }
  50. // 6. AES-CBC decrypt
  51. const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
  52. decipher.setAutoPadding(false);
  53. const decryptedCbc = concatUint8Arrays([decipher.update(dataCbc), decipher.final()]);
  54. if (decryptedCbc.length !== 224) {
  55. throw new Error(`Decrypted AES payload length is not 224 bytes, received ${decryptedCbc.length}`);
  56. }
  57. // 7. SHA256 check
  58. const currentHash = crypto
  59. .createHash('sha256')
  60. .update(decryptedCbc.subarray(0, 208))
  61. .digest()
  62. .subarray(0, 16);
  63. const expectedHash = decryptedCbc.subarray(208, 224);
  64. // check if hash matches
  65. if (!currentHash.equals(expectedHash)) {
  66. throw new Error('SHA256 hash mismatch');
  67. }
  68. const parser = new TgExtensions.BinaryReader(Buffer.from(decryptedCbc.buffer, decryptedCbc.byteOffset, decryptedCbc.byteLength));
  69. const len = parser.readInt();
  70. if (len < 8 || len > 208) throw new Error(`Invalid TL data length: ${len}`);
  71. const constructorId = parser.readInt();
  72. if (constructorId !== Api.help.ConfigSimple.CONSTRUCTOR_ID) {
  73. throw new Error(`Invalid constructor ID: ${constructorId.toString(16)}`);
  74. }
  75. const payload = decryptedCbc.subarray(8, len);
  76. const configSimple = Api.help.ConfigSimple.fromReader(new TgExtensions.BinaryReader(Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength)));
  77. return configSimple.rules.flatMap(rule => rule.ips.map(ip => {
  78. switch (ip.CONSTRUCTOR_ID) {
  79. case Api.IpPort.CONSTRUCTOR_ID:
  80. case Api.IpPortSecret.CONSTRUCTOR_ID:
  81. return {
  82. ip: bigint2ip(
  83. ip.ipv4 > 0
  84. ? BigInt(ip.ipv4)
  85. : (2n ** 32n) + BigInt(ip.ipv4),
  86. 4
  87. ),
  88. port: ip.port
  89. };
  90. default:
  91. throw new TypeError(`Unknown IP type: 0x${ip.CONSTRUCTOR_ID.toString(16)}`);
  92. }
  93. }));
  94. }
  95. const pool = new Worktank({
  96. pool: {
  97. name: 'get-telegram-backup-ips',
  98. size: 1 // The number of workers to keep in the pool, if more workers are needed they will be spawned up to this limit
  99. },
  100. worker: {
  101. autoAbort: 10000,
  102. 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
  103. autoInstantiate: true,
  104. methods: {
  105. // eslint-disable-next-line object-shorthand -- workertank
  106. getTelegramBackupIPs: async function (__filename: string): Promise<{ timestamp: number, ipcidr: string[], ipcidr6: string[] }> {
  107. // TODO: createRequire is a temporary workaround for https://github.com/nodejs/node/issues/51956
  108. const { default: module } = await import('node:module');
  109. const __require = module.createRequire(__filename);
  110. const picocolors = __require('picocolors') as typeof import('picocolors');
  111. const { fetch } = __require('./fetch-retry') as typeof import('./fetch-retry');
  112. const DNS2 = __require('dns2') as typeof import('dns2');
  113. const { createReadlineInterfaceFromResponse } = __require('./fetch-text-by-line') as typeof import('./fetch-text-by-line');
  114. const { getTelegramBackupIPFromBase64 } = __require('./get-telegram-backup-ip') as typeof import('./get-telegram-backup-ip');
  115. const { fastIpVersion } = __require('foxts/fast-ip-version') as typeof import('foxts/fast-ip-version');
  116. const resp = await fetch('https://core.telegram.org/resources/cidr.txt');
  117. const lastModified = resp.headers.get('last-modified');
  118. const date = lastModified ? new Date(lastModified) : new Date();
  119. const ipcidr: string[] = [
  120. // Unused secret Telegram backup CIDR, announced by AS62041
  121. '95.161.64.0/20'
  122. ];
  123. const ipcidr6: string[] = [];
  124. for await (const cidr of createReadlineInterfaceFromResponse(resp, true)) {
  125. const v = fastIpVersion(cidr);
  126. if (v === 4) {
  127. ipcidr.push(cidr);
  128. } else if (v === 6) {
  129. ipcidr6.push(cidr);
  130. }
  131. }
  132. const backupIPs = new Set<string>();
  133. // https://github.com/tdlib/td/blob/master/td/telegram/ConfigManager.cpp
  134. // Backup IP Source 1 (DoH)
  135. await Promise.all([
  136. DNS2.DOHClient({ dns: 'https://8.8.4.4/dns-query?dns={query}' }),
  137. DNS2.DOHClient({ dns: 'https://1.0.0.1/dns-query?dns={query}' })
  138. ].flatMap(
  139. (client) => [
  140. 'apv3.stel.com', // prod
  141. 'tapv3.stel.com' // test
  142. ].map(async (domain) => {
  143. try {
  144. // tapv3.stel.com was for testing server
  145. const resp = await client(domain, 'TXT');
  146. const strings = resp.answers.map(i => i.data);
  147. const str = strings[0]!.length > strings[1]!.length
  148. ? strings[0]! + strings[1]!
  149. : strings[1]! + strings[0]!;
  150. const ips = getTelegramBackupIPFromBase64(str);
  151. ips.forEach(i => backupIPs.add(i.ip));
  152. console.log('[telegram backup ip]', picocolors.green('DoH TXT'), { domain, ips });
  153. } catch (e) {
  154. console.error('[telegram backup ip]', picocolors.red('DoH TXT error'), { domain }, e);
  155. }
  156. })
  157. ));
  158. // Backup IP Source 2: Firebase Realtime Database (test server not supported)
  159. try {
  160. const text = await (await fetch('https://reserve-5a846.firebaseio.com/ipconfigv3.json')).json();
  161. if (typeof text === 'string' && text.length === 344) {
  162. const ips = getTelegramBackupIPFromBase64(text);
  163. ips.forEach(i => backupIPs.add(i.ip));
  164. console.log('[telegram backup ip]', picocolors.green('Firebase Realtime DB'), { ips });
  165. }
  166. } catch (e) {
  167. console.error('[telegram backup ip]', picocolors.red('Firebase Realtime DB error'), e);
  168. // ignore all errors
  169. }
  170. // Backup IP Source 3: Firebase Value Store (test server not supported)
  171. try {
  172. const json = await (await fetch('https://firestore.googleapis.com/v1/projects/reserve-5a846/databases/(default)/documents/ipconfig/v3', {
  173. headers: {
  174. Accept: '*/*',
  175. 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
  176. }
  177. })).json();
  178. // const json = await resp.json();
  179. if (
  180. json && typeof json === 'object'
  181. && 'fields' in json && typeof json.fields === 'object' && json.fields
  182. && 'data' in json.fields && typeof json.fields.data === 'object' && json.fields.data
  183. && 'stringValue' in json.fields.data && typeof json.fields.data.stringValue === 'string' && json.fields.data.stringValue.length === 344
  184. ) {
  185. const ips = getTelegramBackupIPFromBase64(json.fields.data.stringValue);
  186. ips.forEach(i => backupIPs.add(i.ip));
  187. console.log('[telegram backup ip]', picocolors.green('Firebase Value Store'), { ips });
  188. } else {
  189. console.error('[telegram backup ip]', picocolors.red('Firebase Value Store data format invalid'), { json });
  190. }
  191. } catch (e) {
  192. console.error('[telegram backup ip]', picocolors.red('Firebase Value Store error'), e);
  193. }
  194. // Backup IP Source 4: Google App Engine
  195. await Promise.all([
  196. 'https://dns-telegram.appspot.com',
  197. 'https://dns-telegram.appspot.com/test'
  198. ].map(async (url) => {
  199. try {
  200. const text = await (await fetch(url)).text();
  201. if (text.length === 344) {
  202. const ips = getTelegramBackupIPFromBase64(text);
  203. ips.forEach(i => backupIPs.add(i.ip));
  204. console.log('[telegram backup ip]', picocolors.green('Google App Engine'), { url, ips });
  205. }
  206. } catch (e) {
  207. console.error('[telegram backup ip]', picocolors.red('Google App Engine error'), { url }, e);
  208. }
  209. }));
  210. // tcdnb.azureedge.net no longer works
  211. console.log('[telegram backup ip]', `Found ${backupIPs.size} backup IPs:`, backupIPs);
  212. ipcidr.push(...Array.from(backupIPs).map(i => i + '/32'));
  213. return { timestamp: date.getTime(), ipcidr, ipcidr6 };
  214. }
  215. }
  216. }
  217. });
  218. export const getTelegramCIDRPromise = once(() => wait(0).then(() => pool.exec(
  219. 'getTelegramBackupIPs',
  220. [__filename]
  221. )).finally(() => pool.terminate()), false);