build-telegram-cidr.worker.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. // @ts-check
  2. import { task } from './trace';
  3. import { SHARED_DESCRIPTION } from './constants/description';
  4. import { RulesetOutput } from './lib/rules/ruleset';
  5. import { getTelegramBackupIPFromBase64 } from './lib/get-telegram-backup-ip';
  6. import picocolors from 'picocolors';
  7. import { $$fetch } from './lib/fetch-retry';
  8. import dns from 'node:dns/promises';
  9. import { createReadlineInterfaceFromResponse } from './lib/fetch-text-by-line';
  10. import { fastIpVersion } from 'foxts/fast-ip-version';
  11. import { fastStringArrayJoin } from 'foxts/fast-string-array-join';
  12. import { appendArrayInPlace } from 'foxts/append-array-in-place';
  13. export const buildTelegramCIDR = task(require.main === module, __filename)(async (span) => {
  14. const { timestamp, ipcidr, ipcidr6 } = await span.traceChildAsync('get telegram cidr', async (childSpan) => {
  15. const ipcidr: string[] = [
  16. // Unused secret Telegram backup CIDR, announced by AS62041
  17. '95.161.64.0/20'
  18. ];
  19. const ipcidr6: string[] = [];
  20. const date = await childSpan.traceChildAsync('fetch from official cidr list', async () => {
  21. const resp = await $$fetch('https://core.telegram.org/resources/cidr.txt');
  22. const lastModified = resp.headers.get('last-modified');
  23. for await (const cidr of createReadlineInterfaceFromResponse(resp, true)) {
  24. const v = fastIpVersion(cidr);
  25. if (v === 4) {
  26. ipcidr.push(cidr);
  27. } else if (v === 6) {
  28. ipcidr6.push(cidr);
  29. }
  30. }
  31. return lastModified ? new Date(lastModified) : new Date();
  32. });
  33. // https://github.com/tdlib/td/blob/master/td/telegram/ConfigManager.cpp
  34. const backupIPs = await childSpan.traceChildAsync('fetch backup ip', async (innerSpan) => {
  35. const backupIPs = new Set<string>();
  36. const resolvers = ['8.8.8.8', '1.0.0.1'].map((ip) => {
  37. const resolver = new dns.Resolver();
  38. resolver.setServers([ip]);
  39. return Object.assign(resolver, { server: ip });
  40. });
  41. await innerSpan.traceChildAsync('backup source 1: DNS TXT', () => Promise.all(resolvers.flatMap((resolver) => [
  42. 'apv3.stel.com', // prod
  43. 'tapv3.stel.com' // test
  44. ].map(async (domain) => {
  45. try {
  46. // tapv3.stel.com was for testing server
  47. const resp = await resolver.resolveTxt(domain);
  48. const strings = resp.map(r => fastStringArrayJoin(r, '')); // flatten
  49. if (strings.length !== 2) {
  50. throw new TypeError(`Unexpected TXT record count: ${strings.length}`);
  51. }
  52. const str = strings[0].length > strings[1].length
  53. ? strings[0] + strings[1]
  54. : strings[1] + strings[0];
  55. const ips = getTelegramBackupIPFromBase64(str);
  56. ips.forEach(i => backupIPs.add(i.ip));
  57. console.log('[telegram backup ip]', picocolors.green('DNS TXT'), { domain, ips, server: resolver.server });
  58. } catch (e) {
  59. console.error('[telegram backup ip]', picocolors.red('DNS TXT error'), { domain }, e);
  60. }
  61. }))));
  62. // Backup IP Source 2: Firebase Realtime Database (test server not supported)
  63. await innerSpan.traceChildAsync('backup source 2: Firebase Realtime DB', async () => {
  64. try {
  65. const text = await (await $$fetch('https://reserve-5a846.firebaseio.com/ipconfigv3.json')).json();
  66. if (typeof text === 'string' && text.length === 344) {
  67. const ips = getTelegramBackupIPFromBase64(text);
  68. ips.forEach(i => backupIPs.add(i.ip));
  69. console.log('[telegram backup ip]', picocolors.green('Firebase Realtime DB'), { ips });
  70. }
  71. } catch (e) {
  72. console.error('[telegram backup ip]', picocolors.red('Firebase Realtime DB error'), e);
  73. // ignore all errors
  74. }
  75. });
  76. // Backup IP Source 3: Firebase Value Store (test server not supported)
  77. await innerSpan.traceChildAsync('backup source 3: Firebase Value Store', async () => {
  78. try {
  79. const json = await (await $$fetch('https://firestore.googleapis.com/v1/projects/reserve-5a846/databases/(default)/documents/ipconfig/v3', {
  80. headers: {
  81. Accept: '*/*',
  82. 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
  83. }
  84. })).json();
  85. if (
  86. json && typeof json === 'object'
  87. && 'fields' in json && typeof json.fields === 'object' && json.fields
  88. && 'data' in json.fields && typeof json.fields.data === 'object' && json.fields.data
  89. && 'stringValue' in json.fields.data && typeof json.fields.data.stringValue === 'string' && json.fields.data.stringValue.length === 344
  90. ) {
  91. const ips = getTelegramBackupIPFromBase64(json.fields.data.stringValue);
  92. ips.forEach(i => backupIPs.add(i.ip));
  93. console.log('[telegram backup ip]', picocolors.green('Firebase Value Store'), { ips });
  94. } else {
  95. console.error('[telegram backup ip]', picocolors.red('Firebase Value Store data format invalid'), { json });
  96. }
  97. } catch (e) {
  98. console.error('[telegram backup ip]', picocolors.red('Firebase Value Store error'), e);
  99. }
  100. });
  101. // Backup IP Source 4: Google App Engine
  102. await innerSpan.traceChildAsync('backup source 4: Google App Engine', () => Promise.all([
  103. 'https://dns-telegram.appspot.com',
  104. 'https://dns-telegram.appspot.com/test'
  105. ].map(async (url) => {
  106. try {
  107. const text = await (await $$fetch(url)).text();
  108. if (text.length === 344) {
  109. const ips = getTelegramBackupIPFromBase64(text);
  110. ips.forEach(i => backupIPs.add(i.ip));
  111. console.log('[telegram backup ip]', picocolors.green('Google App Engine'), { url, ips });
  112. }
  113. } catch (e) {
  114. console.error('[telegram backup ip]', picocolors.red('Google App Engine error'), { url }, e);
  115. }
  116. })));
  117. // tcdnb.azureedge.net no longer works
  118. return backupIPs;
  119. });
  120. console.log('[telegram backup ip]', `Found ${backupIPs.size} backup IPs:`, backupIPs);
  121. appendArrayInPlace(ipcidr, Array.from(backupIPs).map(i => i + '/32'));
  122. return { timestamp: date.getTime(), ipcidr, ipcidr6 };
  123. });
  124. if (ipcidr.length + ipcidr6.length === 0) {
  125. throw new Error('Failed to fetch data!');
  126. }
  127. const description = [
  128. ...SHARED_DESCRIPTION,
  129. 'Data from:',
  130. ' - https://core.telegram.org/resources/cidr.txt'
  131. ];
  132. return new RulesetOutput(span, 'telegram', 'ip')
  133. .withTitle('Sukka\'s Ruleset - Telegram IP CIDR')
  134. .withDescription(description)
  135. // .withDate(date) // With extra data source, we no longer use last-modified for file date
  136. .appendDataSource(
  137. 'https://core.telegram.org/resources/cidr.txt (last updated: ' + new Date(timestamp).toISOString() + ')'
  138. )
  139. .bulkAddCIDR4NoResolve(ipcidr)
  140. .bulkAddCIDR6NoResolve(ipcidr6)
  141. .write();
  142. });
  143. export const ___ = '';