build-domestic-direct-lan-ruleset-dns-mapping-module.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. // @ts-check
  2. import path from 'node:path';
  3. import { DOMESTICS, DOH_BOOTSTRAP, AdGuardHomeDNSMapping } from '../Source/non_ip/domestic';
  4. import { DIRECTS, LAN } from '../Source/non_ip/direct';
  5. import type { DNSMapping } from '../Source/non_ip/direct';
  6. import { readFileIntoProcessedArray } from './lib/fetch-text-by-line';
  7. import { compareAndWriteFile } from './lib/create-file';
  8. import { task } from './trace';
  9. import { SHARED_DESCRIPTION } from './constants/description';
  10. import { createMemoizedPromise } from './lib/memo-promise';
  11. import * as yaml from 'yaml';
  12. import { appendArrayInPlace } from './lib/append-array-in-place';
  13. import { OUTPUT_INTERNAL_DIR, OUTPUT_MODULES_DIR, OUTPUT_MODULES_RULES_DIR, SOURCE_DIR } from './constants/dir';
  14. import { RulesetOutput } from './lib/create-file';
  15. import { SurgeOnlyRulesetOutput } from './lib/rules/ruleset';
  16. export function createGetDnsMappingRule(allowWildcard: boolean) {
  17. const hasWildcard = (domain: string) => {
  18. if (domain.includes('*') || domain.includes('?')) {
  19. if (!allowWildcard) {
  20. throw new TypeError(`Wildcard domain is not supported: ${domain}`);
  21. }
  22. return true;
  23. }
  24. return false;
  25. };
  26. return (domain: string): string[] => {
  27. const results: string[] = [];
  28. if (domain[0] === '$') {
  29. const d = domain.slice(1);
  30. if (hasWildcard(domain)) {
  31. results.push(`DOMAIN-WILDCARD,${d}`);
  32. } else {
  33. results.push(`DOMAIN,${d}`);
  34. }
  35. } else if (domain[0] === '+') {
  36. const d = domain.slice(1);
  37. if (hasWildcard(domain)) {
  38. results.push(`DOMAIN-WILDCARD,*.${d}`);
  39. } else {
  40. results.push(`DOMAIN-SUFFIX,${d}`);
  41. }
  42. } else if (hasWildcard(domain)) {
  43. results.push(`DOMAIN-WILDCARD,${domain}`, `DOMAIN-WILDCARD,*.${domain}`);
  44. } else {
  45. results.push(`DOMAIN-SUFFIX,${domain}`);
  46. }
  47. return results;
  48. };
  49. }
  50. export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(async () => {
  51. const domestics = await readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/domestic.conf'));
  52. const directs = await readFileIntoProcessedArray(path.resolve(SOURCE_DIR, 'non_ip/direct.conf'));
  53. const lans: string[] = [];
  54. const getDnsMappingRuleWithWildcard = createGetDnsMappingRule(true);
  55. [DOH_BOOTSTRAP, DOMESTICS].forEach((item) => {
  56. Object.values(item).forEach(({ domains }) => {
  57. appendArrayInPlace(domestics, domains.flatMap(getDnsMappingRuleWithWildcard));
  58. });
  59. });
  60. Object.values(DIRECTS).forEach(({ domains }) => {
  61. appendArrayInPlace(directs, domains.flatMap(getDnsMappingRuleWithWildcard));
  62. });
  63. Object.values(LAN).forEach(({ domains }) => {
  64. appendArrayInPlace(directs, domains.flatMap(getDnsMappingRuleWithWildcard));
  65. });
  66. return [domestics, directs, lans] as const;
  67. });
  68. export const buildDomesticRuleset = task(require.main === module, __filename)(async (span) => {
  69. const [domestics, directs, lans] = await getDomesticAndDirectDomainsRulesetPromise();
  70. const dataset: Array<[name: string, DNSMapping]> = ([DOH_BOOTSTRAP, DOMESTICS, DIRECTS, LAN] as const).flatMap(Object.entries);
  71. return Promise.all([
  72. new RulesetOutput(span, 'domestic', 'non_ip')
  73. .withTitle('Sukka\'s Ruleset - Domestic Domains')
  74. .withDescription([
  75. ...SHARED_DESCRIPTION,
  76. '',
  77. 'This file contains known addresses that are avaliable in the Mainland China.'
  78. ])
  79. .addFromRuleset(domestics)
  80. .write(),
  81. new RulesetOutput(span, 'direct', 'non_ip')
  82. .withTitle('Sukka\'s Ruleset - Direct Rules')
  83. .withDescription([
  84. ...SHARED_DESCRIPTION,
  85. '',
  86. 'This file contains domains and process that should not be proxied.'
  87. ])
  88. .addFromRuleset(directs)
  89. .write(),
  90. new RulesetOutput(span, 'lan', 'non_ip')
  91. .withTitle('Sukka\'s Ruleset - LAN')
  92. .withDescription([
  93. ...SHARED_DESCRIPTION,
  94. '',
  95. 'This file includes rules for LAN DOMAIN and reserved TLDs.'
  96. ])
  97. .addFromRuleset(lans)
  98. .write(),
  99. ...dataset.map(([name, { ruleset, domains }]) => {
  100. if (!ruleset) {
  101. return;
  102. }
  103. const output = new SurgeOnlyRulesetOutput(span, name.toLowerCase(), 'sukka_local_dns_mapping', OUTPUT_MODULES_RULES_DIR)
  104. .withTitle(`Sukka's Ruleset - Local DNS Mapping (${name})`)
  105. .withDescription([
  106. ...SHARED_DESCRIPTION,
  107. '',
  108. 'This is an internal rule that is only referenced by sukka_local_dns_mapping.sgmodule',
  109. 'Do not use this file in your Rule section, all rules are included in non_ip/domestic.conf already.'
  110. ]);
  111. domains.forEach((domain) => {
  112. switch (domain[0]) {
  113. case '$':
  114. output.addDomain(domain.slice(1));
  115. break;
  116. case '+':
  117. output.addDomainSuffix(domain.slice(1));
  118. break;
  119. default:
  120. output.addDomainSuffix(domain);
  121. break;
  122. }
  123. });
  124. return output.write();
  125. }),
  126. compareAndWriteFile(
  127. span,
  128. [
  129. '#!name=[Sukka] Local DNS Mapping',
  130. `#!desc=Last Updated: ${new Date().toISOString()}`,
  131. '',
  132. '[Host]',
  133. ...Object.entries(
  134. // I use an object to deduplicate the domains
  135. // Otherwise I could just construct an array directly
  136. dataset.reduce<Record<string, string>>((acc, cur) => {
  137. const ruleset_name = cur[0].toLowerCase();
  138. const { domains, dns, hosts, ruleset } = cur[1];
  139. Object.entries(hosts).forEach(([dns, ips]) => {
  140. acc[dns] ||= ips.join(', ');
  141. });
  142. if (ruleset) {
  143. acc[`RULE-SET:https://ruleset.skk.moe/Modules/Rules/sukka_local_dns_mapping/${ruleset_name}.conf`] ||= `server:${dns}`;
  144. } else {
  145. domains.forEach((domain) => {
  146. switch (domain[0]) {
  147. case '$':
  148. acc[domain.slice(1)] ||= `server:${dns}`;
  149. break;
  150. case '+':
  151. acc[`*.${domain.slice(1)}`] ||= `server:${dns}`;
  152. break;
  153. default:
  154. acc[domain] ||= `server:${dns}`;
  155. acc[`*.${domain}`] ||= `server:${dns}`;
  156. break;
  157. }
  158. });
  159. }
  160. return acc;
  161. }, {})
  162. ).map(([dns, ips]) => `${dns} = ${ips}`)
  163. ],
  164. path.resolve(OUTPUT_MODULES_DIR, 'sukka_local_dns_mapping.sgmodule')
  165. ),
  166. compareAndWriteFile(
  167. span,
  168. yaml.stringify(
  169. dataset.reduce<{
  170. dns: { 'nameserver-policy': Record<string, string | string[]> },
  171. hosts: Record<string, string>
  172. }>((acc, cur) => {
  173. const { domains, dns, ...rest } = cur[1];
  174. domains.forEach((domain) => {
  175. switch (domain[0]) {
  176. case '$':
  177. domain = domain.slice(1);
  178. break;
  179. case '+':
  180. domain = `*.${domain.slice(1)}`;
  181. break;
  182. default:
  183. domain = `+.${domain}`;
  184. break;
  185. }
  186. acc.dns['nameserver-policy'][domain] = dns === 'system'
  187. ? ['system://', 'system', 'dhcp://system']
  188. : dns;
  189. });
  190. if ('hosts' in rest) {
  191. Object.assign(acc.hosts, rest.hosts);
  192. }
  193. return acc;
  194. }, {
  195. dns: { 'nameserver-policy': {} },
  196. hosts: {}
  197. }),
  198. { version: '1.1' }
  199. ).split('\n'),
  200. path.join(OUTPUT_INTERNAL_DIR, 'clash_nameserver_policy.yaml')
  201. ),
  202. compareAndWriteFile(
  203. span,
  204. [
  205. '# Local DNS Mapping for AdGuard Home',
  206. 'tls://1.12.12.12',
  207. 'tls://120.53.53.53',
  208. 'https://1.12.12.12/dns-query',
  209. 'https://120.53.53.53/dns-query',
  210. '[//]udp://10.10.1.1:53',
  211. ...(([DOMESTICS, DIRECTS, LAN] as const).flatMap(Object.values) as DNSMapping[]).flatMap(({ domains, dns: _dns }) => domains.flatMap((domain) => {
  212. let dns;
  213. if (_dns in AdGuardHomeDNSMapping) {
  214. dns = AdGuardHomeDNSMapping[_dns as keyof typeof AdGuardHomeDNSMapping].join(' ');
  215. } else {
  216. console.warn(`Unknown DNS "${_dns}" not in AdGuardHomeDNSMapping`);
  217. dns = _dns;
  218. }
  219. // if (
  220. // // AdGuard Home has built-in AS112 / private PTR handling
  221. // domain.endsWith('.arpa')
  222. // // Ignore simple hostname
  223. // || !domain.includes('.')
  224. // ) {
  225. // return [];
  226. // }
  227. if (domain[0] === '$') {
  228. return [
  229. `[/${domain.slice(1)}/]${dns}`
  230. ];
  231. }
  232. if (domain[0] === '+') {
  233. return [
  234. `[/${domain.slice(1)}/]${dns}`
  235. ];
  236. }
  237. return [
  238. `[/${domain}/]${dns}`
  239. ];
  240. }))
  241. ],
  242. path.resolve(OUTPUT_INTERNAL_DIR, 'dns_mapping_adguardhome.conf')
  243. )
  244. ]);
  245. });