get-telegram-backup-ip.ts 10 KB

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