瀏覽代碼

Support Clash Fake IP Filter Ruleset

SukkaW 1 年之前
父節點
當前提交
a5a3e061f6

+ 70 - 61
Build/build-domestic-direct-lan-ruleset-dns-mapping-module.ts

@@ -1,7 +1,7 @@
 // @ts-check
 import path from 'node:path';
 import { DOMESTICS } from '../Source/non_ip/domestic';
-import { DIRECTS, LANS } from '../Source/non_ip/direct';
+import { DIRECTS } from '../Source/non_ip/direct';
 import { readFileIntoProcessedArray } from './lib/fetch-text-by-line';
 import { compareAndWriteFile } from './lib/create-file';
 import { task } from './trace';
@@ -12,21 +12,42 @@ import { appendArrayInPlace } from './lib/append-array-in-place';
 import { OUTPUT_INTERNAL_DIR, OUTPUT_MODULES_DIR, SOURCE_DIR } from './constants/dir';
 import { RulesetOutput } from './lib/create-file';
 
-function getRule(domain: string): string[] {
-  const results: string[] = [];
+export function createGetDnsMappingRule(allowWildcard: boolean) {
+  const hasWildcard = (domain: string) => {
+    if (domain.includes('*') || domain.includes('?')) {
+      if (!allowWildcard) {
+        throw new TypeError(`Wildcard domain is not supported: ${domain}`);
+      }
+      return true;
+    }
 
-  if (domain[0] === '$' || domain[0] === '+') {
-    results.push(`DOMAIN-SUFFIX,${domain.slice(1)}`);
-  } else if (domain.includes('?')) {
-    results.push(
-      `DOMAIN-WILDCARD,${domain}`,
-      `DOMAIN-WILDCARD,*.${domain}`
-    );
-  } else {
-    results.push(`DOMAIN-SUFFIX,${domain}`);
-  }
+    return false;
+  };
 
-  return results;
+  return (domain: string): string[] => {
+    const results: string[] = [];
+    if (domain[0] === '$') {
+      const d = domain.slice(1);
+      if (hasWildcard(domain)) {
+        results.push(`DOMAIN-WILDCARD,${d}`);
+      } else {
+        results.push(`DOMAIN,${d}`);
+      }
+    } else if (domain[0] === '+') {
+      const d = domain.slice(1);
+      if (hasWildcard(domain)) {
+        results.push(`DOMAIN-WILDCARD,*.${d}`);
+      } else {
+        results.push(`DOMAIN-SUFFIX,${d}`);
+      }
+    } else if (hasWildcard(domain)) {
+      results.push(`DOMAIN-WILDCARD,${domain}`, `DOMAIN-WILDCARD,*.${domain}`);
+    } else {
+      results.push(`DOMAIN-SUFFIX,${domain}`);
+    }
+
+    return results;
+  };
 }
 
 export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(async () => {
@@ -34,14 +55,13 @@ export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(a
   const directs = await readFileIntoProcessedArray(path.resolve(SOURCE_DIR, 'non_ip/direct.conf'));
   const lans: string[] = [];
 
-  Object.entries(DOMESTICS).forEach(([, { domains }]) => {
-    appendArrayInPlace(domestics, domains.flatMap(getRule));
-  });
-  Object.entries(DIRECTS).forEach(([, { domains }]) => {
-    appendArrayInPlace(directs, domains.flatMap(getRule));
+  const getDnsMappingRuleWithWildcard = createGetDnsMappingRule(true);
+
+  Object.values(DOMESTICS).forEach(({ domains }) => {
+    appendArrayInPlace(domestics, domains.flatMap(getDnsMappingRuleWithWildcard));
   });
-  Object.entries(LANS).forEach(([, { domains }]) => {
-    appendArrayInPlace(lans, domains.flatMap(getRule));
+  Object.values(DIRECTS).forEach(({ domains }) => {
+    appendArrayInPlace(directs, domains.flatMap(getDnsMappingRuleWithWildcard));
   });
 
   return [domestics, directs, lans] as const;
@@ -50,9 +70,7 @@ export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(a
 export const buildDomesticRuleset = task(require.main === module, __filename)(async (span) => {
   const [domestics, directs, lans] = await getDomesticAndDirectDomainsRulesetPromise();
 
-  const dataset = Object.entries(DOMESTICS);
-  appendArrayInPlace(dataset, Object.entries(DIRECTS));
-  appendArrayInPlace(dataset, Object.entries(LANS));
+  const dataset = appendArrayInPlace(Object.values(DOMESTICS), Object.values(DIRECTS));
 
   return Promise.all([
     new RulesetOutput(span, 'domestic', 'non_ip')
@@ -89,7 +107,7 @@ export const buildDomesticRuleset = task(require.main === module, __filename)(as
         `#!desc=Last Updated: ${new Date().toISOString()}`,
         '',
         '[Host]',
-        ...dataset.flatMap(([, { domains, dns, hosts }]) => [
+        ...dataset.flatMap(({ domains, dns, hosts }) => [
           ...Object.entries(hosts).flatMap(([dns, ips]: [dns: string, ips: string[]]) => `${dns} = ${ips.join(', ')}`),
           ...domains.flatMap((domain) => {
             if (domain[0] === '$') {
@@ -114,44 +132,35 @@ export const buildDomesticRuleset = task(require.main === module, __filename)(as
     compareAndWriteFile(
       span,
       yaml.stringify(
-        {
-          dns: {
-            'nameserver-policy': dataset.reduce<Record<string, string | string[]>>(
-              (acc, [, { domains, dns }]) => {
-                domains.forEach((domain) => {
-                  let domainWildcard = domain;
-                  if (domain[0] === '$') {
-                    domainWildcard = domain.slice(1);
-                  } else if (domain[0] === '+') {
-                    domainWildcard = `*.${domain.slice(1)}`;
-                  } else {
-                    domainWildcard = `+.${domain}`;
-                  }
+        dataset.reduce<{
+          dns: { 'nameserver-policy': Record<string, string | string[]> },
+          hosts: Record<string, string>
+        }>((acc, cur) => {
+          const { domains, dns, ...rest } = cur;
+          domains.forEach((domain) => {
+            let domainWildcard = domain;
+            if (domain[0] === '$') {
+              domainWildcard = domain.slice(1);
+            } else if (domain[0] === '+') {
+              domainWildcard = `*.${domain.slice(1)}`;
+            } else {
+              domainWildcard = `+.${domain}`;
+            }
+
+            acc.dns['nameserver-policy'][domainWildcard] = dns === 'system'
+              ? ['system://', 'system', 'dhcp://system']
+              : dns;
+          });
 
-                  acc[domainWildcard] = dns === 'system'
-                    ? [
-                      'system://',
-                      'system',
-                      'dhcp://system'
-                    ]
-                    : dns;
-                });
+          if ('hosts' in rest) {
+            Object.assign(acc.hosts, rest.hosts);
+          }
 
-                return acc;
-              },
-              {}
-            )
-          },
-          hosts: dataset.reduce<Record<string, string>>(
-            (acc, [, { domains, dns, ...rest }]) => {
-              if ('hosts' in rest) {
-                Object.assign(acc, rest.hosts);
-              }
-              return acc;
-            },
-            {}
-          )
-        },
+          return acc;
+        }, {
+          dns: { 'nameserver-policy': {} },
+          hosts: {}
+        }),
         { version: '1.1' }
       ).split('\n'),
       path.join(OUTPUT_INTERNAL_DIR, 'clash_nameserver_policy.yaml')

+ 31 - 17
Build/build-sgmodule-always-realip.ts

@@ -1,10 +1,14 @@
 import path from 'node:path';
 import { task } from './trace';
-import { compareAndWriteFile } from './lib/create-file';
-import { DIRECTS, LANS } from '../Source/non_ip/direct';
+import { compareAndWriteFile, DomainsetOutput } from './lib/create-file';
+import { DIRECTS } from '../Source/non_ip/direct';
+import type { DNSMapping } from '../Source/non_ip/direct';
+import { DOMESTICS } from '../Source/non_ip/domestic';
 import * as yaml from 'yaml';
 import { OUTPUT_INTERNAL_DIR, OUTPUT_MODULES_DIR } from './constants/dir';
 import { appendArrayInPlace } from './lib/append-array-in-place';
+import { SHARED_DESCRIPTION } from './lib/constants';
+import { createGetDnsMappingRule } from './build-domestic-direct-lan-ruleset-dns-mapping-module';
 
 const HOSTNAMES = [
   // Network Detection, Captive Portal
@@ -33,22 +37,31 @@ const HOSTNAMES = [
 ];
 
 export const buildAlwaysRealIPModule = task(require.main === module, __filename)(async (span) => {
+  const surge: string[] = [];
+  const clashFakeIpFilter = new DomainsetOutput(span, 'clash_fake_ip_filter')
+    .withTitle('Sukka\'s Ruleset - Always Real IP Plus')
+    .withDescription([
+      ...SHARED_DESCRIPTION,
+      '',
+      'Clash.Meta fake-ip-filter as ruleset'
+    ]);
+
   // Intranet, Router Setup, and mant more
-  const dataset = [
-    DIRECTS.HOTSPOT_CAPTIVE_PORTAL,
-    DIRECTS.SYSTEM,
-    ...Object.values(LANS)
-  ];
-  const surge = dataset.flatMap(({ domains }) => domains.flatMap((domain) => {
-    switch (domain[0]) {
-      case '+':
-        return [`*.${domain.slice(1)}`];
-      case '$':
-        return [domain.slice(1)];
-      default:
-        return [domain, `*.${domain}`];
-    }
-  }));
+  const dataset = [DIRECTS, DOMESTICS].reduce<DNSMapping[]>((acc, item) => {
+    Object.values(item).forEach((i: DNSMapping) => {
+      if (i.realip) {
+        acc.push(i);
+      }
+    });
+
+    return acc;
+  }, []);
+
+  const getDnsMappingRuleWithoutWildcard = createGetDnsMappingRule(false);
+
+  for (const { domains } of dataset) {
+    clashFakeIpFilter.addFromRuleset(domains.flatMap(getDnsMappingRuleWithoutWildcard));
+  }
 
   return Promise.all([
     compareAndWriteFile(
@@ -62,6 +75,7 @@ export const buildAlwaysRealIPModule = task(require.main === module, __filename)
       ],
       path.resolve(OUTPUT_MODULES_DIR, 'sukka_common_always_realip.sgmodule')
     ),
+    clashFakeIpFilter.writeClash(),
     compareAndWriteFile(
       span,
       yaml.stringify(

+ 4 - 4
Build/lib/fetch-retry.ts

@@ -11,7 +11,7 @@ import type {
   Response
 } from 'undici';
 
-export type UndiciResponseData = Dispatcher.ResponseData<any>;
+export type UndiciResponseData = Dispatcher.ResponseData;
 
 import CacheableLookup from 'cacheable-lookup';
 import type { LookupOptions as CacheableLookupOptions } from 'cacheable-lookup';
@@ -50,9 +50,9 @@ setGlobalDispatcher(agent.compose(
         return cb(err);
       }
 
-      if (errorCode !== 'UND_ERR_REQ_RETRY') {
-        return cb(err);
-      }
+      // if (errorCode === 'UND_ERR_REQ_RETRY') {
+      //   return cb(err);
+      // }
 
       const { method, retryOptions = {} } = opts;
 

+ 19 - 5
Build/lib/rules/base.ts

@@ -65,10 +65,7 @@ export abstract class RuleOutput<TPreprocessed = unknown> {
     return result;
   };
 
-  constructor(
-    protected readonly span: Span,
-    protected readonly id: string
-  ) { }
+  constructor(protected readonly span: Span, protected readonly id: string) { }
 
   protected title: string | null = null;
   withTitle(title: string) {
@@ -245,7 +242,6 @@ export abstract class RuleOutput<TPreprocessed = unknown> {
   }
 
   private $$preprocessed: TPreprocessed | null = null;
-
   get $preprocessed() {
     if (this.$$preprocessed === null) {
       this.$$preprocessed = this.span.traceChildSync('RuleOutput#preprocess: ' + this.id, () => this.preprocess());
@@ -253,6 +249,24 @@ export abstract class RuleOutput<TPreprocessed = unknown> {
     return this.$$preprocessed;
   }
 
+  async writeClash(outputDir?: null | string) {
+    await this.done();
+
+    invariant(this.title, 'Missing title');
+    invariant(this.description, 'Missing description');
+
+    return compareAndWriteFile(
+      this.span,
+      withBannerArray(
+        this.title,
+        this.description,
+        this.date,
+        this.clash()
+      ),
+      path.join(outputDir ?? OUTPUT_CLASH_DIR, this.type, this.id + '.txt')
+    );
+  }
+
   async write(): Promise<void> {
     await this.done();
 

+ 24 - 19
Source/non_ip/direct.ts

@@ -2,6 +2,8 @@ export interface DNSMapping {
   hosts: {
     [domain: string]: string[]
   },
+  /** which also disallows wildcard */
+  realip: boolean,
   dns: string,
   /**
    * domain[0]
@@ -13,10 +15,11 @@ export interface DNSMapping {
   domains: string[]
 }
 
-export const DIRECTS = {
+export const DIRECTS: Record<string, DNSMapping> = {
   HOTSPOT_CAPTIVE_PORTAL: {
     dns: 'system',
     hosts: {},
+    realip: false,
     domains: [
       'securelogin.com.cn',
       '$captive.apple.com',
@@ -26,6 +29,7 @@ export const DIRECTS = {
   ROUTER: {
     dns: 'system',
     hosts: {},
+    realip: false,
     domains: [
       '+home',
       // 'zte.home', // ZTE CPE
@@ -82,6 +86,7 @@ export const DIRECTS = {
   SYSTEM: {
     dns: 'system',
     hosts: {},
+    realip: true,
     domains: [
       '+m2m',
       // TailScale Magic DNS
@@ -94,17 +99,17 @@ export const DIRECTS = {
       '+bogon',
       '+_msdcs'
     ]
-  }
-} satisfies Record<string, DNSMapping>;
-
-export const LANS = {
+  },
   LAN: {
     dns: 'system',
-    hosts: {},
+    hosts: {
+      localhost: ['127.0.0.1']
+    },
+    realip: true,
     domains: [
       '+lan',
       // 'amplifi.lan',
-      '$localhost',
+      // '$localhost',
       'localdomain',
       'home.arpa',
       // AS112
@@ -113,21 +118,21 @@ export const LANS = {
       '17.172.in-addr.arpa',
       '18.172.in-addr.arpa',
       '19.172.in-addr.arpa',
-      '2?.172.in-addr.arpa',
-      // '20.172.in-addr.arpa',
-      // '21.172.in-addr.arpa',
-      // '22.172.in-addr.arpa',
-      // '23.172.in-addr.arpa',
-      // '24.172.in-addr.arpa',
-      // '25.172.in-addr.arpa',
-      // '26.172.in-addr.arpa',
-      // '27.172.in-addr.arpa',
-      // '28.172.in-addr.arpa',
-      // '29.172.in-addr.arpa',
+      // '2?.172.in-addr.arpa',
+      '20.172.in-addr.arpa',
+      '21.172.in-addr.arpa',
+      '22.172.in-addr.arpa',
+      '23.172.in-addr.arpa',
+      '24.172.in-addr.arpa',
+      '25.172.in-addr.arpa',
+      '26.172.in-addr.arpa',
+      '27.172.in-addr.arpa',
+      '28.172.in-addr.arpa',
+      '29.172.in-addr.arpa',
       '30.172.in-addr.arpa',
       '31.172.in-addr.arpa',
       '168.192.in-addr.arpa',
       '254.169.in-addr.arpa'
     ]
   }
-} satisfies Record<string, DNSMapping>;
+};

+ 11 - 2
Source/non_ip/domestic.ts

@@ -1,11 +1,12 @@
 import type { DNSMapping } from './direct';
 
-export const DOMESTICS = {
+export const DOMESTICS: Record<string, DNSMapping> = {
   ALIBABA: {
     hosts: {
       'dns.alidns.com': ['223.5.5.5', '223.6.6.6', '2400:3200:baba::1', '2400:3200::1']
     },
     dns: 'quic://dns.alidns.com:853',
+    realip: false,
     domains: [
       'uc.cn',
       // 'ucweb.com', // UC International
@@ -89,6 +90,7 @@ export const DOMESTICS = {
       'dns.pub': ['120.53.53.53', '1.12.12.12', '1.12.34.56']
     },
     dns: 'https://doh.pub/dns-query',
+    realip: false,
     domains: [
       // 'dns.pub',
       // 'doh.pub',
@@ -142,6 +144,7 @@ export const DOMESTICS = {
   BILIBILI_ALI: {
     dns: 'quic://dns.alidns.com:853',
     hosts: {},
+    realip: false,
     domains: [
       '$upos-sz-mirrorali.bilivideo.com',
       '$upos-sz-estgoss.bilivideo.com'
@@ -150,6 +153,7 @@ export const DOMESTICS = {
   BILIBILI_BD: {
     dns: '180.76.76.76',
     hosts: {},
+    realip: false,
     domains: [
       '$upos-sz-mirrorbd.bilivideo.com',
       '$upos-sz-mirrorbos.bilivideo.com'
@@ -158,6 +162,7 @@ export const DOMESTICS = {
   BILIBILI: {
     dns: 'https://doh.pub/dns-query',
     hosts: {},
+    realip: false,
     domains: [
       '$upos-sz-mirrorcoso1.bilivideo.com',
       '$upos-sz-mirrorcosbstar1.bilivideo.com', // Bilibili Intl with Tencent Cloud CDN
@@ -187,6 +192,7 @@ export const DOMESTICS = {
   XIAOMI: {
     dns: 'https://doh.pub/dns-query',
     hosts: {},
+    realip: false,
     domains: [
       'mi.com',
       'duokan.com',
@@ -205,6 +211,7 @@ export const DOMESTICS = {
   BYTEDANCE: {
     dns: '180.184.2.2',
     hosts: {},
+    realip: false,
     domains: [
       '+bytecdn.cn',
       '+toutiaoimg.com',
@@ -251,6 +258,7 @@ export const DOMESTICS = {
   BAIDU: {
     dns: '180.76.76.76',
     hosts: {},
+    realip: false,
     domains: [
       '91.com',
       'hao123.com',
@@ -288,6 +296,7 @@ export const DOMESTICS = {
       'dns.360.net': ['101.198.198.198', '101.198.199.200']
     },
     dns: 'https://dns.360.net/dns-query',
+    realip: false,
     domains: [
       '+qhimg.com',
       '+qhimgs.com',
@@ -323,4 +332,4 @@ export const DOMESTICS = {
       'qiku.com'
     ]
   }
-} satisfies Record<string, DNSMapping>;
+};