get-telegram-backup-ip.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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': fetch } = __require('./fetch-retry') as typeof import('./fetch-retry');
  112. const dns = __require('node:dns/promises') as typeof import('node:dns/promises');
  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 { fastStringArrayJoin } = __require('foxts/fast-string-array-join') as typeof import('foxts/fast-string-array-join');
  117. const resp = await fetch('https://core.telegram.org/resources/cidr.txt');
  118. const lastModified = resp.headers.get('last-modified');
  119. const date = lastModified ? new Date(lastModified) : new Date();
  120. const ipcidr: string[] = [
  121. // Unused secret Telegram backup CIDR, announced by AS62041
  122. '95.161.64.0/20'
  123. ];
  124. const ipcidr6: string[] = [];
  125. for await (const cidr of createReadlineInterfaceFromResponse(resp, true)) {
  126. const v = fastIpVersion(cidr);
  127. if (v === 4) {
  128. ipcidr.push(cidr);
  129. } else if (v === 6) {
  130. ipcidr6.push(cidr);
  131. }
  132. }
  133. const backupIPs = new Set<string>();
  134. // https://github.com/tdlib/td/blob/master/td/telegram/ConfigManager.cpp
  135. const resolvers = ['8.8.8.8', '1.0.0.1'].map((ip) => {
  136. const resolver = new dns.Resolver();
  137. resolver.setServers([ip]);
  138. return Object.assign(resolver, { server: ip });
  139. });
  140. // Backup IP Source 1 (DNS)
  141. await Promise.all(resolvers.flatMap((resolver) => [
  142. 'apv3.stel.com', // prod
  143. 'tapv3.stel.com' // test
  144. ].map(async (domain) => {
  145. try {
  146. // tapv3.stel.com was for testing server
  147. const resp = await resolver.resolveTxt(domain);
  148. const strings = resp.map(r => fastStringArrayJoin(r, '')); // flatten
  149. if (strings.length !== 2) {
  150. throw new TypeError(`Unexpected TXT record count: ${strings.length}`);
  151. }
  152. const str = strings[0].length > strings[1].length
  153. ? strings[0] + strings[1]
  154. : strings[1] + strings[0];
  155. const ips = getTelegramBackupIPFromBase64(str);
  156. ips.forEach(i => backupIPs.add(i.ip));
  157. console.log('[telegram backup ip]', picocolors.green('DNS TXT'), { domain, ips, server: resolver.server });
  158. } catch (e) {
  159. console.error('[telegram backup ip]', picocolors.red('DNS TXT error'), { domain }, e);
  160. }
  161. })));
  162. // Backup IP Source 2: Firebase Realtime Database (test server not supported)
  163. try {
  164. const text = await (await fetch('https://reserve-5a846.firebaseio.com/ipconfigv3.json')).json();
  165. if (typeof text === 'string' && text.length === 344) {
  166. const ips = getTelegramBackupIPFromBase64(text);
  167. ips.forEach(i => backupIPs.add(i.ip));
  168. console.log('[telegram backup ip]', picocolors.green('Firebase Realtime DB'), { ips });
  169. }
  170. } catch (e) {
  171. console.error('[telegram backup ip]', picocolors.red('Firebase Realtime DB error'), e);
  172. // ignore all errors
  173. }
  174. // Backup IP Source 3: Firebase Value Store (test server not supported)
  175. try {
  176. const json = await (await fetch('https://firestore.googleapis.com/v1/projects/reserve-5a846/databases/(default)/documents/ipconfig/v3', {
  177. headers: {
  178. Accept: '*/*',
  179. 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
  180. }
  181. })).json();
  182. // const json = await resp.json();
  183. if (
  184. json && typeof json === 'object'
  185. && 'fields' in json && typeof json.fields === 'object' && json.fields
  186. && 'data' in json.fields && typeof json.fields.data === 'object' && json.fields.data
  187. && 'stringValue' in json.fields.data && typeof json.fields.data.stringValue === 'string' && json.fields.data.stringValue.length === 344
  188. ) {
  189. const ips = getTelegramBackupIPFromBase64(json.fields.data.stringValue);
  190. ips.forEach(i => backupIPs.add(i.ip));
  191. console.log('[telegram backup ip]', picocolors.green('Firebase Value Store'), { ips });
  192. } else {
  193. console.error('[telegram backup ip]', picocolors.red('Firebase Value Store data format invalid'), { json });
  194. }
  195. } catch (e) {
  196. console.error('[telegram backup ip]', picocolors.red('Firebase Value Store error'), e);
  197. }
  198. // Backup IP Source 4: Google App Engine
  199. await Promise.all([
  200. 'https://dns-telegram.appspot.com',
  201. 'https://dns-telegram.appspot.com/test'
  202. ].map(async (url) => {
  203. try {
  204. const text = await (await fetch(url)).text();
  205. if (text.length === 344) {
  206. const ips = getTelegramBackupIPFromBase64(text);
  207. ips.forEach(i => backupIPs.add(i.ip));
  208. console.log('[telegram backup ip]', picocolors.green('Google App Engine'), { url, ips });
  209. }
  210. } catch (e) {
  211. console.error('[telegram backup ip]', picocolors.red('Google App Engine error'), { url }, e);
  212. }
  213. }));
  214. // tcdnb.azureedge.net no longer works
  215. console.log('[telegram backup ip]', `Found ${backupIPs.size} backup IPs:`, backupIPs);
  216. ipcidr.push(...Array.from(backupIPs).map(i => i + '/32'));
  217. return { timestamp: date.getTime(), ipcidr, ipcidr6 };
  218. }
  219. }
  220. }
  221. });
  222. export const getTelegramCIDRPromise = once(() => wait(0).then(() => pool.exec(
  223. 'getTelegramBackupIPs',
  224. [__filename]
  225. )).finally(() => pool.terminate()), false);