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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. // @ts-check
  2. import path from 'node:path';
  3. import { DOMESTICS } from '../Source/non_ip/domestic';
  4. import { DIRECTS } from '../Source/non_ip/direct';
  5. import { readFileIntoProcessedArray } from './lib/fetch-text-by-line';
  6. import { compareAndWriteFile } from './lib/create-file';
  7. import { task } from './trace';
  8. import { SHARED_DESCRIPTION } from './lib/constants';
  9. import { createMemoizedPromise } from './lib/memo-promise';
  10. import * as yaml from 'yaml';
  11. import { appendArrayInPlace } from './lib/append-array-in-place';
  12. import { OUTPUT_INTERNAL_DIR, OUTPUT_MODULES_DIR, SOURCE_DIR } from './constants/dir';
  13. import { RulesetOutput } from './lib/create-file';
  14. export function createGetDnsMappingRule(allowWildcard: boolean) {
  15. const hasWildcard = (domain: string) => {
  16. if (domain.includes('*') || domain.includes('?')) {
  17. if (!allowWildcard) {
  18. throw new TypeError(`Wildcard domain is not supported: ${domain}`);
  19. }
  20. return true;
  21. }
  22. return false;
  23. };
  24. return (domain: string): string[] => {
  25. const results: string[] = [];
  26. if (domain[0] === '$') {
  27. const d = domain.slice(1);
  28. if (hasWildcard(domain)) {
  29. results.push(`DOMAIN-WILDCARD,${d}`);
  30. } else {
  31. results.push(`DOMAIN,${d}`);
  32. }
  33. } else if (domain[0] === '+') {
  34. const d = domain.slice(1);
  35. if (hasWildcard(domain)) {
  36. results.push(`DOMAIN-WILDCARD,*.${d}`);
  37. } else {
  38. results.push(`DOMAIN-SUFFIX,${d}`);
  39. }
  40. } else if (hasWildcard(domain)) {
  41. results.push(`DOMAIN-WILDCARD,${domain}`, `DOMAIN-WILDCARD,*.${domain}`);
  42. } else {
  43. results.push(`DOMAIN-SUFFIX,${domain}`);
  44. }
  45. return results;
  46. };
  47. }
  48. export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(async () => {
  49. const domestics = await readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/domestic.conf'));
  50. const directs = await readFileIntoProcessedArray(path.resolve(SOURCE_DIR, 'non_ip/direct.conf'));
  51. const lans: string[] = [];
  52. const getDnsMappingRuleWithWildcard = createGetDnsMappingRule(true);
  53. Object.values(DOMESTICS).forEach(({ domains }) => {
  54. appendArrayInPlace(domestics, domains.flatMap(getDnsMappingRuleWithWildcard));
  55. });
  56. Object.values(DIRECTS).forEach(({ domains }) => {
  57. appendArrayInPlace(directs, domains.flatMap(getDnsMappingRuleWithWildcard));
  58. });
  59. return [domestics, directs, lans] as const;
  60. });
  61. export const buildDomesticRuleset = task(require.main === module, __filename)(async (span) => {
  62. const [domestics, directs, lans] = await getDomesticAndDirectDomainsRulesetPromise();
  63. const dataset = appendArrayInPlace(Object.values(DOMESTICS), Object.values(DIRECTS));
  64. return Promise.all([
  65. new RulesetOutput(span, 'domestic', 'non_ip')
  66. .withTitle('Sukka\'s Ruleset - Domestic Domains')
  67. .withDescription([
  68. ...SHARED_DESCRIPTION,
  69. '',
  70. 'This file contains known addresses that are avaliable in the Mainland China.'
  71. ])
  72. .addFromRuleset(domestics)
  73. .write(),
  74. new RulesetOutput(span, 'direct', 'non_ip')
  75. .withTitle('Sukka\'s Ruleset - Direct Rules')
  76. .withDescription([
  77. ...SHARED_DESCRIPTION,
  78. '',
  79. 'This file contains domains and process that should not be proxied.'
  80. ])
  81. .addFromRuleset(directs)
  82. .write(),
  83. new RulesetOutput(span, 'lan', 'non_ip')
  84. .withTitle('Sukka\'s Ruleset - LAN')
  85. .withDescription([
  86. ...SHARED_DESCRIPTION,
  87. '',
  88. 'This file includes rules for LAN DOMAIN and reserved TLDs.'
  89. ])
  90. .addFromRuleset(lans)
  91. .write(),
  92. compareAndWriteFile(
  93. span,
  94. [
  95. '#!name=[Sukka] Local DNS Mapping',
  96. `#!desc=Last Updated: ${new Date().toISOString()}`,
  97. '',
  98. '[Host]',
  99. ...dataset.flatMap(({ domains, dns, hosts }) => [
  100. ...Object.entries(hosts).flatMap(([dns, ips]: [dns: string, ips: string[]]) => `${dns} = ${ips.join(', ')}`),
  101. ...domains.flatMap((domain) => {
  102. if (domain[0] === '$') {
  103. return [
  104. `${domain.slice(1)} = server:${dns}`
  105. ];
  106. }
  107. if (domain[0] === '+') {
  108. return [
  109. `*.${domain.slice(1)} = server:${dns}`
  110. ];
  111. }
  112. return [
  113. `${domain} = server:${dns}`,
  114. `*.${domain} = server:${dns}`
  115. ];
  116. })
  117. ])
  118. ],
  119. path.resolve(OUTPUT_MODULES_DIR, 'sukka_local_dns_mapping.sgmodule')
  120. ),
  121. compareAndWriteFile(
  122. span,
  123. yaml.stringify(
  124. dataset.reduce<{
  125. dns: { 'nameserver-policy': Record<string, string | string[]> },
  126. hosts: Record<string, string>
  127. }>((acc, cur) => {
  128. const { domains, dns, ...rest } = cur;
  129. domains.forEach((domain) => {
  130. let domainWildcard = domain;
  131. if (domain[0] === '$') {
  132. domainWildcard = domain.slice(1);
  133. } else if (domain[0] === '+') {
  134. domainWildcard = `*.${domain.slice(1)}`;
  135. } else {
  136. domainWildcard = `+.${domain}`;
  137. }
  138. acc.dns['nameserver-policy'][domainWildcard] = dns === 'system'
  139. ? ['system://', 'system', 'dhcp://system']
  140. : dns;
  141. });
  142. if ('hosts' in rest) {
  143. Object.assign(acc.hosts, rest.hosts);
  144. }
  145. return acc;
  146. }, {
  147. dns: { 'nameserver-policy': {} },
  148. hosts: {}
  149. }),
  150. { version: '1.1' }
  151. ).split('\n'),
  152. path.join(OUTPUT_INTERNAL_DIR, 'clash_nameserver_policy.yaml')
  153. )
  154. ]);
  155. });