From 48b948c8eb204a0bbe1201d6ea0132e4fcf107d3 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 14 Aug 2026 20:21:58 +0800 Subject: [PATCH] Chore: sort Telegram DC Options --- Build/lib/mtproto-dc-config.test.ts | 48 +++++++++++++++++++++++++++++ Build/lib/mtproto-dc-config.ts | 39 ++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/Build/lib/mtproto-dc-config.test.ts b/Build/lib/mtproto-dc-config.test.ts index a7edbcec..65e08011 100644 --- a/Build/lib/mtproto-dc-config.test.ts +++ b/Build/lib/mtproto-dc-config.test.ts @@ -3,6 +3,7 @@ import { expect } from 'earl'; import { DC_OPTION_FLAG_IPV6, + DC_OPTION_FLAG_MEDIA_ONLY, DC_OPTION_FLAG_STATIC, mergeFallbackEndpoints, TELEGRAM_BOOTSTRAP_ENDPOINTS @@ -69,6 +70,53 @@ describe('MTProto DC config', () => { expect(result.bootstrapAdded).toEqual(TELEGRAM_BOOTSTRAP_ENDPOINTS.length - 1); }); + it('emits a deterministic option order regardless of input order', () => { + const options = [ + { id: 2, ip: '149.154.167.222', port: 443, flags: DC_OPTION_FLAG_MEDIA_ONLY }, + { id: 1, ip: '2001:0b28:f23d:f001:0000:0000:0000:000a', port: 443, flags: DC_OPTION_FLAG_IPV6 }, + // Numerically before .222 but lexically after it. + { id: 2, ip: '149.154.167.41', port: 443, flags: DC_OPTION_FLAG_STATIC }, + { id: 1, ip: '149.154.175.56', port: 443, flags: 0 } + ]; + + const build = (input: typeof options) => { + const config: MTProtoDCConfig = { + version: 1, + date: 1, + expires: 2, + this_dc: 1, + options: structuredClone(input) + }; + mergeFallbackEndpoints(config, []); + return config.options; + }; + + const forward = build(options); + expect(build([...options].reverse())).toEqual(forward); + + const ipsOfDc = (id: number) => forward.reduce((acc, option) => { + if (option.id === id) acc.push(option.ip); + return acc; + }, []); + + // IPv4 sorts numerically and precedes IPv6 within the same DC. + expect(ipsOfDc(1)).toEqual([ + '149.154.175.50', + '149.154.175.56', + '2001:b28:f23d:f001::a' + ]); + expect(ipsOfDc(2)).toEqual([ + '95.161.76.100', + '149.154.167.41', + '149.154.167.50', + '149.154.167.51', + '149.154.167.222', + '2001:67c:4e8:f002::a' + ]); + // DC ids are non-decreasing across the whole array. + expect(forward.every((option, i) => i === 0 || forward[i - 1].id <= option.id)).toEqual(true); + }); + it('keeps functional variants separate and preserves backup secrets', () => { const config: MTProtoDCConfig = { version: 1, diff --git a/Build/lib/mtproto-dc-config.ts b/Build/lib/mtproto-dc-config.ts index 7ecfa786..72755adf 100644 --- a/Build/lib/mtproto-dc-config.ts +++ b/Build/lib/mtproto-dc-config.ts @@ -7,6 +7,7 @@ import type { TelegramBackupEndpoint } from './get-telegram-backup-ip'; import { setBit, getBit } from 'foxts/bitwise'; import { bigint2ip, ip2bigint } from 'fast-cidr-tools'; import { isProbablyIpv6 } from 'foxts/is-probably-ip'; +import { fastIpVersion } from 'foxts/fast-ip-version'; export const DC_OPTION_FLAG_IPV6 = 1 << 0; export const DC_OPTION_FLAG_MEDIA_ONLY = 1 << 1; @@ -148,6 +149,39 @@ function mergeEndpoint(config: MTProtoDCConfig, endpoint: MTProtoEndpoint) { return !matched; } +/** + * The output order is otherwise a mix of whatever help.getConfig returned and + * the order the concurrent backup-endpoint lookups happened to resolve in, so + * two builds over identical upstream data can produce a spurious diff. Sort on + * the full option identity to keep the committed JSON reviewable. + */ +function sortOptions(config: MTProtoDCConfig) { + config.options.sort((a, b) => { + if (a.id !== b.id) return a.id - b.id; + + // IPv4 before IPv6, each ordered numerically rather than lexically so + // 149.154.167.51 sorts before 149.154.167.222. + const aVersion = fastIpVersion(a.ip); + const bVersion = fastIpVersion(b.ip); + if (aVersion !== bVersion) return aVersion - bVersion; + + if (aVersion === 0 || bVersion === 0) return 0; // how is invalid ip even here? just leave them in their original order + + const aIp = ip2bigint(a.ip, aVersion); + const bIp = ip2bigint(b.ip, bVersion); + if (aIp !== bIp) return aIp < bIp ? -1 : 1; + + if (a.port !== b.port) return a.port - b.port; + if (a.flags !== b.flags) return a.flags - b.flags; + + // Secrets are base64, so a plain code-unit compare is stable and locale-free. + const aSecret = a.secret ?? ''; + const bSecret = b.secret ?? ''; + if (aSecret === bSecret) return 0; + return aSecret < bSecret ? -1 : 1; + }); +} + function deduplicateOptions(config: MTProtoDCConfig) { const previousCount = config.options.length; const seen = new Set(); @@ -186,9 +220,12 @@ export function mergeFallbackEndpoints( if (mergeEndpoint(config, endpoint)) bootstrapAdded++; } + const duplicatesRemoved = deduplicateOptions(config); + sortOptions(config); + return { backupAdded, bootstrapAdded, - duplicatesRemoved: deduplicateOptions(config) + duplicatesRemoved }; }