mirror of
https://github.com/SukkaW/Surge.git
synced 2026-01-28 17:41:54 +08:00
Feat: support Telegram Backup IP
This commit is contained in:
@@ -6,6 +6,8 @@ import { once } from 'foxts/once';
|
||||
import { RulesetOutput } from './lib/rules/ruleset';
|
||||
import { $$fetch } from './lib/fetch-retry';
|
||||
import { fastIpVersion } from 'foxts/fast-ip-version';
|
||||
import DNS2 from 'dns2';
|
||||
import { getTelegramBackupIPFromBase64 } from './lib/get-telegram-backup-ip';
|
||||
|
||||
export const getTelegramCIDRPromise = once(async () => {
|
||||
const resp = await $$fetch('https://core.telegram.org/resources/cidr.txt');
|
||||
@@ -28,6 +30,58 @@ export const getTelegramCIDRPromise = once(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const backupIPs = new Set<string>();
|
||||
|
||||
// Backup IP Source 1 (DoH)
|
||||
await Promise.all([
|
||||
DNS2.DOHClient({
|
||||
dns: '8.8.8.8',
|
||||
http: false
|
||||
}),
|
||||
DNS2.DOHClient({
|
||||
dns: '1.0.0.1',
|
||||
http: false
|
||||
})
|
||||
].map(async (client) => {
|
||||
// tapv3.stel.com was for testing server
|
||||
const resp = await client('apv3.stel.com', '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 => i && backupIPs.add(i.ip));
|
||||
}));
|
||||
|
||||
// Backup IP Source 2: Firebase Storage
|
||||
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 => i && backupIPs.add(i.ip));
|
||||
}
|
||||
} catch {
|
||||
// ignore all errors
|
||||
}
|
||||
|
||||
// Backup IP Source 3: Firebase Value Store
|
||||
try {
|
||||
const json = await (await $$fetch('https://firestore.googleapis.com/v1/projects/reserve-5a846/databases/(default)/documents/ipconfig/v3')).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 => i && backupIPs.add(i.ip));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
ipcidr.push(...Array.from(backupIPs).map(i => i + '/32'));
|
||||
|
||||
return { date, ipcidr, ipcidr6 };
|
||||
});
|
||||
|
||||
|
||||
107
Build/lib/get-telegram-backup-ip.ts
Normal file
107
Build/lib/get-telegram-backup-ip.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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';
|
||||
|
||||
const mtptoto_public_rsa = '-----BEGIN RSA PUBLIC KEY-----\n'
|
||||
+ 'MIIBCgKCAQEAyr+18Rex2ohtVy8sroGP\n'
|
||||
+ 'BwXD3DOoKCSpjDqYoXgCqB7ioln4eDCFfOBUlfXUEvM/fnKCpF46VkAftlb4VuPD\n'
|
||||
+ 'eQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd192xRGreuXIaUKmkwlM9JID9WS2jUsTp\n'
|
||||
+ 'zQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c9G5IzzBm+otMS/YKwmR1olzRCyEkyAEj\n'
|
||||
+ 'XWqBI9Ftv5eG8m0VkBzOG655WIYdyV0HfDK/NWcvGqa0w/nriMD6mDjKOryamw0O\n'
|
||||
+ 'P9QuYgMN0C9xMW9y8SmP4h92OAWodTYgY1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWY\n'
|
||||
+ 'xwIDAQAB\n'
|
||||
+ '-----END RSA PUBLIC KEY-----\n';
|
||||
|
||||
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 Buffer.from
|
||||
|
||||
// 3. Decode base64 to Buffer
|
||||
const decoded = Buffer.from(base64, '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 = Buffer.concat([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(decryptedCbc);
|
||||
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(payload));
|
||||
|
||||
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)}`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user