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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. // @ts-check
  2. import path from 'node:path';
  3. import { DOMESTICS, DOH_BOOTSTRAP } from '../Source/non_ip/domestic';
  4. import { DIRECTS } 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 './lib/constants';
  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, SOURCE_DIR } from './constants/dir';
  14. import { RulesetOutput } from './lib/create-file';
  15. export function createGetDnsMappingRule(allowWildcard: boolean) {
  16. const hasWildcard = (domain: string) => {
  17. if (domain.includes('*') || domain.includes('?')) {
  18. if (!allowWildcard) {
  19. throw new TypeError(`Wildcard domain is not supported: ${domain}`);
  20. }
  21. return true;
  22. }
  23. return false;
  24. };
  25. return (domain: string): string[] => {
  26. const results: string[] = [];
  27. if (domain[0] === '$') {
  28. const d = domain.slice(1);
  29. if (hasWildcard(domain)) {
  30. results.push(`DOMAIN-WILDCARD,${d}`);
  31. } else {
  32. results.push(`DOMAIN,${d}`);
  33. }
  34. } else if (domain[0] === '+') {
  35. const d = domain.slice(1);
  36. if (hasWildcard(domain)) {
  37. results.push(`DOMAIN-WILDCARD,*.${d}`);
  38. } else {
  39. results.push(`DOMAIN-SUFFIX,${d}`);
  40. }
  41. } else if (hasWildcard(domain)) {
  42. results.push(`DOMAIN-WILDCARD,${domain}`, `DOMAIN-WILDCARD,*.${domain}`);
  43. } else {
  44. results.push(`DOMAIN-SUFFIX,${domain}`);
  45. }
  46. return results;
  47. };
  48. }
  49. export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(async () => {
  50. const domestics = await readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/domestic.conf'));
  51. const directs = await readFileIntoProcessedArray(path.resolve(SOURCE_DIR, 'non_ip/direct.conf'));
  52. const lans: string[] = [];
  53. const getDnsMappingRuleWithWildcard = createGetDnsMappingRule(true);
  54. [DOH_BOOTSTRAP, DOMESTICS].forEach((item) => {
  55. Object.values(item).forEach(({ domains }) => {
  56. appendArrayInPlace(domestics, domains.flatMap(getDnsMappingRuleWithWildcard));
  57. });
  58. });
  59. Object.values(DIRECTS).forEach(({ domains }) => {
  60. appendArrayInPlace(directs, domains.flatMap(getDnsMappingRuleWithWildcard));
  61. });
  62. return [domestics, directs, lans] as const;
  63. });
  64. export const buildDomesticRuleset = task(require.main === module, __filename)(async (span) => {
  65. const [domestics, directs, lans] = await getDomesticAndDirectDomainsRulesetPromise();
  66. const dataset: DNSMapping[] = ([DOH_BOOTSTRAP, DOMESTICS, DIRECTS] as const).flatMap(Object.values);
  67. return Promise.all([
  68. new RulesetOutput(span, 'domestic', 'non_ip')
  69. .withTitle('Sukka\'s Ruleset - Domestic Domains')
  70. .withDescription([
  71. ...SHARED_DESCRIPTION,
  72. '',
  73. 'This file contains known addresses that are avaliable in the Mainland China.'
  74. ])
  75. .addFromRuleset(domestics)
  76. .write(),
  77. new RulesetOutput(span, 'direct', 'non_ip')
  78. .withTitle('Sukka\'s Ruleset - Direct Rules')
  79. .withDescription([
  80. ...SHARED_DESCRIPTION,
  81. '',
  82. 'This file contains domains and process that should not be proxied.'
  83. ])
  84. .addFromRuleset(directs)
  85. .write(),
  86. new RulesetOutput(span, 'lan', 'non_ip')
  87. .withTitle('Sukka\'s Ruleset - LAN')
  88. .withDescription([
  89. ...SHARED_DESCRIPTION,
  90. '',
  91. 'This file includes rules for LAN DOMAIN and reserved TLDs.'
  92. ])
  93. .addFromRuleset(lans)
  94. .write(),
  95. compareAndWriteFile(
  96. span,
  97. [
  98. '#!name=[Sukka] Local DNS Mapping',
  99. `#!desc=Last Updated: ${new Date().toISOString()}`,
  100. '',
  101. '[Host]',
  102. ...Object.entries(
  103. // I use an object to deduplicate the domains
  104. // Otherwise I could just construct an array directly
  105. dataset.reduce<Record<string, string>>((acc, cur) => {
  106. const { domains, dns, hosts } = cur;
  107. Object.entries(hosts).forEach(([dns, ips]) => {
  108. if (!(dns in acc)) {
  109. acc[dns] = ips.join(', ');
  110. }
  111. });
  112. domains.forEach((domain) => {
  113. if (domain[0] === '$') {
  114. const d = domain.slice(1);
  115. if (!(d in acc)) {
  116. acc[d] = `server:${dns}`;
  117. }
  118. } else if (domain[0] === '+') {
  119. const d = `*.${domain.slice(1)}`;
  120. if (!(d in acc)) {
  121. acc[d] = `server:${dns}`;
  122. }
  123. } else {
  124. if (!(domain in acc)) {
  125. acc[domain] = `server:${dns}`;
  126. }
  127. const d = `*.${domain}`;
  128. if (!(d in acc)) {
  129. acc[d] = `server:${dns}`;
  130. }
  131. }
  132. });
  133. return acc;
  134. }, {})
  135. ).map(([dns, ips]) => `${dns} = ${ips}`)
  136. ],
  137. path.resolve(OUTPUT_MODULES_DIR, 'sukka_local_dns_mapping.sgmodule')
  138. ),
  139. compareAndWriteFile(
  140. span,
  141. yaml.stringify(
  142. dataset.reduce<{
  143. dns: { 'nameserver-policy': Record<string, string | string[]> },
  144. hosts: Record<string, string>
  145. }>((acc, cur) => {
  146. const { domains, dns, ...rest } = cur;
  147. domains.forEach((domain) => {
  148. let domainWildcard = domain;
  149. if (domain[0] === '$') {
  150. domainWildcard = domain.slice(1);
  151. } else if (domain[0] === '+') {
  152. domainWildcard = `*.${domain.slice(1)}`;
  153. } else {
  154. domainWildcard = `+.${domain}`;
  155. }
  156. acc.dns['nameserver-policy'][domainWildcard] = dns === 'system'
  157. ? ['system://', 'system', 'dhcp://system']
  158. : dns;
  159. });
  160. if ('hosts' in rest) {
  161. Object.assign(acc.hosts, rest.hosts);
  162. }
  163. return acc;
  164. }, {
  165. dns: { 'nameserver-policy': {} },
  166. hosts: {}
  167. }),
  168. { version: '1.1' }
  169. ).split('\n'),
  170. path.join(OUTPUT_INTERNAL_DIR, 'clash_nameserver_policy.yaml')
  171. ),
  172. compareAndWriteFile(
  173. span,
  174. [
  175. '# Local DNS Mapping for AdGuard Home',
  176. '',
  177. '[//]udp://10.10.1.1:53',
  178. ...dataset.flatMap(({ domains, dns: _dns }) => domains.flatMap((domain) => {
  179. const dns = _dns === 'system'
  180. ? 'udp://10.10.1.1:53'
  181. : _dns;
  182. if (
  183. // AdGuard Home has built-in AS112 / private PTR handling
  184. domain.endsWith('.arpa')
  185. // Ignore simple hostname
  186. || !domain.includes('.')
  187. ) {
  188. return [];
  189. }
  190. if (domain[0] === '$') {
  191. return [
  192. `[/${domain.slice(1)}/]${dns}`
  193. ];
  194. }
  195. if (domain[0] === '+') {
  196. return [
  197. `[/${domain.slice(1)}/]${dns}`
  198. ];
  199. }
  200. return [
  201. `[/${domain}/]${dns}`
  202. ];
  203. }))
  204. ],
  205. path.resolve(OUTPUT_INTERNAL_DIR, 'dns_mapping_adguardhome.conf')
  206. )
  207. ]);
  208. });