xdns-mask.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. type Raw = Record<string, unknown>;
  2. /** The EDNS0 payload the pre-26.9.30 xdns always negotiated; without it answers cap at 512. */
  3. export const XDNS_LEGACY_EDNS0 = 1232;
  4. const LEGACY_RECORD_TYPES: Record<string, number> = { '': 16, txt: 16, a: 1, aaaa: 28 };
  5. function isRaw(value: unknown): value is Raw {
  6. return !!value && typeof value === 'object' && !Array.isArray(value);
  7. }
  8. function legacyDomain(spec: string): Raw | null {
  9. let name = spec.trim();
  10. let method = '';
  11. const colon = name.lastIndexOf(':');
  12. if (colon >= 0) {
  13. method = name.slice(colon + 1).toLowerCase();
  14. name = name.slice(0, colon);
  15. }
  16. name = name.replace(/^\.+|\.+$/g, '');
  17. const type = LEGACY_RECORD_TYPES[method];
  18. if (!name || type === undefined) return null;
  19. return { names: [name], types: [type], edns0: XDNS_LEGACY_EDNS0 };
  20. }
  21. /** Folds a 26.9.30 domain's single `name` into the `names` list. */
  22. function upgradeDomainObject(domain: Raw): Raw {
  23. if (!('name' in domain)) return domain;
  24. const { name: raw, ...rest } = domain;
  25. const name = typeof raw === 'string' ? raw.trim() : '';
  26. const names = Array.isArray(rest.names) ? [...(rest.names as unknown[])] : [];
  27. if (name && !names.includes(name)) names.unshift(name);
  28. return { ...rest, names };
  29. }
  30. /** A 26.9.30 {type, settings:{addr}} resolver as an addrs URL; that loader took only udp/tcp. */
  31. function legacyResolverAddr(resolver: Raw): string | null {
  32. const kind = typeof resolver.type === 'string' ? resolver.type.trim().toLowerCase() : '';
  33. const settings = isRaw(resolver.settings) ? resolver.settings : {};
  34. const addr = typeof settings.addr === 'string' ? settings.addr.trim() : '';
  35. if ((kind !== 'udp' && kind !== 'tcp') || !addr) return null;
  36. return `${kind}://${addr}`;
  37. }
  38. const isLegacyDomain = (v: unknown) => typeof v === 'string' || (isRaw(v) && 'name' in v);
  39. const isLegacyResolver = (v: unknown) => typeof v === 'string' || (isRaw(v) && !('addrs' in v));
  40. // xray-core 26.10.10 (#7090) reads xdns only as names/addrs lists. Mirrors
  41. // internal/util/maskcompat: a bare name becomes TXT, entries the old core refused are dropped.
  42. export function upgradeLegacyXdnsSettings(settings: Raw): { next: Raw; changed: boolean } {
  43. const rawDomains = Array.isArray(settings.domains) ? (settings.domains as unknown[]) : [];
  44. const rawResolvers = Array.isArray(settings.resolvers) ? (settings.resolvers as unknown[]) : [];
  45. if (!rawDomains.some(isLegacyDomain) && !rawResolvers.some(isLegacyResolver)) {
  46. return { next: settings, changed: false };
  47. }
  48. const domains: unknown[] = [];
  49. const listed = new Set<string>();
  50. const addDomain = (domain: Raw) => {
  51. const names = Array.isArray(domain.names) ? (domain.names as unknown[]) : [];
  52. for (const name of names) if (typeof name === 'string') listed.add(name.toLowerCase());
  53. domains.push(domain);
  54. };
  55. const addLegacyDomain = (domain: Raw) => {
  56. const [name] = domain.names as string[];
  57. if (!listed.has(name.toLowerCase())) addDomain(domain);
  58. };
  59. for (const entry of rawDomains) {
  60. if (typeof entry === 'string') {
  61. const domain = legacyDomain(entry);
  62. if (domain) addLegacyDomain(domain);
  63. } else if (isRaw(entry)) {
  64. addDomain(upgradeDomainObject(entry));
  65. }
  66. }
  67. const resolvers: unknown[] = [];
  68. for (const entry of rawResolvers) {
  69. if (typeof entry === 'string') {
  70. const sep = entry.indexOf('+udp://');
  71. const addr = sep >= 0 ? entry.slice(sep + '+udp://'.length).trim() : '';
  72. const domain = sep >= 0 ? legacyDomain(entry.slice(0, sep)) : null;
  73. if (!addr || !domain) continue;
  74. addLegacyDomain(domain);
  75. resolvers.push({ addrs: [`udp://${addr}`] });
  76. } else if (isRaw(entry) && isLegacyResolver(entry)) {
  77. const addr = legacyResolverAddr(entry);
  78. if (addr) resolvers.push({ addrs: [addr] });
  79. } else {
  80. resolvers.push(entry);
  81. }
  82. }
  83. const next: Raw = { ...settings, domains };
  84. if (resolvers.length > 0) next.resolvers = resolvers;
  85. else delete next.resolvers;
  86. return { next, changed: true };
  87. }
  88. /** Applies upgradeLegacyXdnsSettings to every xdns entry of a finalmask.udp list. */
  89. export function upgradeLegacyXdnsMasks(udp: unknown[]): { next: unknown[]; changed: boolean } {
  90. let changed = false;
  91. const next = udp.map((entry) => {
  92. const mask = entry as Raw | null;
  93. if (!mask || typeof mask !== 'object' || String(mask.type).toLowerCase() !== 'xdns') {
  94. return entry;
  95. }
  96. if (!mask.settings || typeof mask.settings !== 'object') return entry;
  97. const upgraded = upgradeLegacyXdnsSettings(mask.settings as Raw);
  98. if (!upgraded.changed) return entry;
  99. changed = true;
  100. return { ...mask, settings: upgraded.next };
  101. });
  102. return { next, changed };
  103. }