├── Mock ├── 200.txt ├── 200.json ├── 200.array.json ├── doubleclick-net_instream_ad_status.js ├── www-google-analytics-com_inpage_linkid.js ├── static-chartbeat-com_chartbeat_mab.js ├── ampproject-org_v0.js ├── www-google-analytics-com_cx_api.js ├── addthis-com_addthis_widget.js ├── widgets-outbrain-com_outbrain.js └── securepubads-g-doubleclick-net_tag_js_gpt.js ├── .node-version ├── .gitignore ├── tsconfig.test.json ├── Source ├── non_ip │ ├── gitlab.conf │ ├── my_git.conf │ ├── my_us.conf │ ├── neteasemusic.conf │ ├── my_direct.conf │ ├── my_tw.conf │ ├── my_proxy.conf │ ├── cloudmounter.ts │ ├── apple_cn.conf │ ├── telegram.conf │ ├── download.conf │ ├── sogouinput.conf │ ├── my_plus.conf │ ├── apple_intelligence.conf │ ├── reject-drop.conf │ ├── reject-no-drop.conf │ ├── my_reject.conf │ ├── apple_services.conf │ ├── microsoft.conf │ ├── ai.conf │ ├── cdn.conf │ ├── global.ts │ ├── direct.ts │ └── direct.conf ├── ip │ ├── cdn.conf │ ├── domestic.conf │ ├── lan.conf │ ├── apple_services.conf │ ├── telegram_asn.conf │ ├── download.conf │ ├── badboy_asn.ts │ ├── neteasemusic.conf │ └── reject.conf └── domainset │ ├── icloud_private_relay.conf │ ├── game-download.conf │ └── speedtest.conf ├── Modules ├── google_cn_307.sgmodule ├── sukka_surge_network_test_domain.sgmodule ├── sukka_mitm_all_hostnames.sgmodule ├── sukka_disable_netease_music_v2_update_check.sgmodule └── sukka_enhance_adblock.sgmodule ├── Build ├── lib │ ├── normalize-domain.test.ts │ ├── parse-filter │ │ ├── parse-filter.test.ts │ │ ├── shared.ts │ │ ├── domainlists.ts │ │ └── hosts.ts │ ├── process-line.bench.ts │ ├── process-line.test.ts │ ├── fetch-text-by-line.bench.ts │ ├── rules │ │ ├── ip.ts │ │ ├── domainset.ts │ │ └── ruleset.ts │ ├── writing-strategy │ │ ├── legacy-clash-premium.ts │ │ ├── surfboard.ts │ │ ├── base.ts │ │ ├── adguardhome.ts │ │ ├── singbox.ts │ │ └── clash.ts │ ├── parse-dnsmasq.ts │ ├── cache-apply.ts │ ├── tldts.bench.ts │ ├── run-against-source-file.ts │ ├── create-file.ts │ ├── process-line.ts │ ├── tree-dir.ts │ ├── misc.ts │ ├── fetch-text-by-line.ts │ ├── create-file.test.ts │ ├── fetch-assets.ts │ ├── is-domain-alive.ts │ ├── normalize-domain.ts │ └── fetch-retry.ts ├── constants │ ├── description.ts │ ├── microsoft-cdn.ts │ ├── loose-tldts-opt.ts │ ├── dir.ts │ ├── domains.ts │ └── phishing-score-source.ts ├── build-reject-ip-list.ts ├── build-cloudmounter-rules.ts ├── build-apple-cdn.ts ├── build-telegram-cidr.ts ├── validate-global-tld.ts ├── trim-source.ts ├── validate-reject-stats.ts ├── validate-hash-collision-test.ts ├── build-global-server-dns-mapping.ts ├── build-chn-cidr.ts ├── tools-lum-apex-domains.ts ├── download-mock-assets.ts ├── build-stream-service.ts ├── build-deprecate-files.ts ├── validate-domestic.ts ├── validate-domain-alive.ts ├── build-speedtest-domainset.ts ├── tools-migrate-domains.ts ├── build-microsoft-cdn.ts ├── download-previous-build.ts ├── tools-dedupe-src.ts ├── build-sgmodule-always-realip.ts ├── build-common.ts ├── build-cdn-download-conf.ts ├── index.ts └── validate-gfwlist.ts ├── eslint.config.js ├── tsconfig.json ├── .github └── workflows │ └── check-source-domain.yml └── package.json /Mock/200.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /Mock/200.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Mock/200.array.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /Mock/doubleclick-net_instream_ad_status.js: -------------------------------------------------------------------------------- 1 | window.google_ad_status = 1; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .clinic 4 | .wireit 5 | .cache 6 | public 7 | tmp.* 8 | .BUILD_FINISHED 9 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "inlineSourceMap": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Mock/www-google-analytics-com_inpage_linkid.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | window._gaq = window._gaq || { 5 | push() { 6 | // noop 7 | } 8 | }; 9 | }()); 10 | -------------------------------------------------------------------------------- /Source/non_ip/gitlab.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - GitLab 2 | # $ meta_description GitLab forbids usage from CN, HK, MO. This file contains ruleset for GitLab. 3 | 4 | DOMAIN-SUFFIX,gitlab.com 5 | -------------------------------------------------------------------------------- /Source/non_ip/my_git.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Internal Special 2 | 3 | DOMAIN-SUFFIX,huggingface.co 4 | DOMAIN,github.com 5 | DOMAIN,github.dev 6 | DOMAIN,api.github.com 7 | DOMAIN,alive.github.com 8 | DOMAIN-SUFFIX,githubcopilot.com 9 | -------------------------------------------------------------------------------- /Modules/google_cn_307.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=谷歌中国重定向 2 | #!desc=将 google.cn 的请求重定向到 google.com 3 | 4 | [URL Rewrite] 5 | ^https?://(www.)?(g|google)\.cn https://www.google.com 307 6 | 7 | [MITM] 8 | hostname = %APPEND% g.cn,www.g.cn,google.cn,www.google.cn 9 | -------------------------------------------------------------------------------- /Source/non_ip/my_us.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - US 2 | 3 | DOMAIN,copymanga.site 4 | DOMAIN-SUFFIX,mandaom.com 5 | DOMAIN-SUFFIX,crunchyroll.com 6 | DOMAIN,www.coinbase.com 7 | DOMAIN,javdb.com 8 | DOMAIN,cableav.tv 9 | DOMAIN,jav.guru 10 | -------------------------------------------------------------------------------- /Modules/sukka_surge_network_test_domain.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=[Sukka] Change Test Url 2 | #!desc=设置 Surge 网络检测地址为 Vivo 和 Cloudflare 3 | 4 | [General] 5 | internet-test-url = http://wifi.vivo.com.cn/generate_204 6 | proxy-test-url = http://latency-test.skk.moe/endpoint 7 | -------------------------------------------------------------------------------- /Source/ip/cdn.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - CDN 2 | # $ meta_description This file contains CDN IP addresses for object storage and static assets CDN. 3 | 4 | # GitHub BYOIP 5 | IP-CIDR,185.199.108.0/22 6 | # Cloudflare NTP 7 | IP-CIDR,162.159.200.1/32 8 | IP-CIDR,162.159.200.123/32 9 | -------------------------------------------------------------------------------- /Build/lib/normalize-domain.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { normalizeDomain } from './normalize-domain'; 4 | 5 | describe('normalizeDomain', () => { 6 | it('mine.torrent.pw', () => { 7 | console.log(normalizeDomain('mine.torrent.pw')); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('eslint-config-sukka').sukka({ 4 | ignores: [ 5 | '**/*.conf', 6 | '**/*.txt' 7 | ], 8 | js: { 9 | disableNoConsoleInCLI: ['Build/**'] 10 | }, 11 | node: true, 12 | ts: true, 13 | yaml: false 14 | }); 15 | -------------------------------------------------------------------------------- /Source/ip/domestic.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Mainland China Supplement CIDR 2 | # $ meta_description This file contains IPs broadcast inside Mainland China. 3 | 4 | # cloud.tencent.com AIA 5 | IP-CIDR,162.14.0.0/18,no-resolve 6 | # aliyun.com Premium DNS 7 | IP-CIDR,140.205.1.0/24,no-resolve 8 | -------------------------------------------------------------------------------- /Source/non_ip/neteasemusic.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Netease Music 2 | # $ meta_description This file contains rules for Netease Music. 3 | 4 | USER-AGENT,%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90 5 | USER-AGENT,NeteaseMusic* 6 | DOMAIN-SUFFIX,music.126.net 7 | DOMAIN-SUFFIX,music.163.com 8 | -------------------------------------------------------------------------------- /Modules/sukka_mitm_all_hostnames.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=[Sukka] MitM All Hostnames 2 | #!desc=Perform MitM on all hostnames with port 443, except those to Apple and other common sites which can't be inspected. 3 | 4 | [MITM] 5 | hostname = -*.apple.com, -*.icloud.com, -*.mzstatic.com, -*.crashlytics.com, -*.facebook.com, -*.instagram.com, * 6 | -------------------------------------------------------------------------------- /Source/non_ip/my_direct.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Direct 2 | 3 | DOMAIN-SUFFIX,torrentmac.net 4 | DOMAIN-SUFFIX,download.555mac.com 5 | DOMAIN-KEYWORD,mac-torrent-download 6 | DOMAIN-SUFFIX,engage.cloudflareclient.com 7 | 8 | DOMAIN,mirrorbits.lineageos.org 9 | 10 | PROCESS-NAME,AdGuardHome 11 | PROCESS-NAME,nmap 12 | -------------------------------------------------------------------------------- /Source/domainset/icloud_private_relay.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - iCloud Private Relay 2 | # $ meta_description This file contains domains for iCloud Private Relay Endpoint. 3 | 4 | mask.icloud.com 5 | mask-canary.icloud.com 6 | mask-h2.icloud.com 7 | mask-api.icloud.com 8 | mask.apple-dns.net 9 | canary.mask.apple-dns.net 10 | -------------------------------------------------------------------------------- /Mock/static-chartbeat-com_chartbeat_mab.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const noopfn = function () { /* noop */ }; 5 | window.pSUPERFLY = { 6 | activity: noopfn, 7 | virtualPage: noopfn 8 | }; 9 | for (const hider of document.querySelectorAll('style[id^=chartbeat-flicker-control]')) { 10 | hider.remove(); 11 | } 12 | }()); 13 | -------------------------------------------------------------------------------- /Mock/ampproject-org_v0.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const head = document.head; 5 | if (!head) { return; } 6 | const style = document.createElement('style'); 7 | style.textContent = [ 8 | 'body {', 9 | ' animation: none !important;', 10 | ' overflow: unset !important;', 11 | '}' 12 | ].join('\n'); 13 | head.appendChild(style); 14 | }()); 15 | -------------------------------------------------------------------------------- /Mock/www-google-analytics-com_cx_api.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const noopfn = function () { 5 | // noop 6 | }; 7 | window.cxApi = { 8 | chooseVariation() { 9 | return 0; 10 | }, 11 | getChosenVariation: noopfn, 12 | setAllowHash: noopfn, 13 | setChosenVariation: noopfn, 14 | setCookiePath: noopfn, 15 | setDomainName: noopfn 16 | }; 17 | }()); 18 | -------------------------------------------------------------------------------- /Source/ip/lan.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - LAN 2 | # $ meta_description Includes rules for LAN IP addresses and .local suffix. 3 | 4 | IP-CIDR,0.0.0.0/8 5 | IP-CIDR,10.0.0.0/8 6 | IP-CIDR,100.64.0.0/10 7 | IP-CIDR,127.0.0.0/8 8 | IP-CIDR,172.16.0.0/12 9 | IP-CIDR,169.254.0.0/16 10 | IP-CIDR,192.168.0.0/16 11 | IP-CIDR,224.0.0.0/4 12 | IP-CIDR6,::1/128 13 | IP-CIDR6,fc00::/7 14 | IP-CIDR6,fe80::/10 15 | -------------------------------------------------------------------------------- /Source/non_ip/my_tw.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - TW 2 | 3 | DOMAIN,hanime1.me 4 | DOMAIN-SUFFIX,cdn.hinet.net 5 | DOMAIN,av.jkforum.net 6 | DOMAIN,avbebe.com 7 | 8 | DOMAIN,mhgui.com 9 | DOMAIN,www.manhuagui.com 10 | 11 | DOMAIN,yymanhua.com 12 | DOMAIN,www.yymanhua.com 13 | 14 | DOMAIN-WILDCARD,*.ffzy-online?.com 15 | DOMAIN-WILDCARD,*.ffzy-play?.com 16 | DOMAIN-WILDCARD,*.lz-cdn?.com 17 | DOMAIN-WILDCARD,*.lz-cdn??.com 18 | -------------------------------------------------------------------------------- /Build/lib/parse-filter/parse-filter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { parse } from './filters'; 4 | import type { ParseType } from './filters'; 5 | 6 | describe('parse', () => { 7 | const MUTABLE_PARSE_LINE_RESULT: [string, ParseType] = ['', 1000]; 8 | 9 | it('||top.mail.ru^$badfilter', () => { 10 | console.log(parse('||top.mail.ru^$badfilter', MUTABLE_PARSE_LINE_RESULT, false)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /Build/lib/process-line.bench.ts: -------------------------------------------------------------------------------- 1 | import { fetchRemoteTextByLine } from './fetch-text-by-line'; 2 | import { processLine } from './process-line'; 3 | 4 | import { bench, run } from 'mitata'; 5 | 6 | (async () => { 7 | const data = await Array.fromAsync(await fetchRemoteTextByLine('https://filters.adtidy.org/extension/ublock/filters/3_optimized.txt', false)); 8 | 9 | bench('processLine', () => data.forEach(processLine)); 10 | 11 | run(); 12 | })(); 13 | -------------------------------------------------------------------------------- /Mock/addthis-com_addthis_widget.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const noopfn = function () { 5 | // noop 6 | }; 7 | window.addthis = { 8 | addEventListener: noopfn, 9 | button: noopfn, 10 | counter: noopfn, 11 | init: noopfn, 12 | layers: noopfn, 13 | ready: noopfn, 14 | sharecounters: { 15 | getShareCounts: noopfn 16 | }, 17 | toolbox: noopfn, 18 | update: noopfn 19 | }; 20 | }()); 21 | -------------------------------------------------------------------------------- /Modules/sukka_disable_netease_music_v2_update_check.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=[Sukka] Disable NetEase Music Mac v2 Update Check 2 | #!desc=禁用网易云音乐 Mac 版 v2 更新提示 3 | 4 | [Rule] 5 | URL-REGEX,https?://music\.163\.com/eapi/mac/upgrade/get,REJECT 6 | URL-REGEX,https?://music\.163\.com/eapi/osx/version,REJECT 7 | URL-REGEX,https?://music\.163\.com/eapi/cdns,REJECT 8 | URL-REGEX,https?://music\.163\.com/eapi/push/init,REJECT 9 | 10 | [MITM] 11 | hostname = %APPEND% music.163.com 12 | -------------------------------------------------------------------------------- /Source/non_ip/my_proxy.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Proxy 2 | 3 | DOMAIN,ip.cn 4 | DOMAIN-SUFFIX,ip.sb 5 | DOMAIN-SUFFIX,ip-api.com 6 | DOMAIN-SUFFIX,ipdata.co 7 | DOMAIN-SUFFIX,ipwho.is 8 | DOMAIN-SUFFIX,ipregistry.co 9 | DOMAIN-SUFFIX,ipgeolocation.io 10 | DOMAIN-SUFFIX,db-ip.com 11 | DOMAIN-SUFFIX,ipstack.com 12 | 13 | DOMAIN-SUFFIX,nextdns.io 14 | DOMAIN-SUFFIX,services.googleapis.cn 15 | 16 | # Prevents pollution 17 | DOMAIN,fonts.gstatic.cn 18 | DOMAIN,fonts.googleapis.cn 19 | -------------------------------------------------------------------------------- /Source/non_ip/cloudmounter.ts: -------------------------------------------------------------------------------- 1 | export const DOMAINS = [ 2 | 'DOMAIN-SUFFIX,sharepoint.com', 3 | 'DOMAIN-SUFFIX,graph.microsoft.com', 4 | 'DOMAIN,www.googleapis.com', 5 | 'DOMAIN,api.onedrive.com', 6 | 'DOMAIN-SUFFIX,storage.live.com', 7 | 'DOMAIN-SUFFIX,files.1drv.com', 8 | 'DOMAIN-SUFFIX,my.microsoftpersonalcontent.com', 9 | 'DOMAIN-WILDCARD,*-medi*.svc.ms', 10 | 'DOMAIN-SUFFIX,upload.box.com' 11 | ]; 12 | 13 | export const PROCESS_NAMES = [ 14 | '*CloudMounter' 15 | // 'RaiDrive' 16 | ]; 17 | -------------------------------------------------------------------------------- /Source/ip/apple_services.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Apple IP 2 | # $ meta_description This file contains IPs owned/used by Apple, Inc. 3 | 4 | IP-CIDR,17.0.0.0/8,no-resolve 5 | IP-CIDR,63.92.224.0/19,no-resolve 6 | IP-CIDR,65.199.22.0/23,no-resolve 7 | IP-CIDR,139.178.128.0/18,no-resolve 8 | IP-CIDR,144.178.0.0/19,no-resolve 9 | IP-CIDR,144.178.36.0/22,no-resolve 10 | IP-CIDR,144.178.48.0/20,no-resolve 11 | IP-CIDR,192.35.50.0/24,no-resolve 12 | IP-CIDR,198.183.17.0/24,no-resolve 13 | IP-CIDR,205.180.175.0/24,no-resolve 14 | -------------------------------------------------------------------------------- /Source/non_ip/apple_cn.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Apple China Special Domains 2 | # $ meta_description This file contains domains of Apple, Inc that have host service specific for the Mainland China. 3 | 4 | DOMAIN-SUFFIX,cn.apple.com 5 | DOMAIN-SUFFIX,apple.com.cn 6 | DOMAIN,api.smoot.apple.cn 7 | DOMAIN-SUFFIX,cn.ls.apple.com 8 | DOMAIN-SUFFIX,cn-ssl.ls.apple.com 9 | DOMAIN,gs-loc-cn.apple.com 10 | DOMAIN-SUFFIX,icloud.com.cn 11 | DOMAIN-SUFFIX,gspe19-cn-ssl.ls.apple.com 12 | DOMAIN,appleintelligencefeedback.care.apple.com 13 | -------------------------------------------------------------------------------- /Source/ip/telegram_asn.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Telegram ASN 2 | # $ meta_description This file contains ASN owned/used by Telegram Messenger. 3 | # $ meta_description Note that unlike "ip/telegram" file which is based on officially released list by Telegram themselves. Use this file at your own risk. 4 | 5 | # Telegram DC2/DC4 - Amsterdam 6 | IP-ASN,62041 7 | # Telegram DC5 - Singapore 8 | IP-ASN,62014 9 | # Telegram DC1/DC3 - Miami 10 | IP-ASN,59930 11 | # Telegram India CDN 12 | IP-ASN,44907 13 | # Telegram Finland, RETN Transit Only 14 | IP-ASN,211157 15 | -------------------------------------------------------------------------------- /Source/non_ip/telegram.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Telegram Domains 2 | # $ meta_description This file contains domains used by Telegram Messenger. 3 | 4 | DOMAIN-SUFFIX,t.me 5 | DOMAIN-SUFFIX,tx.me 6 | DOMAIN-SUFFIX,tg.dev 7 | DOMAIN-SUFFIX,tdesktop.com 8 | DOMAIN-SUFFIX,telegra.ph 9 | DOMAIN-SUFFIX,telega.one 10 | DOMAIN-SUFFIX,telegram.me 11 | DOMAIN-SUFFIX,telegram.org 12 | DOMAIN-SUFFIX,telegram.dog 13 | DOMAIN-SUFFIX,telegram.space 14 | DOMAIN-SUFFIX,graph.org 15 | DOMAIN-SUFFIX,legra.ph 16 | DOMAIN-SUFFIX,telesco.pe 17 | DOMAIN-SUFFIX,cdn-telegram.org 18 | -------------------------------------------------------------------------------- /Build/constants/description.ts: -------------------------------------------------------------------------------- 1 | export function createFileDescription(license = 'AGPL 3.0') { 2 | return [ 3 | `License: ${license}`, 4 | 'Homepage: https://ruleset.skk.moe', 5 | 'GitHub: https://github.com/SukkaW/Surge' 6 | ]; 7 | } 8 | 9 | export const SHARED_DESCRIPTION = createFileDescription('AGPL 3.0'); 10 | 11 | // this_ruleset_is_made_by_sukkaw.ruleset.skk.moe 12 | // th1s_rule5et_1s_m4d3_by_5ukk4w_ruleset.skk.moe 13 | // 7h1s_rul35et_i5_mad3_by_5ukk4w-ruleset.skk.moe 14 | export const MARKER_DOMAIN = '7h1s_rul35et_i5_mad3_by_5ukk4w-ruleset.skk.moe'; 15 | -------------------------------------------------------------------------------- /Source/non_ip/download.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Large Files Hosting IPs 2 | # $ meta_description This file contains ruleset for software updating & large file hosting. 3 | 4 | DOMAIN-WILDCARD,download*.mediafire.com 5 | DOMAIN-WILDCARD,ftp.??.debian.org 6 | DOMAIN-WILDCARD,ftp.??.freebsd.org 7 | 8 | # >> BackBlaze B2 9 | DOMAIN-WILDCARD,*.s3.*.backblazeb2.com 10 | 11 | # >> CoMaps.app 12 | DOMAIN-WILDCARD,cdn-*.comaps.app 13 | DOMAIN-WILDCARD,cdn-*.comaps.tech 14 | DOMAIN-WILDCARD,cdn-*.organicmaps.app 15 | 16 | # >> HTTP Only 17 | URL-REGEX,^http://(.+)\.gvt\d\.com/edgedl 18 | -------------------------------------------------------------------------------- /Source/non_ip/sogouinput.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Sogou Input 2 | # $ meta_description This file contains rules for Sogou Input. 3 | 4 | PROCESS-NAME,SogouInput 5 | PROCESS-NAME,SogouTaskManager 6 | PROCESS-NAME,SogouServices 7 | USER-AGENT,SogouInput 8 | USER-AGENT,com.sogou.sogouinput.BaseKeyboard 9 | DOMAIN-SUFFIX,get.sogou.com 10 | DOMAIN-SUFFIX,shouji.sogou.com 11 | DOMAIN-SUFFIX,account.sogou.com 12 | DOMAIN-SUFFIX,pinyin.sogou.com 13 | DOMAIN-SUFFIX,ime.sogou.com 14 | DOMAIN-SUFFIX,macime.sogou.com 15 | DOMAIN-SUFFIX,sginput.qq.com 16 | DOMAIN-SUFFIX,beacon.qq.com 17 | DOMAIN,oth.str.mdt.qq.com 18 | -------------------------------------------------------------------------------- /Source/ip/download.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Large Files Hosting IPs 2 | # $ meta_description This file contains IPs for software updating & large file hosting. 3 | 4 | # >> MEGA 5 | 6 | IP-CIDR,185.206.24.0/22,no-resolve 7 | IP-CIDR,66.203.126.0/23,no-resolve 8 | IP-CIDR,94.24.36.0/23,no-resolve 9 | IP-CIDR,162.208.16.0/24,no-resolve 10 | IP-CIDR6,2a0b:e40:1::/48,no-resolve 11 | IP-CIDR6,2a0b:e40:2::/47,no-resolve 12 | IP-CIDR6,2a0b:e41:1::/48,no-resolve 13 | IP-CIDR6,2a0b:e42:1::/48,no-resolve 14 | IP-CIDR6,2a0b:e43:1::/48,no-resolve 15 | IP-CIDR6,2a0b:e44:1::/48,no-resolve 16 | IP-CIDR6,2a0b:e45:1::/48,no-resolve 17 | -------------------------------------------------------------------------------- /Build/lib/process-line.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { processLine } from './process-line'; 4 | import { expect } from 'expect'; 5 | 6 | describe('processLine', () => { 7 | ([ 8 | ['! comment', null], 9 | [' ! comment', null], 10 | ['// xommwnr', null], 11 | ['# comment', null], 12 | [' # comment', null], 13 | ['###id', '###id'], 14 | ['##.class', '##.class'], 15 | ['## EOF', '## EOF'], 16 | ['##### EOF', null] 17 | ] as const).forEach(([input, expected]) => { 18 | it(input, () => { 19 | expect(processLine(input)).toBe(expected); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /Source/non_ip/my_plus.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Internal Special 2 | 3 | # Misc 4 | DOMAIN-SUFFIX,azurefd.net 5 | DOMAIN-SUFFIX,vo.msecnd.net 6 | 7 | DOMAIN-SUFFIX,18comic.vip 8 | DOMAIN-SUFFIX,18comic.org 9 | 10 | DOMAIN-SUFFIX,sososo.io 11 | 12 | DOMAIN,simpcraft.com 13 | 14 | DOMAIN-SUFFIX,hentaiverse.org 15 | 16 | # SKK.MOE 17 | DOMAIN,hv-monster-submit.skk.moe 18 | # Wakatime 19 | DOMAIN,api.wakatime.com 20 | # Mintlify 21 | DOMAIN,writer-api.mintlify.com 22 | # Testing CDN Domains 23 | DOMAIN-SUFFIX,kxcdn.com 24 | DOMAIN-SUFFIX,akamaihd.net 25 | DOMAIN-SUFFIX,akamaized.net 26 | DOMAIN-SUFFIX,akamai-content-networks.com 27 | -------------------------------------------------------------------------------- /Build/constants/microsoft-cdn.ts: -------------------------------------------------------------------------------- 1 | export const PROBE_DOMAINS = ['.microsoft.com', '.windows.net', '.windows.com', '.windowsupdate.com', '.windowssearch.com', '.office.net']; 2 | 3 | export const RULES = [ 4 | // Microsoft OCSP (HTTP Only) 5 | String.raw`URL-REGEX,^http://www\.microsoft\.com/pki/`, 6 | String.raw`URL-REGEX,^http://www\.microsoft\.com/pkiops/` 7 | ]; 8 | 9 | export const DOMAINS = [ 10 | 'res.cdn.office.net', 11 | 'res-1.cdn.office.net', 12 | 'res-h3.public.cdn.office.net', 13 | 'statics.teams.cdn.office.net' 14 | ]; 15 | 16 | export const DOMAIN_SUFFIXES = []; 17 | 18 | export const BLACKLIST = [ 19 | 'www.microsoft.com', 20 | 'windowsupdate.com' 21 | ]; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["ESNext", "ESNext.Iterator"], 5 | "module": "nodenext", 6 | "moduleResolution": "nodenext", 7 | "allowImportingTsExtensions": true, 8 | "allowJs": true, 9 | "noEmit": true, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "strict": true, 14 | "strictNullChecks": true, 15 | "skipLibCheck": true 16 | }, 17 | "include": [ 18 | "./Source/**/*.js", 19 | "./Build/**/*.ts", 20 | "./Source/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "public" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /Build/build-reject-ip-list.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import path from 'node:path'; 3 | import { task } from './trace'; 4 | import { compareAndWriteFile } from './lib/create-file'; 5 | import { OUTPUT_INTERNAL_DIR } from './constants/dir'; 6 | import { AUGUST_ASN, HUIZE_ASN } from '../Source/ip/badboy_asn'; 7 | 8 | // Notice: botnet and bogus_nxdomain has been moved to build-reject-domainset 9 | export const buildRejectIPList = task(require.main === module, __filename)(async (span) => Promise.all([ 10 | compareAndWriteFile(span, [AUGUST_ASN.join(' ')], path.join(OUTPUT_INTERNAL_DIR, 'august_asn.txt')), 11 | compareAndWriteFile(span, [HUIZE_ASN.join(' ')], path.join(OUTPUT_INTERNAL_DIR, 'huize_asn.txt')) 12 | ])); 13 | -------------------------------------------------------------------------------- /Build/lib/fetch-text-by-line.bench.ts: -------------------------------------------------------------------------------- 1 | import { readFileByLine/* , readFileByLineNew */ } from './fetch-text-by-line'; 2 | import path from 'node:path'; 3 | import fsp from 'node:fs/promises'; 4 | import { OUTPUT_SURGE_DIR } from '../constants/dir'; 5 | 6 | const file = path.join(OUTPUT_SURGE_DIR, 'domainset/reject_extra.conf'); 7 | 8 | (async () => { 9 | const { bench, group, run } = await import('mitata'); 10 | 11 | group(() => { 12 | bench('readFileByLine', () => Array.fromAsync(readFileByLine(file))); 13 | // bench('readFileByLineNew', async () => Array.fromAsync(await readFileByLineNew(file))); 14 | bench('fsp.readFile', () => fsp.readFile(file, 'utf-8').then((content) => content.split('\n'))); 15 | }); 16 | 17 | run(); 18 | })(); 19 | -------------------------------------------------------------------------------- /Source/non_ip/apple_intelligence.conf: -------------------------------------------------------------------------------- 1 | 2 | # $ meta_title Sukka's Ruleset - Apple Intelligence 3 | # $ meta_description This file contains domains of OpenAI, Claude. 4 | 5 | # >> Apple Relay 6 | # Sadly, even though Oblivious HTTP Relay is an open standard, supposed to enhance privacy. Apple uses a loophole 7 | # to get user real request country and IP address from third-party CDN providers (like Cloudflare or Fastly) to 8 | # blocks country from accessing Apple Intelligence services. 9 | DOMAIN,apple-relay.fastly-edge.com 10 | DOMAIN,apple-relay.cloudflare.com 11 | DOMAIN,apple-relay.apple.com 12 | DOMAIN,cp4.cloudflare.com 13 | 14 | # >> com.apple.geod 15 | DOMAIN,gspe1-ssl.ls.apple.com 16 | # TODO: find other domains that com.apple.geod is trying to connect to. 17 | -------------------------------------------------------------------------------- /Source/ip/badboy_asn.ts: -------------------------------------------------------------------------------- 1 | // Internet Neutrality doesn't apply to those who are actively breaking it. 2 | export const AUGUST_ASN = [ 3 | '1012', // Moe BGP, peers with both AS40111 and AS945 4 | '945', // hkgo LLC, peers with AS1012 5 | '401111', // peers with both AS1012 and AS945 6 | '62853', // hkgo LLC, peers with AS945 7 | '7719', // hkgo LLC 8 | '54625', // August, peers with AS945 9 | '7257', // only upstream is AS945 10 | '18044', // only upstream is AS945 11 | '62489', // only upstream is AS945 and AS1012 12 | '5111', // exists in AS-WAKUWAKU 13 | '14651', // only upstream is AS945 14 | 15 | '7480' // Friend of August, stealing others' XC at STUIX 16 | ]; 17 | 18 | export const HUIZE_ASN = [ 19 | '61302', 20 | '60539', 21 | '60842' 22 | ]; 23 | -------------------------------------------------------------------------------- /Build/lib/rules/ip.ts: -------------------------------------------------------------------------------- 1 | import type { Span } from '../../trace'; 2 | import type { BaseWriteStrategy } from '../writing-strategy/base'; 3 | import { ClashClassicRuleSet, ClashIPSet } from '../writing-strategy/clash'; 4 | import { SingboxSource } from '../writing-strategy/singbox'; 5 | import { SurgeRuleSet } from '../writing-strategy/surge'; 6 | import { FileOutput } from './base'; 7 | 8 | export class IPListOutput extends FileOutput { 9 | strategies: BaseWriteStrategy[]; 10 | 11 | constructor(span: Span, id: string, private readonly clashUseRule = true) { 12 | super(span, id); 13 | 14 | this.strategies = [ 15 | new SurgeRuleSet('ip'), 16 | this.clashUseRule ? new ClashClassicRuleSet('ip') : new ClashIPSet(), 17 | new SingboxSource('ip') 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Build/lib/writing-strategy/legacy-clash-premium.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'foxts/noop'; 2 | import { OUTPUT_LEAGCY_CLASH_PREMIUM_DIR } from '../../constants/dir'; 3 | import { ClashClassicRuleSet } from './clash'; 4 | import { MARKER_DOMAIN } from '../../constants/description'; 5 | 6 | export class LegacyClashPremiumClassicRuleSet extends ClashClassicRuleSet { 7 | public override readonly name = 'legacy clash premium classic ruleset'; 8 | 9 | readonly fileExtension = 'txt'; 10 | 11 | protected result: string[] = [`DOMAIN,${MARKER_DOMAIN}`]; 12 | 13 | constructor(public readonly type: 'ip' | 'non_ip' /* | (string & {}) */, public readonly outputDir = OUTPUT_LEAGCY_CLASH_PREMIUM_DIR) { 14 | super(type, outputDir); 15 | } 16 | 17 | override writeDomainWildcard = noop; 18 | override writeIpAsns = noop; 19 | } 20 | -------------------------------------------------------------------------------- /Build/lib/parse-dnsmasq.ts: -------------------------------------------------------------------------------- 1 | import { createReadlineInterfaceFromResponse } from './fetch-text-by-line'; 2 | 3 | import type { UndiciResponseData } from './fetch-retry'; 4 | import type { Response } from 'undici'; 5 | 6 | export function extractDomainsFromFelixDnsmasq(line: string): string | null { 7 | if (line.startsWith('server=/') && line.endsWith('/114.114.114.114')) { 8 | return line.slice(8, -16); 9 | } 10 | return null; 11 | } 12 | 13 | export async function parseFelixDnsmasqFromResp(resp: UndiciResponseData | Response): Promise { 14 | const results: string[] = []; 15 | 16 | for await (const line of createReadlineInterfaceFromResponse(resp, true)) { 17 | const domain = extractDomainsFromFelixDnsmasq(line); 18 | if (domain) { 19 | results.push(domain); 20 | } 21 | } 22 | 23 | return results; 24 | } 25 | -------------------------------------------------------------------------------- /Build/constants/loose-tldts-opt.ts: -------------------------------------------------------------------------------- 1 | import type * as tldts from 'tldts'; 2 | 3 | export const looseTldtsOpt: NonNullable[1]> = { 4 | allowPrivateDomains: false, 5 | extractHostname: false, 6 | mixedInputs: false, 7 | validateHostname: false, 8 | detectIp: false 9 | }; 10 | 11 | export const loosTldOptWithPrivateDomains: NonNullable[1]> = { 12 | ...looseTldtsOpt, 13 | allowPrivateDomains: true 14 | }; 15 | 16 | export const normalizeTldtsOpt: NonNullable[1]> = { 17 | allowPrivateDomains: true, 18 | // in normalizeDomain, we only care if it contains IP, we don't care if we need to extract it 19 | // by setting detectIp to false and manually check ip outside tldts.parse, we can skip the tldts 20 | // inner "extractHostname" call 21 | detectIp: false 22 | }; 23 | -------------------------------------------------------------------------------- /Source/ip/neteasemusic.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Netease Music IPs 2 | # $ meta_description This file contains IPs used by Netease Music. 3 | 4 | IP-CIDR,39.105.63.80/32,no-resolve 5 | IP-CIDR,45.254.48.1/32,no-resolve 6 | IP-CIDR,47.100.127.239/32,no-resolve 7 | IP-CIDR,59.111.160.195/32,no-resolve 8 | IP-CIDR,59.111.160.197/32,no-resolve 9 | IP-CIDR,59.111.181.35/32,no-resolve 10 | IP-CIDR,59.111.181.38/32,no-resolve 11 | IP-CIDR,59.111.181.60/32,no-resolve 12 | IP-CIDR,101.71.154.241/32,no-resolve 13 | IP-CIDR,103.126.92.132/32,no-resolve 14 | IP-CIDR,103.126.92.133/32,no-resolve 15 | IP-CIDR,112.13.119.17/32,no-resolve 16 | IP-CIDR,112.13.122.1/32,no-resolve 17 | IP-CIDR,115.236.118.33/32,no-resolve 18 | IP-CIDR,115.236.121.1/32,no-resolve 19 | IP-CIDR,118.24.63.156/32,no-resolve 20 | IP-CIDR,193.112.159.225/32,no-resolve 21 | IP-CIDR,223.252.199.66/32,no-resolve 22 | IP-CIDR,223.252.199.67/32,no-resolve 23 | -------------------------------------------------------------------------------- /Build/lib/cache-apply.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | 3 | export function createCache(namespace?: string, printStats = false) { 4 | const cache = new Map(); 5 | 6 | let hit = 0; 7 | if (namespace && printStats) { 8 | process.on('exit', () => { 9 | console.log(`🔋 [cache] ${namespace} hit: ${hit}, size: ${cache.size}`); 10 | }); 11 | } 12 | 13 | return { 14 | sync(key: string, fn: () => T): T { 15 | if (cache.has(key)) { 16 | hit++; 17 | return cache.get(key); 18 | } 19 | const value = fn(); 20 | cache.set(key, value); 21 | return value; 22 | }, 23 | async async(key: string, fn: () => Promise): Promise { 24 | if (cache.has(key)) { 25 | hit++; 26 | return cache.get(key); 27 | } 28 | const value = await fn(); 29 | cache.set(key, value); 30 | return value; 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /Build/lib/parse-filter/shared.ts: -------------------------------------------------------------------------------- 1 | import picocolors from 'picocolors'; 2 | import { DEBUG_DOMAIN_TO_FIND } from '../../constants/reject-data-source'; 3 | import { noop } from 'foxts/noop'; 4 | 5 | export const foundDebugDomain = { value: false }; 6 | 7 | export const onBlackFound = DEBUG_DOMAIN_TO_FIND 8 | ? (line: string, meta: string) => { 9 | if (line.includes(DEBUG_DOMAIN_TO_FIND!)) { 10 | console.warn(picocolors.red(meta), '(black)', line.replaceAll(DEBUG_DOMAIN_TO_FIND!, picocolors.bold(DEBUG_DOMAIN_TO_FIND))); 11 | foundDebugDomain.value = true; 12 | } 13 | } 14 | : noop; 15 | 16 | export const onWhiteFound = DEBUG_DOMAIN_TO_FIND 17 | ? (line: string, meta: string) => { 18 | if (line.includes(DEBUG_DOMAIN_TO_FIND!)) { 19 | console.warn(picocolors.red(meta), '(white)', line.replaceAll(DEBUG_DOMAIN_TO_FIND!, picocolors.bold(DEBUG_DOMAIN_TO_FIND))); 20 | foundDebugDomain.value = true; 21 | } 22 | } 23 | : noop; 24 | -------------------------------------------------------------------------------- /Build/lib/rules/domainset.ts: -------------------------------------------------------------------------------- 1 | import type { Span } from '../../trace'; 2 | import { AdGuardHome } from '../writing-strategy/adguardhome'; 3 | import type { BaseWriteStrategy } from '../writing-strategy/base'; 4 | import { ClashDomainSet } from '../writing-strategy/clash'; 5 | import { SingboxSource } from '../writing-strategy/singbox'; 6 | import { SurgeDomainSet } from '../writing-strategy/surge'; 7 | import { FileOutput } from './base'; 8 | 9 | export class DomainsetOutput extends FileOutput { 10 | strategies: BaseWriteStrategy[] = [ 11 | new SurgeDomainSet(), 12 | new ClashDomainSet(), 13 | new SingboxSource('domainset') 14 | ]; 15 | } 16 | 17 | export class AdGuardHomeOutput extends FileOutput { 18 | strategies: BaseWriteStrategy[]; 19 | 20 | constructor( 21 | span: Span, 22 | id: string, 23 | outputDir: string 24 | ) { 25 | super(span, id); 26 | 27 | this.strategies = [ 28 | new AdGuardHome(outputDir) 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Build/build-cloudmounter-rules.ts: -------------------------------------------------------------------------------- 1 | import { DOMAINS, PROCESS_NAMES } from '../Source/non_ip/cloudmounter'; 2 | import { SHARED_DESCRIPTION } from './constants/description'; 3 | import { task } from './trace'; 4 | import { RulesetOutput } from './lib/rules/ruleset'; 5 | 6 | export const buildCloudMounterRules = task(require.main === module, __filename)(async (span) => { 7 | // AND,((SRC-IP,192.168.1.110), (DOMAIN, example.com)) 8 | 9 | const results = DOMAINS.flatMap(domain => PROCESS_NAMES.flatMap(process => [ 10 | `AND,((${domain}),(PROCESS-NAME,${process}))`, 11 | ...[ 12 | '10.0.0.0/8', 13 | // '127.0.0.0/8', 14 | '172.16.0.0/12', 15 | '192.168.0.0/16' 16 | ].map(cidr => `AND,((${domain}),(SRC-IP,${cidr}))`) 17 | ])); 18 | 19 | return new RulesetOutput(span, 'cloudmounter', 'non_ip') 20 | .withTitle('Sukka\'s Ruleset - CloudMounter / RaiDrive') 21 | .withDescription(SHARED_DESCRIPTION) 22 | .addFromRuleset(results) 23 | .write(); 24 | }); 25 | -------------------------------------------------------------------------------- /Build/constants/dir.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import process from 'node:process'; 3 | 4 | export const ROOT_DIR = path.resolve(__dirname, '../..'); 5 | 6 | export const CACHE_DIR = path.resolve(ROOT_DIR, '.cache'); 7 | 8 | export const SOURCE_DIR = path.join(ROOT_DIR, 'Source'); 9 | 10 | export const PUBLIC_DIR = process.env.PUBLIC_DIR || path.resolve(ROOT_DIR, 'public'); 11 | 12 | export const OUTPUT_SURGE_DIR = path.join(PUBLIC_DIR, 'List'); 13 | export const OUTPUT_CLASH_DIR = path.resolve(PUBLIC_DIR, 'Clash'); 14 | export const OUTPUT_LEAGCY_CLASH_PREMIUM_DIR = path.resolve(PUBLIC_DIR, 'LegacyClashPremium'); 15 | export const OUTPUT_SINGBOX_DIR = path.resolve(PUBLIC_DIR, 'sing-box'); 16 | export const OUTPUT_SURFBOARD_DIR = path.resolve(PUBLIC_DIR, 'Surfboard'); 17 | export const OUTPUT_MODULES_DIR = path.resolve(PUBLIC_DIR, 'Modules'); 18 | export const OUTPUT_MODULES_RULES_DIR = path.resolve(OUTPUT_MODULES_DIR, 'Rules'); 19 | export const OUTPUT_INTERNAL_DIR = path.resolve(PUBLIC_DIR, 'Internal'); 20 | export const OUTPUT_MOCK_DIR = path.resolve(PUBLIC_DIR, 'Mock'); 21 | -------------------------------------------------------------------------------- /Build/lib/writing-strategy/surfboard.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'foxts/noop'; 2 | import { SurgeRuleSet } from './surge'; 3 | import { OUTPUT_SURFBOARD_DIR } from '../../constants/dir'; 4 | import { appendSetElementsToArray } from 'foxts/append-set-elements-to-array'; 5 | import { MARKER_DOMAIN } from '../../constants/description'; 6 | 7 | export class SurfboardRuleSet extends SurgeRuleSet { 8 | public override readonly name: string = 'surfboard for android ruleset'; 9 | 10 | protected result: string[] = [`DOMAIN,${MARKER_DOMAIN}`]; 11 | constructor(public readonly type: 'ip' | 'non_ip' /* | (string & {}) */, public readonly outputDir = OUTPUT_SURFBOARD_DIR) { 12 | super(type, outputDir); 13 | } 14 | 15 | override writeDomainWildcard = noop; 16 | override writeUserAgents = noop; 17 | override writeUrlRegexes = noop; 18 | override writeIpAsns = noop; 19 | 20 | override writeSourcePorts(port: Set): void { 21 | // https://getsurfboard.com/docs/profile-format/rule/misc 22 | appendSetElementsToArray(this.result, port, i => `IN-PORT,${i}`); 23 | } 24 | 25 | override writeOtherRules = noop; 26 | } 27 | -------------------------------------------------------------------------------- /Source/non_ip/reject-drop.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Reject And Drop 2 | # $ meta_description This file This file contains rules for domain should be used with REJECT-DROP policy. 3 | 4 | DOMAIN-SUFFIX,geo2.adobe.com 5 | 6 | # VSCode GitHub Copilot plugin sends telemetry going haywire, at 2 req/s 7 | # Let's Surge drop the packets to prevent flooding 8 | # https://github.com/microsoft/vscode-copilot-release/issues/1496#issuecomment-2422464393 9 | DOMAIN-SUFFIX,in.applicationinsights.azure.com 10 | 11 | # Slidepad storming telemetry 12 | DOMAIN,nom.telemetrydeck.com 13 | 14 | # Sogou Input storming telemetry 15 | DOMAIN-SUFFIX,beacon.qq.com 16 | 17 | # Weixin/Tencent storming telemetry 18 | DOMAIN-SUFFIX,trace.qq.com 19 | 20 | # Xiaomi Home storming telemetry 21 | DOMAIN-SUFFIX,tracking.miui.com 22 | DOMAIN-SUFFIX,tracking.intl.miui.com 23 | DOMAIN,stat.youpin.mi.com 24 | DOMAIN-SUFFIX,jpush.cn 25 | DOMAIN-SUFFIX,jpush.io 26 | DOMAIN-SUFFIX,getui.net 27 | DOMAIN-SUFFIX,getui.com 28 | DOMAIN-SUFFIX,gepush.com 29 | DOMAIN-SUFFIX,easytomessage.com 30 | 31 | # Grammarly Telemetry 32 | DOMAIN-SUFFIX,f-log-extension.grammarly.io 33 | 34 | DOMAIN-SUFFIX,undefined 35 | DOMAIN-SUFFIX,null 36 | -------------------------------------------------------------------------------- /Build/build-apple-cdn.ts: -------------------------------------------------------------------------------- 1 | import { parseFelixDnsmasqFromResp } from './lib/parse-dnsmasq'; 2 | import { task } from './trace'; 3 | import { SHARED_DESCRIPTION } from './constants/description'; 4 | import { once } from 'foxts/once'; 5 | import { DomainsetOutput } from './lib/rules/domainset'; 6 | import { $$fetch } from './lib/fetch-retry'; 7 | 8 | export const getAppleCdnDomainsPromise = once(() => $$fetch('https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/apple.china.conf').then(parseFelixDnsmasqFromResp)); 9 | 10 | export const buildAppleCdn = task(require.main === module, __filename)(async (span) => { 11 | const res: string[] = await span.traceChildPromise('get apple cdn domains', getAppleCdnDomainsPromise()); 12 | 13 | return new DomainsetOutput(span, 'apple_cdn') 14 | .withTitle('Sukka\'s Ruleset - Apple CDN') 15 | .appendDescription(SHARED_DESCRIPTION) 16 | .appendDescription( 17 | '', 18 | 'This file contains Apple\'s domains using their China mainland CDN servers.', 19 | '', 20 | 'Data from:', 21 | ' - https://github.com/felixonmars/dnsmasq-china-list' 22 | ) 23 | .bulkAddDomainSuffix(res) 24 | .write(); 25 | }); 26 | -------------------------------------------------------------------------------- /Build/lib/tldts.bench.ts: -------------------------------------------------------------------------------- 1 | import { fetchRemoteTextByLine } from './fetch-text-by-line'; 2 | 3 | import { bench, group, run } from 'mitata'; 4 | 5 | import * as tldts from 'tldts'; 6 | import * as tldtsExperimental from 'tldts-experimental'; 7 | 8 | (async () => { 9 | const data = await Array.fromAsync(await fetchRemoteTextByLine('https://phishing.army/download/phishing_army_blocklist.txt', true)); 10 | 11 | const tldtsOpt: Parameters[1] = { 12 | allowPrivateDomains: false, 13 | extractHostname: false, 14 | validateHostname: false, 15 | detectIp: false, 16 | mixedInputs: false 17 | }; 18 | 19 | (['getDomain', 'getPublicSuffix', 'getSubdomain', 'parse'] as const).forEach(methodName => { 20 | group(() => { 21 | bench('tldts - ' + methodName, () => { 22 | for (let i = 0, len = data.length; i < len; i++) { 23 | tldts[methodName](data[i], tldtsOpt); 24 | } 25 | }); 26 | 27 | bench('tldts-experimental - ' + methodName, () => { 28 | for (let i = 0, len = data.length; i < len; i++) { 29 | tldtsExperimental[methodName](data[i], tldtsOpt); 30 | } 31 | }); 32 | }); 33 | }); 34 | 35 | return run(); 36 | })(); 37 | -------------------------------------------------------------------------------- /Mock/widgets-outbrain-com_outbrain.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const noopfn = () => { 5 | // noop 6 | }; 7 | const obr = {}; 8 | const methods = [ 9 | 'callClick', 10 | 'callLoadMore', 11 | 'callRecs', 12 | 'callUserZapping', 13 | 'callWhatIs', 14 | 'cancelRecommendation', 15 | 'cancelRecs', 16 | 'closeCard', 17 | 'closeModal', 18 | 'closeTbx', 19 | 'errorInjectionHandler', 20 | 'getCountOfRecs', 21 | 'getStat', 22 | 'imageError', 23 | 'manualVideoClicked', 24 | 'onOdbReturn', 25 | 'onVideoClick', 26 | 'pagerLoad', 27 | 'recClicked', 28 | 'refreshSpecificWidget', 29 | 'renderSpaWidgets', 30 | 'refreshWidget', 31 | 'reloadWidget', 32 | 'researchWidget', 33 | 'returnedError', 34 | 'returnedHtmlData', 35 | 'returnedIrdData', 36 | 'returnedJsonData', 37 | 'scrollLoad', 38 | 'showDescription', 39 | 'showRecInIframe', 40 | 'userZappingMessage', 41 | 'zappingFormAction' 42 | ]; 43 | obr.extern = { 44 | video: { 45 | getVideoRecs: noopfn, 46 | videoClicked: noopfn 47 | } 48 | }; 49 | methods.forEach((a) => { 50 | obr.extern[a] = noopfn; 51 | }); 52 | window.OBR = obr; 53 | }()); 54 | -------------------------------------------------------------------------------- /Build/build-telegram-cidr.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { task } from './trace'; 3 | import { SHARED_DESCRIPTION } from './constants/description'; 4 | import { RulesetOutput } from './lib/rules/ruleset'; 5 | import { getTelegramCIDRPromise } from './lib/get-telegram-backup-ip'; 6 | 7 | export const buildTelegramCIDR = task(require.main === module, __filename)(async (span) => { 8 | const { timestamp, ipcidr, ipcidr6 } = await span.traceChildAsync('get telegram cidr', getTelegramCIDRPromise); 9 | 10 | if (ipcidr.length + ipcidr6.length === 0) { 11 | throw new Error('Failed to fetch data!'); 12 | } 13 | 14 | const description = [ 15 | ...SHARED_DESCRIPTION, 16 | 'Data from:', 17 | ' - https://core.telegram.org/resources/cidr.txt' 18 | ]; 19 | 20 | return new RulesetOutput(span, 'telegram', 'ip') 21 | .withTitle('Sukka\'s Ruleset - Telegram IP CIDR') 22 | .withDescription(description) 23 | // .withDate(date) // With extra data source, we no longer use last-modified for file date 24 | .appendDataSource( 25 | 'https://core.telegram.org/resources/cidr.txt (last updated: ' + new Date(timestamp).toISOString() + ')' 26 | ) 27 | .bulkAddCIDR4NoResolve(ipcidr) 28 | .bulkAddCIDR6NoResolve(ipcidr6) 29 | .write(); 30 | }); 31 | -------------------------------------------------------------------------------- /Build/validate-global-tld.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { HostnameSmolTrie } from './lib/trie'; 3 | import { OUTPUT_SURGE_DIR } from './constants/dir'; 4 | import { ICP_TLD } from './constants/domains'; 5 | import tldts from 'tldts-experimental'; 6 | import { looseTldtsOpt } from './constants/loose-tldts-opt'; 7 | import runAgainstSourceFile from './lib/run-against-source-file'; 8 | import { MARKER_DOMAIN } from './constants/description'; 9 | 10 | (async () => { 11 | const trie = new HostnameSmolTrie(); 12 | const extraWhiteTLDs = new Set(); 13 | 14 | await runAgainstSourceFile(path.join(OUTPUT_SURGE_DIR, 'non_ip', 'domestic.conf'), (domain) => { 15 | if (domain === MARKER_DOMAIN) { 16 | return; 17 | } 18 | const tld = tldts.getPublicSuffix(domain, looseTldtsOpt); 19 | if (tld) { 20 | extraWhiteTLDs.add(tld); 21 | } 22 | }, 'ruleset'); 23 | 24 | await runAgainstSourceFile(path.join(OUTPUT_SURGE_DIR, 'non_ip', 'global.conf'), (domain, includeAllSubDomain) => { 25 | trie.add(domain, includeAllSubDomain); 26 | }, 'ruleset'); 27 | 28 | ICP_TLD.forEach(tld => trie.whitelist(tld, true)); 29 | extraWhiteTLDs.forEach(tld => trie.whitelist(tld, true)); 30 | 31 | console.log(trie.dump().join('\n')); 32 | })(); 33 | -------------------------------------------------------------------------------- /Build/trim-source.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fsp from 'node:fs/promises'; 3 | import { fdir as Fdir } from 'fdir'; 4 | import { readFileByLine } from './lib/fetch-text-by-line'; 5 | import { SOURCE_DIR } from './constants/dir'; 6 | 7 | (async () => { 8 | const promises: Array> = []; 9 | 10 | const paths = await new Fdir() 11 | .withFullPaths() 12 | // .exclude((dirName, dirPath) => { 13 | // if (dirName === 'domainset' || dirName === 'ip' || dirName === 'non_ip') { 14 | // return false; 15 | // } 16 | // console.error(picocolors.red(`[build-comman] Unknown dir: ${dirPath}`)); 17 | // return true; 18 | // }) 19 | .filter((filepath, isDirectory) => { 20 | if (isDirectory) return true; 21 | 22 | const extname = path.extname(filepath); 23 | 24 | return extname !== '.js' && extname !== '.ts'; 25 | }) 26 | .crawl(SOURCE_DIR) 27 | .withPromise(); 28 | 29 | for (let i = 0, len = paths.length; i < len; i++) { 30 | const fullPath = paths[i]; 31 | promises.push(trimFileLines(fullPath)); 32 | } 33 | 34 | return Promise.all(promises); 35 | })(); 36 | 37 | async function trimFileLines(file: string) { 38 | let result = ''; 39 | for await (const line of readFileByLine(file)) { 40 | result += line.trim() + '\n'; 41 | } 42 | 43 | return fsp.writeFile(file, result); 44 | } 45 | -------------------------------------------------------------------------------- /Build/validate-reject-stats.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { OUTPUT_SURGE_DIR } from './constants/dir'; 3 | import tldts from 'tldts-experimental'; 4 | import { loosTldOptWithPrivateDomains } from './constants/loose-tldts-opt'; 5 | import runAgainstSourceFile from './lib/run-against-source-file'; 6 | 7 | (async () => { 8 | const rejectDomainCountMap = new Map(); 9 | const rejectExtraDomainCountMap = new Map(); 10 | 11 | const callback = (map: Map) => (domain: string) => { 12 | const apexDomain = tldts.getDomain(domain, loosTldOptWithPrivateDomains); 13 | if (!apexDomain) { 14 | return; 15 | } 16 | 17 | map.set( 18 | apexDomain, 19 | map.has(apexDomain) 20 | ? map.get(apexDomain)! + 1 21 | : 1 22 | ); 23 | }; 24 | 25 | await runAgainstSourceFile( 26 | path.join(OUTPUT_SURGE_DIR, 'domainset', 'reject.conf'), 27 | callback(rejectDomainCountMap) 28 | ); 29 | await runAgainstSourceFile( 30 | path.join(OUTPUT_SURGE_DIR, 'domainset', 'reject_extra.conf'), 31 | callback(rejectExtraDomainCountMap) 32 | ); 33 | 34 | const rejectDomainCountArr = Array.from(rejectDomainCountMap).sort((a, b) => b[1] - a[1]).filter(([, count]) => count > 20); 35 | const rejectExtraDomainCountArr = Array.from(rejectExtraDomainCountMap).sort((a, b) => b[1] - a[1]).filter(([, count]) => count > 20); 36 | 37 | console.table(rejectDomainCountArr); 38 | console.table(rejectExtraDomainCountArr); 39 | })(); 40 | -------------------------------------------------------------------------------- /Build/validate-hash-collision-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop -- no concurrent */ 2 | import { fdir as Fdir } from 'fdir'; 3 | import { OUTPUT_SURGE_DIR } from './constants/dir'; 4 | import path from 'node:path'; 5 | import { readFileIntoProcessedArray } from './lib/fetch-text-by-line'; 6 | import { xxhash3 } from 'hash-wasm'; 7 | 8 | (async () => { 9 | const hashMap = new Map>(); 10 | 11 | const runHash = async (inputs: string[]) => { 12 | for (const input of inputs) { 13 | const hash = await xxhash3(input); 14 | if (!hashMap.has(hash)) { 15 | hashMap.set(hash, new Set()); 16 | } 17 | hashMap.get(hash)!.add(input); 18 | } 19 | }; 20 | 21 | const files = await new Fdir() 22 | .withRelativePaths() 23 | .crawl(OUTPUT_SURGE_DIR) 24 | .withPromise(); 25 | 26 | for (const file of files) { 27 | const fullpath = path.join(OUTPUT_SURGE_DIR, file); 28 | if (file.startsWith('domainset' + path.sep)) { 29 | await runHash((await readFileIntoProcessedArray(fullpath)).map(i => (i[0] === '.' ? i.slice(1) : i))); 30 | } else if (file.startsWith('non_ip' + path.sep)) { 31 | await runHash((await readFileIntoProcessedArray(fullpath)).map(i => i.split(',')[1])); 32 | } 33 | } 34 | 35 | console.log(hashMap.size); 36 | let collision = 0; 37 | hashMap.forEach((v, k) => { 38 | if (v.size > 1) { 39 | collision++; 40 | console.log(k, '=>', v); 41 | } 42 | }); 43 | if (collision === 0) { 44 | console.log(hashMap); 45 | } 46 | })(); 47 | -------------------------------------------------------------------------------- /Build/build-global-server-dns-mapping.ts: -------------------------------------------------------------------------------- 1 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 2 | import { GLOBAL } from '../Source/non_ip/global'; 3 | import { createGetDnsMappingRule } from './build-domestic-direct-lan-ruleset-dns-mapping-module'; 4 | import { SOURCE_DIR } from './constants/dir'; 5 | import { task } from './trace'; 6 | import { once } from 'foxts/once'; 7 | import path from 'node:path'; 8 | import { readFileIntoProcessedArray } from './lib/fetch-text-by-line'; 9 | import { SHARED_DESCRIPTION } from './constants/description'; 10 | import { RulesetOutput } from './lib/rules/ruleset'; 11 | 12 | export const getGlobalRulesetPromise = once(async () => { 13 | const globals = await readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/global.conf')); 14 | const getDnsMappingRuleWithWildcard = createGetDnsMappingRule(true); 15 | 16 | [GLOBAL].forEach((item) => { 17 | Object.values(item).forEach(({ domains }) => { 18 | appendArrayInPlace(globals, domains.flatMap(getDnsMappingRuleWithWildcard)); 19 | }); 20 | }); 21 | 22 | return [globals] as const; 23 | }); 24 | 25 | export const buildGlobalRuleset = task(require.main === module, __filename)(async (span) => { 26 | const [globals] = await getGlobalRulesetPromise(); 27 | return new RulesetOutput(span, 'global', 'non_ip') 28 | .withTitle('Sukka\'s Ruleset - General Global Services') 29 | .appendDescription( 30 | SHARED_DESCRIPTION, 31 | '', 32 | 'This file contains rules for services that are NOT available inside the Mainland China.' 33 | ) 34 | .addFromRuleset(globals) 35 | .write(); 36 | }); 37 | -------------------------------------------------------------------------------- /Build/build-chn-cidr.ts: -------------------------------------------------------------------------------- 1 | import { fetchRemoteTextByLine } from './lib/fetch-text-by-line'; 2 | import { task } from './trace'; 3 | 4 | import { once } from 'foxts/once'; 5 | import { IPListOutput } from './lib/rules/ip'; 6 | import { createFileDescription } from './constants/description'; 7 | 8 | export const getChnCidrPromise = once(async function getChnCidr() { 9 | return Promise.all([ 10 | fetchRemoteTextByLine('https://chnroutes2.cdn.skk.moe/chnroutes.txt', true).then(Array.fromAsync), 11 | fetchRemoteTextByLine('https://gaoyifan.github.io/china-operator-ip/china6.txt', true).then(Array.fromAsync) 12 | ]); 13 | }); 14 | 15 | export const buildChnCidr = task(require.main === module, __filename)(async (span) => { 16 | const [filteredCidr4, cidr6] = await span.traceChildAsync('download chnroutes2', getChnCidrPromise); 17 | 18 | // Can not use SHARED_DESCRIPTION here as different license 19 | const description = createFileDescription('CC BY-SA 2.0'); 20 | 21 | return Promise.all([ 22 | new IPListOutput(span, 'china_ip', false) 23 | .withTitle('Sukka\'s Ruleset - Mainland China IPv4 CIDR') 24 | .withDescription(description) 25 | .appendDataSource('https://chnroutes2.cdn.skk.moe/chnroutes.txt') 26 | .bulkAddCIDR4(filteredCidr4) 27 | .write(), 28 | new IPListOutput(span, 'china_ip_ipv6', false) 29 | .withTitle('Sukka\'s Ruleset - Mainland China IPv6 CIDR') 30 | .withDescription(description) 31 | .appendDataSource( 32 | 'https://github.com/gaoyifan/china-operator-ip' 33 | ) 34 | .bulkAddCIDR6(cidr6) 35 | .write() 36 | ]); 37 | }); 38 | -------------------------------------------------------------------------------- /Build/lib/parse-filter/domainlists.ts: -------------------------------------------------------------------------------- 1 | import { fastNormalizeDomain, fastNormalizeDomainWithoutWww } from '../normalize-domain'; 2 | import { onBlackFound } from './shared'; 3 | import { fetchAssets } from '../fetch-assets'; 4 | import type { Span } from '../../trace'; 5 | 6 | function domainListLineCb(line: string, set: string[], meta: string, normalizeDomain = fastNormalizeDomain) { 7 | const domain = normalizeDomain(line); 8 | if (!domain) return; 9 | 10 | onBlackFound(domain, meta); 11 | 12 | set.push(domain); 13 | } 14 | 15 | function domainListLineCbIncludeAllSubdomain(line: string, set: string[], meta: string, normalizeDomain = fastNormalizeDomain) { 16 | const domain = normalizeDomain(line); 17 | if (!domain) return; 18 | 19 | onBlackFound(domain, meta); 20 | 21 | set.push('.' + domain); 22 | } 23 | export function processDomainListsWithPreload( 24 | domainListsUrl: string, mirrors: string[] | null, 25 | includeAllSubDomain = false, 26 | allowEmptyRemote = false 27 | ) { 28 | const downloadPromise = fetchAssets(domainListsUrl, mirrors, true, allowEmptyRemote); 29 | const lineCb = includeAllSubDomain ? domainListLineCbIncludeAllSubdomain : domainListLineCb; 30 | 31 | return (span: Span) => span.traceChildAsync(`process domainlist: ${domainListsUrl}`, async (span) => { 32 | const filterRules = await span.traceChildPromise('download', downloadPromise); 33 | const domainSets: string[] = []; 34 | 35 | span.traceChildSync('parse domain list', () => { 36 | for (let i = 0, len = filterRules.length; i < len; i++) { 37 | lineCb(filterRules[i], domainSets, domainListsUrl, fastNormalizeDomainWithoutWww); 38 | } 39 | }); 40 | 41 | return domainSets; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /Source/non_ip/reject-no-drop.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Reject No Drop 2 | # $ meta_description This file This file contains rules for domain should be used with REJECT-NO-DROP policy. 3 | 4 | # Block YouTube QUIC -- Surge built-in QUIC blocking 5 | 6 | # >> P2P CDN 7 | DOMAIN-SUFFIX,p2p.qq.com 8 | DOMAIN-WILDCARD,p2p*.qq.com 9 | DOMAIN-SUFFIX,p2ptun.qq.com 10 | DOMAIN-SUFFIX,p2pupdate.gamedl.qq.com 11 | DOMAIN-SUFFIX,p2pupgrade.gamedl.qq.com 12 | DOMAIN-SUFFIX,kuiniuca.com 13 | DOMAIN-SUFFIX,onethingpcs.com 14 | DOMAIN-SUFFIX,p2p.huya.com 15 | DOMAIN-WILDCARD,p2p-*.douyucdn.cn 16 | DOMAIN-WILDCARD,p2pchunk-*.douyucdn.cn 17 | DOMAIN-WILDCARD,dyp2p-*.douyucdn.cn 18 | DOMAIN-SUFFIX,p2perrorlog.douyucdn.cn 19 | DOMAIN-WILDCARD,p2plive-*.douyucdn.cn 20 | # DOMAIN-WILDCARD,p2p*-ws.douyucdn.cn 21 | DOMAIN-SUFFIX,p2plog.douyucdn.cn 22 | DOMAIN,stun.douyucdn.cn 23 | DOMAIN,stun1.douyucdn.cn 24 | DOMAIN-WILDCARD,stun*.douyucdn.cn 25 | DOMAIN-SUFFIX,pcdn.yximgs.com 26 | DOMAIN-KEYWORD,-p2p.pull.yximgs.com 27 | DOMAIN-KEYWORD,-p2p-v2.pull.yximgs.com 28 | DOMAIN-SUFFIX,pkoplink.com 29 | DOMAIN-SUFFIX,saxysec.com 30 | DOMAIN-SUFFIX,uhabo.com 31 | DOMAIN-SUFFIX,ietheivaicai.com 32 | 33 | DOMAIN,qn-cdnfile1pcdn.msstatic.com 34 | DOMAIN,livewebbs2pcdn.msstatic.com 35 | 36 | # Xycdn 37 | DOMAIN-SUFFIX,xycdn.com 38 | DOMAIN-SUFFIX,p2cdn.com 39 | # YunFan 40 | DOMAIN-SUFFIX,yfp2p.net 41 | DOMAIN-SUFFIX,yfcdn.net 42 | DOMAIN-SUFFIX,cdnnodedns.cn 43 | DOMAIN-SUFFIX,yfcloud.com 44 | # PPIO Cloud 45 | DOMAIN-SUFFIX,ppio.cloud 46 | DOMAIN-SUFFIX,nexusedgeio.com 47 | # Jiedian Zhijia 48 | DOMAIN-SUFFIX,szbdyd.com 49 | DOMAIN-SUFFIX,tianwenca.com 50 | # Bilibili 51 | DOMAIN-SUFFIX,mcdn.bilivideo.cn 52 | 53 | # >> Disable YouTube QUIC 54 | AND,((PROTOCOL,UDP), (DOMAIN-SUFFIX,googlevideo.com)) 55 | -------------------------------------------------------------------------------- /Source/non_ip/my_reject.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Reject (REJECT-DROP) 2 | 3 | # >> Tencent Lemon 4 | 5 | PROCESS-NAME,Tencent Lemon 6 | PROCESS-NAME,LemonMonitor 7 | PROCESS-NAME,LemonDaemon 8 | PROCESS-NAME,LemonAgent 9 | PROCESS-NAME,LemonService 10 | 11 | # >> Windows Update Delivery Optimization (WUDO) use 7680 port 12 | DEST-PORT,7680 13 | 14 | # >> Misc 15 | 16 | DOMAIN-KEYWORD,bahoom 17 | DOMAIN,daisydiskapp.com 18 | DOMAIN-SUFFIX,adobe.io 19 | DOMAIN-SUFFIX,adobestats.io 20 | DOMAIN-SUFFIX,genuine.autodesk.com 21 | AND,((DOMAIN-KEYWORD,genuine), (DOMAIN-KEYWORD,autodesk)) 22 | 23 | # >> Fake News Website 24 | # https://www.cool3c.com/article/239818 (true story: https://xiaomitime.com/is-xiaomi-planning-a-google-free-android-future-with-hyperos-42899/ ) 25 | DOMAIN-SUFFIX,cool3c.com 26 | 27 | DOMAIN-SUFFIX,0.0.198.in-addr.arpa 28 | 29 | # >> Misc 30 | 31 | # 美团直播 32 | DOMAIN-SUFFIX,mtvod.meituan.net 33 | DOMAIN-KEYWORD,-flv.meituan.net 34 | DOMAIN-KEYWORD,-live.meituan.net 35 | 36 | # 饿了么 清理 37 | DOMAIN-SUFFIX,microfin.ele.me 38 | DOMAIN-SUFFIX,httpizza.ele.me 39 | 40 | # Google Timeline / Location 41 | DOMAIN,userlocation.googleapis.com 42 | # https://locationhistory-pa.googleapis.com/google.internal.locationhistory.v1.LocationHistoryService/GetSettings 43 | # https://locationhistory-pa.googleapis.com/google.internal.locationhistory.v1.LocationHistoryService/ListTombstones 44 | # https://locationhistory-pa.googleapis.com/google.internal.locationhistory.v1.LocationHistoryService/UpdateDeviceMetadata 45 | DOMAIN,locationhistory-pa.googleapis.com 46 | DOMAIN,locationhistoryaggregates-pa.googleapis.com 47 | DOMAIN,locationhistoryplacedetails-pa.googleapis.com 48 | 49 | # 网易云音乐 Look 直播 50 | DOMAIN-SUFFIX,look.163.com 51 | 52 | # 小米米币 53 | DOMAIN-SUFFIX,mibi.xiaomi.com 54 | -------------------------------------------------------------------------------- /Source/non_ip/apple_services.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Apple Domains 2 | # $ meta_description This file contains domains of Apple, Inc. 3 | 4 | # >> Apple 5 | DOMAIN-SUFFIX,aaplimg.com 6 | DOMAIN-SUFFIX,apple-dns.net 7 | DOMAIN-SUFFIX,apple.co 8 | DOMAIN-SUFFIX,apple.com 9 | DOMAIN-SUFFIX,appstore.com 10 | DOMAIN-SUFFIX,cdn-apple.com 11 | # DOMAIN-SUFFIX,crashlytics.com 12 | DOMAIN-SUFFIX,icloud.com 13 | DOMAIN-SUFFIX,icloud-content.com 14 | DOMAIN-SUFFIX,me.com 15 | DOMAIN-SUFFIX,organicfruitapps.com 16 | DOMAIN-SUFFIX,apple-cloudkit.com 17 | DOMAIN-SUFFIX,apple-mapkit.com 18 | DOMAIN-SUFFIX,appsto.re 19 | DOMAIN-SUFFIX,itunes.com 20 | DOMAIN-SUFFIX,apple.news 21 | 22 | # Most of the times, trustd contacts with OCSP servers. 23 | # Even if it contacts Apple's own server, it will be matched by domain rules anyway 24 | # PROCESS-NAME,trustd 25 | 26 | PROCESS-NAME,netbiosd 27 | 28 | # >> Apple Maps 29 | PROCESS-NAME,com.apple.geod 30 | PROCESS-NAME,mapspushd 31 | PROCESS-NAME,com.apple.Maps 32 | 33 | # >> Apple System Services 34 | # DOMAIN api.smoot.apple.com 35 | # DOMAIN captive.apple.com 36 | # DOMAIN xp.apple.com 37 | # DOMAIN configuration.apple.com 38 | # DOMAIN guzzoni.apple.com 39 | # DOMAIN smp-device-content.apple.com 40 | # DOMAIN-SUFFIX ess.apple.com 41 | DOMAIN-SUFFIX,push-apple.com.akadns.net 42 | # DOMAIN-SUFFIX push.apple.com 43 | # DOMAIN aod.itunes.apple.com 44 | # DOMAIN mesu.apple.com 45 | # DOMAIN gs-loc.apple.com 46 | # DOMAIN mvod.itunes.apple.com 47 | # DOMAIN streamingaudio.itunes.apple.com 48 | # DOMAIN-SUFFIX lcdn-locator.apple.com 49 | # DOMAIN-SUFFIX lcdn-registration.apple.com 50 | # DOMAIN-SUFFIX ls.apple.com 51 | PROCESS-NAME,apsd 52 | PROCESS-NAME,fmfd 53 | PROCESS-NAME,findmydevice-user-agent 54 | PROCESS-NAME,CoreLocationAgent 55 | PROCESS-NAME,WeatherWidget 56 | -------------------------------------------------------------------------------- /Build/lib/run-against-source-file.ts: -------------------------------------------------------------------------------- 1 | import { never } from 'foxts/guard'; 2 | import { readFileByLine } from './fetch-text-by-line'; 3 | import { processLine } from './process-line'; 4 | 5 | export default async function runAgainstSourceFile( 6 | filePath: string, 7 | callback: (domain: string, includeAllSubDomain: boolean) => void, 8 | type?: 'ruleset' | 'domainset', 9 | /** Secret keyword collection, only use for special purpose */ 10 | keywordSet?: Set | null 11 | ) { 12 | let l: string | null = ''; 13 | for await (const line of readFileByLine(filePath)) { 14 | l = processLine(line); 15 | if (!l) { 16 | continue; 17 | } 18 | const otherPoundSign = l.lastIndexOf('#'); 19 | 20 | if (otherPoundSign > 0) { 21 | l = l.slice(0, otherPoundSign).trimEnd(); 22 | } 23 | 24 | if (type == null) { 25 | if (l.includes(',')) { 26 | type = 'ruleset'; 27 | } else { 28 | type = 'domainset'; 29 | } 30 | } 31 | 32 | if (type === 'ruleset') { 33 | const [ruleType, domain] = l.split(',', 3); 34 | switch (ruleType.toUpperCase()) { 35 | case 'DOMAIN': { 36 | callback(domain, false); 37 | break; 38 | } 39 | case 'DOMAIN-SUFFIX': { 40 | callback(domain, true); 41 | break; 42 | } 43 | case 'DOMAIN-KEYWORD': { 44 | if (keywordSet) { 45 | keywordSet.add(domain); 46 | } 47 | break; 48 | } 49 | // no default 50 | } 51 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- exhaus options 52 | } else if (type === 'domainset') { 53 | if (l[0] === '.') { 54 | callback(l.slice(1), true); 55 | } else { 56 | callback(l, false); 57 | } 58 | } else { 59 | never(type); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Build/lib/create-file.ts: -------------------------------------------------------------------------------- 1 | import { asyncWriteToStream } from 'foxts/async-write-to-stream'; 2 | import { fastStringArrayJoin } from 'foxts/fast-string-array-join'; 3 | import fs from 'node:fs'; 4 | import picocolors from 'picocolors'; 5 | import type { Span } from '../trace'; 6 | import { readFileByLine } from './fetch-text-by-line'; 7 | import { writeFile } from './misc'; 8 | import { createCompareSource, fileEqualWithCommentComparator } from 'foxts/compare-source'; 9 | 10 | export const fileEqual = createCompareSource(fileEqualWithCommentComparator); 11 | 12 | export async function compareAndWriteFile(span: Span, linesA: string[], filePath: string) { 13 | const isEqual = await span.traceChildAsync(`compare ${filePath}`, async () => { 14 | if (fs.existsSync(filePath)) { 15 | return fileEqual(linesA, readFileByLine(filePath)); 16 | } 17 | 18 | console.log(`${filePath} does not exists, writing...`); 19 | return false; 20 | }); 21 | 22 | if (isEqual) { 23 | console.log(picocolors.gray(picocolors.dim(`same content, bail out writing: ${filePath}`))); 24 | return; 25 | } 26 | 27 | return span.traceChildAsync(`writing ${filePath}`, async () => { 28 | const linesALen = linesA.length; 29 | 30 | // The default highwater mark is normally 16384, 31 | // So we make sure direct write to file if the content is 32 | // most likely less than 250 lines 33 | if (linesALen < 250) { 34 | return writeFile(filePath, fastStringArrayJoin(linesA, '\n') + '\n'); 35 | } 36 | 37 | const writeStream = fs.createWriteStream(filePath); 38 | for (let i = 0; i < linesALen; i++) { 39 | const p = asyncWriteToStream(writeStream, linesA[i] + '\n'); 40 | // eslint-disable-next-line no-await-in-loop -- stream high water mark 41 | if (p) await p; 42 | } 43 | 44 | writeStream.end(); 45 | writeStream.close(); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /Build/lib/process-line.ts: -------------------------------------------------------------------------------- 1 | import { TransformStream } from 'node:stream/web'; 2 | 3 | export function processLine(line: string): string | null { 4 | const trimmed: string = line.trim(); 5 | if (trimmed.length === 0) { 6 | return null; 7 | } 8 | 9 | const line_0 = trimmed.charCodeAt(0); 10 | 11 | if ( 12 | // line_0 === 32 /** [space] */ 13 | // || line_0 === 13 /** \r */ 14 | // || line_0 === 10 /** \n */ 15 | line_0 === 33 /** ! */ 16 | || (line_0 === 47 /** / */ && trimmed.charCodeAt(1) === 47 /** / */) 17 | ) { 18 | return null; 19 | } 20 | 21 | if (line_0 === 35 /** # */) { 22 | if (trimmed.charCodeAt(1) !== 35 /** # */) { 23 | // # Comment 24 | // AdGuard Rule like #@.not_ad 25 | return null; 26 | } 27 | if (trimmed.charCodeAt(2) === 35 /** # */ && trimmed.charCodeAt(3) === 35 /** # */) { 28 | // ################## EOF ################## 29 | return null; 30 | } 31 | /** 32 | * AdGuard Filter can be: 33 | * 34 | * ##.class 35 | * ##tag.class 36 | * ###id 37 | */ 38 | } 39 | 40 | return trimmed; 41 | } 42 | 43 | export class ProcessLineStream extends TransformStream { 44 | // private __buf = ''; 45 | constructor() { 46 | super({ 47 | transform(l, controller) { 48 | const line = processLine(l); 49 | if (line) { 50 | controller.enqueue(line); 51 | } 52 | } 53 | }); 54 | } 55 | } 56 | 57 | // export class ProcessLineNodeStream extends Transform { 58 | // _transform(chunk: string, encoding: BufferEncoding, callback: TransformCallback) { 59 | // // Convert chunk to string and then to uppercase 60 | // const upperCased = chunk.toUpperCase(); 61 | // // Push transformed data to readable side 62 | // this.push(upperCased); 63 | // // Call callback when done 64 | // callback(); 65 | // } 66 | // } 67 | -------------------------------------------------------------------------------- /Build/lib/rules/ruleset.ts: -------------------------------------------------------------------------------- 1 | import type { Span } from '../../trace'; 2 | import { ClashClassicRuleSet } from '../writing-strategy/clash'; 3 | import { LegacyClashPremiumClassicRuleSet } from '../writing-strategy/legacy-clash-premium'; 4 | import { SingboxSource } from '../writing-strategy/singbox'; 5 | import { SurfboardRuleSet } from '../writing-strategy/surfboard'; 6 | import { SurgeRuleSet } from '../writing-strategy/surge'; 7 | import { FileOutput } from './base'; 8 | 9 | export class RulesetOutput extends FileOutput { 10 | constructor(span: Span, id: string, type: 'non_ip' | 'ip') { 11 | super(span, id); 12 | 13 | this.strategies = [ 14 | new SurgeRuleSet(type), 15 | new ClashClassicRuleSet(type), 16 | new LegacyClashPremiumClassicRuleSet(type), 17 | new SurfboardRuleSet(type), 18 | new SingboxSource(type) 19 | ]; 20 | } 21 | } 22 | 23 | export class SurgeOnlyRulesetOutput extends FileOutput { 24 | constructor( 25 | span: Span, 26 | id: string, 27 | type: 'non_ip' | 'ip' | (string & {}), 28 | overrideOutputDir?: string 29 | ) { 30 | super(span, id); 31 | 32 | this.strategies = [ 33 | new SurgeRuleSet(type, overrideOutputDir) 34 | ]; 35 | } 36 | } 37 | 38 | export class MihomoNameserverPolicyOutput extends FileOutput { 39 | constructor( 40 | span: Span, 41 | id: string, 42 | type: 'non_ip' | (string & {}), 43 | overrideOutputDir?: string 44 | ) { 45 | super(span, id); 46 | 47 | this.strategies = [ 48 | new ClashClassicRuleSet(type, overrideOutputDir) 49 | ]; 50 | } 51 | } 52 | 53 | export class ClashOnlyRulesetOutput extends FileOutput { 54 | constructor( 55 | span: Span, 56 | id: string, 57 | type: 'non_ip' | 'ip' 58 | ) { 59 | super(span, id); 60 | 61 | this.strategies = [ 62 | new ClashClassicRuleSet(type), 63 | new LegacyClashPremiumClassicRuleSet(type) 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Build/tools-lum-apex-domains.ts: -------------------------------------------------------------------------------- 1 | import { fetchRemoteTextByLine } from './lib/fetch-text-by-line'; 2 | import tldts from 'tldts-experimental'; 3 | import { HostnameSmolTrie } from './lib/trie'; 4 | import path from 'node:path'; 5 | import { SOURCE_DIR } from './constants/dir'; 6 | import runAgainstSourceFile from './lib/run-against-source-file'; 7 | 8 | (async () => { 9 | const lines1 = await Array.fromAsync(await fetchRemoteTextByLine('https://raw.githubusercontent.com/durablenapkin/block/master/luminati.txt', true)); 10 | const lines2 = await Array.fromAsync(await fetchRemoteTextByLine('https://raw.githubusercontent.com/durablenapkin/block/master/tvstream.txt', true)); 11 | 12 | const trie = new HostnameSmolTrie(); 13 | 14 | lines1.forEach((line) => { 15 | const apexDomain = tldts.getDomain(line.slice(8)); 16 | if (apexDomain) { 17 | trie.add(apexDomain); 18 | } 19 | }); 20 | lines2.forEach((line) => { 21 | const apexDomain = tldts.getDomain(line.slice(8)); 22 | if (apexDomain) { 23 | trie.add(apexDomain); 24 | } 25 | }); 26 | 27 | const dataFromDuckDuckGo = await fetch('https://raw.githubusercontent.com/duckduckgo/tracker-radar/92e086ce38a8a88c964ed0184e5277ec1d5c8038/entities/Bright%20Data%20Ltd..json').then((res) => res.json()); 28 | if (typeof dataFromDuckDuckGo === 'object' && dataFromDuckDuckGo !== null && 'properties' in dataFromDuckDuckGo && Array.isArray(dataFromDuckDuckGo.properties)) { 29 | dataFromDuckDuckGo.properties.forEach((prop) => { 30 | trie.add(prop); 31 | }); 32 | } 33 | 34 | await runAgainstSourceFile(path.join(SOURCE_DIR, 'domainset', 'reject.conf'), (domain, includeAllSubDomain) => { 35 | trie.whitelist(domain, includeAllSubDomain); 36 | }, 'domainset'); 37 | await runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip', 'reject.conf'), (domain, includeAllSubDomain) => { 38 | trie.whitelist(domain, includeAllSubDomain); 39 | }, 'ruleset'); 40 | 41 | console.log(trie.dump().map(i => '.' + i).join('\n')); 42 | })(); 43 | -------------------------------------------------------------------------------- /Modules/sukka_enhance_adblock.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=[Sukka] Enhance Better ADBlock for Surge 2 | #!desc=增强 ADBlock 效果、恢复网站正常功能 3 | 4 | [MITM] 5 | hostname = %APPEND% *.google-analytics.com, *.googletagmanager.com, *.googlesyndication.com, *.googletagservices.com, *.doubleclick.net, cdn.ampproject.org, *.addthis.com, static.chartbeat.com, widgets.outbrain.com 6 | 7 | [URL Rewrite] 8 | ^https?://.+\.google-analytics\.com/analytics\.js https://ruleset.skk.moe/Mock/www-google-analytics-com_analytics.js 302 9 | ^https?://.+\.googletagmanager\.com/gtm\.js https://ruleset.skk.moe/Mock/www-google-analytics-com_analytics.js 302 10 | ^https?://.+\.google-analytics\.com/ga\.js https://ruleset.skk.moe/Mock/www-google-analytics-com_ga.js 302 11 | ^https?://.+\.google-analytics\.com/cx/api\.js https://ruleset.skk.moe/Mock/www-google-analytics-com_cx_api.js 302 12 | ^https?://.+\.googlesyndication\.com/adsbygoogle\.js https://ruleset.skk.moe/Mock/www-googlesyndication-com_adsbygoogle.js 302 13 | ^https?://.+\.googletagservices\.com/gpt\.js https://ruleset.skk.moe/Mock/www-googletagservices-com_gpt.js 302 14 | ^https?://.+\.google-analytics\.com/inpage_linkid\.js https://ruleset.skk.moe/Mock/www-google-analytics-com_inpage_linkid.js 302 15 | ^https?://.+\.doubleclick\.net/instream/ad_status\.js https://ruleset.skk.moe/Mock/doubleclick-net_instream_ad_status.js 302 16 | ^https?://cdn\.ampproject\.org/v0\.js https://ruleset.skk.moe/Mock/ampproject-org_v0.js 302 17 | ^https?://.+\.addthis\.com/addthis_widget\.js https://ruleset.skk.moe/Mock/addthis-com_addthis_widget.js 302 18 | ^https?://.+\.amazon-adsystem\.com/aax2/apstag\.js https://ruleset.skk.moe/Mock/amazon-adsystem-com_amazon-apstag.js 302 19 | ^https?://static\.chartbeat\.com/chartbeat\.js https://ruleset.skk.moe/Mock/static-chartbeat-com_chartbeat_mab.js 302 20 | ^https?://widgets\.outbrain\.com/outbrain\.js https://ruleset.skk.moe/Mock/widgets-outbrain-com_outbrain.js 302 21 | ^https?://securepubads\.g\.doubleclick\.net/tag/js/gpt\.js https://ruleset.skk.moe/Mock/securepubads-g-doubleclick-net_tag_js_gpt.js 302 22 | -------------------------------------------------------------------------------- /Build/download-mock-assets.ts: -------------------------------------------------------------------------------- 1 | import { task } from './trace'; 2 | import path from 'node:path'; 3 | import fs from 'node:fs'; 4 | import { pipeline } from 'node:stream/promises'; 5 | import { OUTPUT_MOCK_DIR } from './constants/dir'; 6 | import { mkdirp } from './lib/misc'; 7 | import { $$fetch } from './lib/fetch-retry'; 8 | 9 | const ASSETS_LIST = { 10 | 'www-google-analytics-com_ga.js': 'https://cdn.jsdelivr.net/npm/@adguard/scriptlets@latest/dist/redirect-files/google-analytics-ga.js', 11 | 'www-googletagservices-com_gpt.js': 'https://cdn.jsdelivr.net/npm/@adguard/scriptlets@latest/dist/redirect-files/googletagservices-gpt.js', 12 | 'www-google-analytics-com_analytics.js': 'https://cdn.jsdelivr.net/npm/@adguard/scriptlets@latest/dist/redirect-files/google-analytics.js', 13 | 'www-googlesyndication-com_adsbygoogle.js': 'https://cdn.jsdelivr.net/npm/@adguard/scriptlets@latest/dist/redirect-files/googlesyndication-adsbygoogle.js', 14 | 'amazon-adsystem-com_amazon-apstag.js': 'https://cdn.jsdelivr.net/npm/@adguard/scriptlets@latest/dist/redirect-files/amazon-apstag.js' 15 | } as const; 16 | 17 | export const downloadMockAssets = task(require.main === module, __filename)(async (span) => { 18 | const p = mkdirp(OUTPUT_MOCK_DIR); 19 | if (p) { 20 | await p; 21 | } 22 | 23 | return Promise.all(Object.entries(ASSETS_LIST).map( 24 | ([filename, url]) => span 25 | .traceChildAsync(url, async () => { 26 | const res = await $$fetch(url); 27 | if (!res.ok) { 28 | console.error(`Failed to download ${url}`); 29 | 30 | // we can safely skip this since we can always use previous version 31 | return; 32 | } 33 | 34 | if (!res.body) { 35 | console.error(`Empty body from ${url}`); 36 | 37 | // we can safely skip this since we can always use previous version 38 | return; 39 | } 40 | 41 | const src = path.join(OUTPUT_MOCK_DIR, filename); 42 | 43 | return pipeline( 44 | res.body, 45 | fs.createWriteStream(src, 'utf-8') 46 | ); 47 | }) 48 | )); 49 | }); 50 | -------------------------------------------------------------------------------- /Build/constants/domains.ts: -------------------------------------------------------------------------------- 1 | export const ICP_TLD = [ 2 | 'ren', 3 | 'wang', 4 | 'citic', 5 | 'top', 6 | 'sohu', 7 | 'xin', 8 | 'com', 9 | 'net', 10 | 'club', 11 | 'xyz', 12 | 'site', 13 | 'shop', 14 | 'info', 15 | 'mobi', 16 | 'red', 17 | 'pro', 18 | 'kim', 19 | 'ltd', 20 | 'group', 21 | 'biz', 22 | 'link', 23 | 'store', 24 | 'tech', 25 | 'fun', 26 | 'online', 27 | 'art', 28 | 'design', 29 | 'love', 30 | 'center', 31 | 'video', 32 | 'social', 33 | 'team', 34 | 'show', 35 | 'cool', 36 | 'zone', 37 | 'world', 38 | 'today', 39 | 'city', 40 | 'chat', 41 | 'company', 42 | 'live', 43 | 'fund', 44 | 'gold', 45 | 'plus', 46 | 'guru', 47 | 'run', 48 | 'pub', 49 | 'email', 50 | 'life', 51 | 'co', 52 | 'baidu', 53 | 'cloud', 54 | 'host', 55 | 'space', 56 | 'press', 57 | 'website', 58 | 'archi', 59 | 'asia', 60 | 'bio', 61 | 'black', 62 | 'blue', 63 | 'green', 64 | 'lotto', 65 | 'organic', 66 | 'pet', 67 | 'pink', 68 | 'poker', 69 | 'promo', 70 | 'ski', 71 | 'vote', 72 | 'voto', 73 | 'icu', 74 | 'fans', 75 | 'unicom', 76 | 'jpmorgan', 77 | 'chase', 78 | 'cc', 79 | 'band', 80 | 'cab', 81 | 'cafe', 82 | 'cash', 83 | 'fan', 84 | 'fyi', 85 | 'games', 86 | 'market', 87 | 'mba', 88 | 'news', 89 | 'media', 90 | 'sale', 91 | 'shopping', 92 | 'studio', 93 | 'tax', 94 | 'technology', 95 | 'vin', 96 | 'baby', 97 | 'college', 98 | 'monster', 99 | 'protection', 100 | 'rent', 101 | 'security', 102 | 'storage', 103 | 'theatre', 104 | 'bond', 105 | 'cyou', 106 | 'uno', 107 | 'school', 108 | 'global', 109 | 'me', 110 | 'pw', 111 | 'hk', 112 | 'tv', 113 | 'saxo', 114 | 'click', 115 | 'auto', 116 | 'autos', 117 | 'beauty', 118 | 'boats', 119 | 'car', 120 | 'cars', 121 | 'hair', 122 | 'homes', 123 | 'makeup', 124 | 'motorcycles', 125 | 'quest', 126 | 'skin', 127 | 'tickets', 128 | 'yachts', 129 | 'kids' 130 | ]; 131 | -------------------------------------------------------------------------------- /Build/lib/tree-dir.ts: -------------------------------------------------------------------------------- 1 | import fsp from 'node:fs/promises'; 2 | import { sep } from 'node:path'; 3 | import type { VoidOrVoidArray } from './misc'; 4 | 5 | // eslint-disable-next-line sukka/no-export-const-enum -- TODO: fix this in the future 6 | export const enum TreeFileType { 7 | FILE = 1, 8 | DIRECTORY = 2 9 | } 10 | 11 | interface TreeFile { 12 | type: TreeFileType.FILE, 13 | name: string, 14 | path: string 15 | } 16 | 17 | interface TreeDirectoryType { 18 | type: TreeFileType.DIRECTORY, 19 | name: string, 20 | path: string, 21 | children: TreeTypeArray 22 | } 23 | 24 | export type TreeType = TreeDirectoryType | TreeFile; 25 | export type TreeTypeArray = TreeType[]; 26 | 27 | export async function treeDir(rootPath: string): Promise { 28 | const tree: TreeTypeArray = []; 29 | 30 | const promises: Array> = []; 31 | 32 | const walk = async (dir: string, node: TreeTypeArray, dirRelativeToRoot = ''): Promise => { 33 | for await (const child of await fsp.opendir(dir)) { 34 | // Ignore hidden files 35 | if (child.name[0] === '.' || child.name === 'CNAME') { 36 | continue; 37 | } 38 | 39 | const childFullPath = child.parentPath + sep + child.name; 40 | const childRelativeToRoot = dirRelativeToRoot + sep + child.name; 41 | 42 | if (child.isDirectory()) { 43 | const newNode: TreeDirectoryType = { 44 | type: TreeFileType.DIRECTORY, 45 | name: child.name, 46 | path: childRelativeToRoot, 47 | children: [] 48 | }; 49 | node.push(newNode); 50 | promises.push(walk(childFullPath, newNode.children, childRelativeToRoot)); 51 | continue; 52 | } 53 | if (child.isFile()) { 54 | const newNode: TreeFile = { 55 | type: TreeFileType.FILE, 56 | name: child.name, 57 | path: childRelativeToRoot 58 | }; 59 | node.push(newNode); 60 | continue; 61 | } 62 | } 63 | }; 64 | 65 | await walk(rootPath, tree); 66 | await Promise.all(promises); 67 | 68 | return tree; 69 | } 70 | -------------------------------------------------------------------------------- /Build/build-stream-service.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import type { Span } from './trace'; 3 | import { task } from './trace'; 4 | 5 | import { ALL, NORTH_AMERICA, EU, HK, TW, JP, KR } from '../Source/stream'; 6 | import { SHARED_DESCRIPTION } from './constants/description'; 7 | import { RulesetOutput } from './lib/rules/ruleset'; 8 | 9 | function createRulesetForStreamService( 10 | span: Span, 11 | fileId: string, title: string, 12 | streamServices: Array 13 | ) { 14 | return [ 15 | // Domains 16 | new RulesetOutput(span, fileId, 'non_ip') 17 | .withTitle(`Sukka's Ruleset - Stream Services: ${title}`) 18 | .appendDescription(SHARED_DESCRIPTION) 19 | .appendDescription('') 20 | .appendDescription(streamServices.map((i) => `- ${i.name}`)) 21 | .addFromRuleset(streamServices.flatMap((i) => i.rules)) 22 | .write(), 23 | // IP 24 | new RulesetOutput(span, fileId, 'ip') 25 | .withTitle(`Sukka's Ruleset - Stream Services IPs: ${title}`) 26 | .appendDescription(SHARED_DESCRIPTION) 27 | .appendDescription('') 28 | .appendDescription(streamServices.map((i) => `- ${i.name}`)) 29 | .bulkAddCIDR4NoResolve(streamServices.flatMap(i => i.ip?.v4 ?? [])) 30 | .bulkAddCIDR6NoResolve(streamServices.flatMap(i => i.ip?.v6 ?? [])) 31 | .write() 32 | ]; 33 | } 34 | 35 | export const buildStreamService = task(require.main === module, __filename)(async (span) => Promise.all([ 36 | createRulesetForStreamService(span, 'stream', 'All', ALL), 37 | createRulesetForStreamService(span, 'stream_us', 'North America', NORTH_AMERICA), 38 | createRulesetForStreamService(span, 'stream_eu', 'Europe', EU), 39 | createRulesetForStreamService(span, 'stream_hk', 'Hong Kong', HK), 40 | createRulesetForStreamService(span, 'stream_tw', 'Taiwan', TW), 41 | createRulesetForStreamService(span, 'stream_jp', 'Japan', JP), 42 | // createRulesetForStreamService('stream_au', 'Oceania', AU), 43 | createRulesetForStreamService(span, 'stream_kr', 'Korean', KR) 44 | // createRulesetForStreamService('stream_south_east_asia', 'South East Asia', SOUTH_EAST_ASIA) 45 | ].flat())); 46 | -------------------------------------------------------------------------------- /Build/build-deprecate-files.ts: -------------------------------------------------------------------------------- 1 | import { OUTPUT_CLASH_DIR, OUTPUT_SURGE_DIR, PUBLIC_DIR } from './constants/dir'; 2 | import { compareAndWriteFile } from './lib/create-file'; 3 | import { task } from './trace'; 4 | import path from 'node:path'; 5 | import fsp from 'node:fs/promises'; 6 | import { globSync } from 'tinyglobby'; 7 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 8 | 9 | const DEPRECATED_FILES = [ 10 | ['non_ip/global_plus', 'This file has been merged with non_ip/global'], 11 | ['domainset/reject_sukka', 'This file has been merged with domainset/reject'] 12 | ]; 13 | 14 | const REMOVED_FILES = [ 15 | 'Internal/chnroutes.txt', 16 | 'List/internal/appprofile.php', 17 | 'Clash/domainset/steam.txt', 18 | 'Clash/non_ip/clash_fake_ip_filter.txt', 19 | 'sing-box/domainset/steam.json', 20 | 'Modules/sukka_unlock_abema.sgmodule', 21 | 'Modules/sukka_exclude_reservered_ip.sgmodule', 22 | 'Modules/Rules/*.sgmodule', 23 | 'Internal/mihomo_nameserver_policy/*.conf' 24 | ]; 25 | 26 | const REMOVED_FOLDERS = [ 27 | 'List/Internal', 28 | 'Clash/Internal' 29 | ]; 30 | 31 | export const buildDeprecateFiles = task(require.main === module, __filename)((span) => span.traceChildAsync('create deprecated files', async (childSpan) => { 32 | const promises: Array> = globSync(REMOVED_FILES, { cwd: PUBLIC_DIR, absolute: true }) 33 | .map(f => fsp.rm(f, { force: true, recursive: true })); 34 | 35 | appendArrayInPlace(promises, REMOVED_FOLDERS.map(folder => fsp.rm(path.join(PUBLIC_DIR, folder), { force: true, recursive: true }))); 36 | 37 | for (const [filePath, description] of DEPRECATED_FILES) { 38 | const content = [ 39 | '#########################################', 40 | '# Sukka\'s Ruleset - Deprecated', 41 | `# ${description}`, 42 | '################## EOF ##################' 43 | ]; 44 | 45 | promises.push( 46 | compareAndWriteFile(childSpan, content, path.resolve(OUTPUT_SURGE_DIR, `${filePath}.conf`)), 47 | compareAndWriteFile(childSpan, content, path.resolve(OUTPUT_CLASH_DIR, `${filePath}.txt`)) 48 | ); 49 | } 50 | 51 | return Promise.all(promises); 52 | })); 53 | -------------------------------------------------------------------------------- /Build/lib/misc.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import fs from 'node:fs'; 3 | import type { PathLike } from 'node:fs'; 4 | import fsp from 'node:fs/promises'; 5 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 6 | 7 | export type MaybePromise = T | Promise; 8 | 9 | interface Write { 10 | ( 11 | destination: string, 12 | input: NodeJS.TypedArray | string, 13 | ): Promise 14 | } 15 | 16 | export type VoidOrVoidArray = void | VoidOrVoidArray[]; 17 | 18 | export function mkdirp(dir: string) { 19 | if (fs.existsSync(dir)) { 20 | return; 21 | } 22 | return fsp.mkdir(dir, { recursive: true }); 23 | } 24 | 25 | export const writeFile: Write = async (destination: string, input, dir = dirname(destination)): Promise => { 26 | const p = mkdirp(dir); 27 | if (p) { 28 | await p; 29 | } 30 | return fsp.writeFile(destination, input, { encoding: 'utf-8' }); 31 | }; 32 | 33 | export function withBannerArray(title: string, description: string[] | readonly string[], date: Date, content: string[]) { 34 | const result: string[] = [ 35 | '#########################################', 36 | `# ${title}`, 37 | `# Last Updated: ${date.toISOString()}`, 38 | `# Size: ${content.length}` 39 | ]; 40 | 41 | appendArrayInPlace(result, description.map(line => (line ? `# ${line}` : '#'))); 42 | 43 | result.push('#########################################'); 44 | 45 | appendArrayInPlace(result, content); 46 | 47 | result.push('################## EOF ##################', ''); 48 | 49 | return result; 50 | }; 51 | 52 | export function notSupported(name: string) { 53 | return (...args: unknown[]) => { 54 | console.error(`${name}: not supported.`, args); 55 | throw new Error(`${name}: not implemented.`); 56 | }; 57 | } 58 | 59 | export function withIdentityContent(title: string, description: string[] | readonly string[], date: Date, content: string[]) { 60 | return content; 61 | }; 62 | 63 | export function isDirectoryEmptySync(path: PathLike) { 64 | const directoryHandle = fs.opendirSync(path); 65 | 66 | try { 67 | return directoryHandle.readSync() === null; 68 | } finally { 69 | directoryHandle.closeSync(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Build/lib/fetch-text-by-line.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import readline from 'node:readline'; 3 | 4 | import { TextLineStream } from 'foxts/text-line-stream'; 5 | import type { ReadableStream } from 'node:stream/web'; 6 | import { TextDecoderStream } from 'node:stream/web'; 7 | import { processLine, ProcessLineStream } from './process-line'; 8 | import { $$fetch } from './fetch-retry'; 9 | import type { UndiciResponseData } from './fetch-retry'; 10 | import type { Response as UnidiciWebResponse } from 'undici'; 11 | import { invariant } from 'foxts/guard'; 12 | 13 | export function readFileByLine(file: string): AsyncIterable { 14 | return readline.createInterface({ 15 | input: fs.createReadStream(file/* , { encoding: 'utf-8' } */), 16 | crlfDelay: Infinity 17 | }); 18 | } 19 | 20 | export const createReadlineInterfaceFromResponse: ((resp: UndiciResponseData | UnidiciWebResponse, processLine?: boolean) => ReadableStream) = (resp, processLine = false) => { 21 | invariant(resp.body, 'Failed to fetch remote text'); 22 | if ('bodyUsed' in resp && resp.bodyUsed) { 23 | throw new Error('Body has already been consumed.'); 24 | } 25 | let webStream: ReadableStream; 26 | if ('pipeThrough' in resp.body) { 27 | webStream = resp.body; 28 | } else { 29 | throw new TypeError('Invalid response body!'); 30 | } 31 | 32 | const resultStream = webStream 33 | .pipeThrough(new TextDecoderStream()) 34 | .pipeThrough(new TextLineStream({ skipEmptyLines: processLine })); 35 | 36 | if (processLine) { 37 | return resultStream.pipeThrough(new ProcessLineStream()); 38 | } 39 | return resultStream; 40 | }; 41 | 42 | export function fetchRemoteTextByLine(url: string, processLine = false): Promise> { 43 | return $$fetch(url).then(resp => createReadlineInterfaceFromResponse(resp, processLine)); 44 | } 45 | 46 | export async function readFileIntoProcessedArray(file: string /* | FileHandle */) { 47 | const results = []; 48 | let processed: string | null = ''; 49 | for await (const line of readFileByLine(file)) { 50 | processed = processLine(line); 51 | if (processed) { 52 | results.push(processed); 53 | } 54 | } 55 | return results; 56 | } 57 | -------------------------------------------------------------------------------- /Build/lib/create-file.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'expect'; 2 | import { fileEqual } from './create-file'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/require-await -- async iterable 5 | async function *createSource(input: string[]) { 6 | for (const line of input) { 7 | yield line; 8 | } 9 | } 10 | 11 | async function test(a: string[], b: string[], expected: boolean) { 12 | expect((await fileEqual(a, createSource(b)))).toBe(expected); 13 | } 14 | 15 | describe('fileEqual', () => { 16 | it('same', () => test( 17 | ['A', 'B'], 18 | ['A', 'B'], 19 | true 20 | )); 21 | 22 | it('ignore comment 1', async () => { 23 | await test( 24 | ['# A', 'B'], 25 | ['# B', 'B'], 26 | true 27 | ); 28 | }); 29 | 30 | it('ignore comment 2', () => test( 31 | ['# A', '# C', 'B'], 32 | ['# A', '# D', 'B'], 33 | true 34 | )); 35 | 36 | it('ignore comment 3', () => test( 37 | ['# A', '# C', 'B'], 38 | ['# A', '# D', 'A'], 39 | false 40 | )); 41 | 42 | it('comment more', () => test( 43 | ['# A', 'B'], 44 | ['# A', '# B', 'B'], 45 | false 46 | )); 47 | 48 | it('comment less', () => test( 49 | ['# A', '# B', 'B'], 50 | ['# A', 'B'], 51 | false 52 | )); 53 | 54 | it('larger', () => test( 55 | ['A', 'B'], 56 | ['A', 'B', 'C'], 57 | false 58 | )); 59 | 60 | it('smaller', () => test( 61 | ['A', 'B', 'C'], 62 | ['A', 'B'], 63 | false 64 | )); 65 | 66 | it('eol more #1', () => test( 67 | ['A', 'B'], 68 | ['A', 'B', ''], 69 | false 70 | )); 71 | 72 | it('eol more #2', () => test( 73 | ['A', 'B', ''], 74 | ['A', 'B', '', ''], 75 | false 76 | )); 77 | 78 | it('eol less #1', () => test( 79 | ['A', 'B', ''], 80 | ['A', 'B'], 81 | false 82 | )); 83 | 84 | it('eol less #2', () => test( 85 | ['A', 'B', '', ''], 86 | ['A', 'B', ''], 87 | false 88 | )); 89 | 90 | it('sgmodule', () => test( 91 | ['#!name=[Sukka] URL Redirect', '#!desc=Last Updated: 2025-04-21T13:01:42.570Z Size: 127', '', 'always-real-ip'], 92 | ['#!name=[Sukka] URL Redirect', '#!desc=Last Updated: 2025-04-20T13:01:42.570Z Size: 130', '', 'always-real-ip'], 93 | true 94 | )); 95 | }); 96 | -------------------------------------------------------------------------------- /Build/validate-domestic.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { SOURCE_DIR } from './constants/dir'; 3 | import { parseFelixDnsmasqFromResp } from './lib/parse-dnsmasq'; 4 | import { $$fetch } from './lib/fetch-retry'; 5 | import runAgainstSourceFile from './lib/run-against-source-file'; 6 | import { getTopOneMillionDomains } from './validate-gfwlist'; 7 | import { HostnameSmolTrie } from './lib/trie'; 8 | import tldts from 'tldts-experimental'; 9 | import { DOMESTICS } from '../Source/non_ip/domestic'; 10 | 11 | export async function parseDomesticList() { 12 | const allChinaDomains = new Set(await parseFelixDnsmasqFromResp(await $$fetch('https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf'))); 13 | 14 | const topDomainTrie = await getTopOneMillionDomains(); 15 | 16 | const resultTrie = new HostnameSmolTrie(); 17 | 18 | topDomainTrie.dumpWithoutDot((domain) => { 19 | const apexDomain = tldts.getDomain(domain); 20 | 21 | if (apexDomain && allChinaDomains.has(apexDomain)) { 22 | resultTrie.add(apexDomain, false); 23 | } 24 | }); 25 | 26 | const callback = (domain: string, includeAllSubdomain: boolean) => resultTrie.whitelist(domain, includeAllSubdomain); 27 | 28 | // await Promise.all([ 29 | await runAgainstSourceFile( 30 | path.resolve(SOURCE_DIR, 'non_ip/domestic.conf'), 31 | callback 32 | ); 33 | await runAgainstSourceFile( 34 | path.resolve(SOURCE_DIR, 'domainset/reject.conf'), 35 | callback 36 | ); 37 | 38 | Object.values(DOMESTICS).forEach(domestic => { 39 | domestic.domains.forEach(domain => { 40 | switch (domain[0]) { 41 | case '+': 42 | case '$': { 43 | resultTrie.whitelist(domain.slice(1), true); 44 | break; 45 | } 46 | default: { 47 | resultTrie.whitelist(domain, true); 48 | break; 49 | } 50 | } 51 | }); 52 | }); 53 | 54 | // noop, DOMAIN-KEYWORD handing 55 | // for (const d of top5000) { 56 | // if (d.includes(domain)) { 57 | // notIncludedDomestic.delete(d); 58 | // } 59 | // } 60 | // ]); 61 | 62 | console.log(resultTrie.dump().join('\n') + '\n'); 63 | } 64 | 65 | if (require.main === module) { 66 | parseDomesticList().catch(console.error); 67 | } 68 | -------------------------------------------------------------------------------- /Source/domainset/game-download.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Game Download 2 | # $ meta_description This file contains domains for Gam download CDN domains for Steam, Epic, Blizzard, Gog, Ubisoft, EA, Xbox, PlayStation, Riot, Bethesda and more. 3 | # $ meta_description No China CDN domains are included. Those domains are included in non_ip/domestic 4 | 5 | # Steam 6 | .steamcontent.com 7 | .steamserver.net 8 | .content.steampowered.com 9 | lancache.steamcontent.com 10 | # content1.steampowered.com 11 | # content2.steampowered.com 12 | # content3.steampowered.com 13 | # content4.steampowered.com 14 | # content5.steampowered.com 15 | # content6.steampowered.com 16 | # content7.steampowered.com 17 | # content8.steampowered.com 18 | content-origin.steampowered.com 19 | steam.apac.qtlglb.com 20 | steam.eca.qtlglb.com 21 | steam.naeu.qtlglb.com 22 | steam.ru.qtlglb.com 23 | edge.steam-dns.top.comcast.net 24 | steampipe.akamaized.net 25 | steampipe-kr.akamaized.net 26 | steampipe-partner.akamaized.net 27 | 28 | # Epic 29 | .download.epicgames.com 30 | download2.epicgames.com 31 | download3.epicgames.com 32 | download4.epicgames.com 33 | epicgames-download1.akamaized.net 34 | fastly-download.epicgames.com 35 | .egdownload.fastly-edge.com 36 | .epicgamescdn.com 37 | 38 | # PlayStation 39 | .dl.playstation.net 40 | 41 | # Battle.net / Blizzard 42 | dist.blizzard.com 43 | blzddist1-a.akamaihd.net 44 | blzddist2-a.akamaihd.net 45 | blzddist3-a.akamaihd.net 46 | blzddistkr1-a.akamaihd.net 47 | blizzard.gcdn.cloudn.co.kr 48 | .cdn.blizzard.com 49 | level3.blizzard.com 50 | 51 | # Gog 52 | gog-cdn-fastly-cp77.gog.com 53 | gog-cdn-fastly.gog.com 54 | # gog-cdn-lumen.secure2.footprint.net 55 | 56 | # Xbox 57 | pf-cdn-content-prod.azureedge.net 58 | xvcf1.xboxlive.com 59 | xvcf2.xboxlive.com 60 | packagespc.xboxlive.com 61 | download.xbox.com 62 | assets1.xboxlive.com 63 | assets2.xboxlive.com 64 | 65 | # Ubisoft 66 | .cdn.ubi.com 67 | 68 | # EA Origin 69 | .download.dm.origin.com 70 | origin-a.akamaihd.net 71 | .cdn.ea.com 72 | dlgarenanow-a.akamaihd.net 73 | cdn-patch.swtor.com 74 | 75 | # Riot 76 | .dyn.riotcdn.net 77 | 78 | # Bethesda 79 | .content.cdp.bethesda.net 80 | .download.cdp.bethesda.net 81 | 82 | # Misc 83 | client.hikarifield.co.jp 84 | download.hikarifield.co.jp 85 | gamedownloads-rockstargames-com.akamaized.net 86 | 87 | # Nintendo 88 | .wup.shop.nintendo.net 89 | .cdn.wup.shop.nintendo.net 90 | .wup.eshop.nintendo.net 91 | .cdn.nintendo.net 92 | -------------------------------------------------------------------------------- /Source/ip/reject.conf: -------------------------------------------------------------------------------- 1 | # $ custom_build_script 2 | 3 | # Commonly used by cloud computing instances as the metadata service 4 | # Should not be used in the local environment 5 | # Also, VSCode GitHub Copilot plugin sends telemetry going haywire, at 2 req/s 6 | # Let's Surge drop the packets to prevent flooding 7 | # https://github.com/microsoft/vscode-copilot-release/issues/1496#issuecomment-2422464393 8 | IP-CIDR,169.254.169.254/32,no-resolve 9 | 10 | # DNS resolved to 0.0.0.0 should not leak to the LAN 11 | IP-CIDR,0.0.0.0/24 12 | 13 | # --- AD Block --- 14 | 15 | # >> iQiyi 16 | IP-CIDR,101.227.97.240/32,no-resolve 17 | IP-CIDR,101.227.200.11/32,no-resolve 18 | IP-CIDR,101.227.200.28/32,no-resolve 19 | IP-CIDR,124.192.153.42/32,no-resolve 20 | 21 | # --- Anti-Hijacking --- 22 | 23 | IP-CIDR,39.107.15.115/32,no-resolve 24 | IP-CIDR,47.89.59.182/32,no-resolve 25 | IP-CIDR,103.49.209.27/32,no-resolve 26 | IP-CIDR,123.56.152.96/32,no-resolve 27 | 28 | # >> ChinaNet 29 | IP-CIDR,61.160.200.223/32,no-resolve 30 | IP-CIDR,61.160.200.242/32,no-resolve 31 | IP-CIDR,61.160.200.252/32,no-resolve 32 | IP-CIDR,61.174.50.214/32,no-resolve 33 | IP-CIDR,111.175.220.163/32,no-resolve 34 | IP-CIDR,111.175.220.164/32,no-resolve 35 | IP-CIDR,124.232.160.178/32,no-resolve 36 | IP-CIDR,175.6.223.15/32,no-resolve 37 | IP-CIDR,183.59.53.237/32,no-resolve 38 | IP-CIDR,218.93.127.37/32,no-resolve 39 | IP-CIDR,221.228.17.152/32,no-resolve 40 | IP-CIDR,221.231.6.79/32,no-resolve 41 | IP-CIDR,222.186.61.91/32,no-resolve 42 | IP-CIDR,222.186.61.95/32,no-resolve 43 | IP-CIDR,222.186.61.96/32,no-resolve 44 | IP-CIDR,222.186.61.97/32,no-resolve 45 | 46 | # >> ChinaUnicom 47 | IP-CIDR,106.75.231.48/32,no-resolve 48 | IP-CIDR,119.4.249.166/32,no-resolve 49 | IP-CIDR,220.196.52.141/32,no-resolve 50 | IP-CIDR,221.6.4.148/32,no-resolve 51 | IP-CIDR,221.228.32.13/32,no-resolve 52 | 53 | # >> ChinaMobile 54 | IP-CIDR,114.247.28.96/32,no-resolve 55 | IP-CIDR,221.179.131.72/32,no-resolve 56 | IP-CIDR,221.179.140.145/32,no-resolve 57 | 58 | # >> Dr.Peng 59 | IP-CIDR,10.72.25.0/24,no-resolve 60 | IP-CIDR,115.182.16.79/32,no-resolve 61 | IP-CIDR,118.144.88.126/32,no-resolve 62 | IP-CIDR,118.144.88.215/32,no-resolve 63 | IP-CIDR,120.76.189.132/32,no-resolve 64 | IP-CIDR,124.14.21.147/32,no-resolve 65 | IP-CIDR,124.14.21.151/32,no-resolve 66 | IP-CIDR,180.166.52.24/32,no-resolve 67 | IP-CIDR,220.115.251.25/32,no-resolve 68 | IP-CIDR,222.73.156.235/32,no-resolve 69 | 70 | # --- Anti-Bogus Domain --- 71 | # https://github.com/felixonmars/dnsmasq-china-list/blob/master/bogus-nxdomain.china.conf 72 | -------------------------------------------------------------------------------- /Build/lib/parse-filter/hosts.ts: -------------------------------------------------------------------------------- 1 | import type { Span } from '../../trace'; 2 | import { fetchAssets } from '../fetch-assets'; 3 | import { fastNormalizeDomainWithoutWww } from '../normalize-domain'; 4 | import { onBlackFound } from './shared'; 5 | 6 | const rSpace = /\s+/; 7 | 8 | function hostsLineCb(line: string, set: string[], meta: string) { 9 | const _domain = line.split(rSpace, 3)[1]; 10 | if (!_domain) { 11 | return; 12 | } 13 | const domain = fastNormalizeDomainWithoutWww(_domain.trim()); 14 | if (!domain) { 15 | return; 16 | } 17 | 18 | onBlackFound(domain, meta); 19 | 20 | set.push(domain); 21 | } 22 | 23 | function hostsLineCbIncludeAllSubdomain(line: string, set: string[], meta: string) { 24 | const _domain = line.split(rSpace, 3)[1]; 25 | if (!_domain) { 26 | return; 27 | } 28 | const domain = fastNormalizeDomainWithoutWww(_domain.trim()); 29 | if (!domain) { 30 | return; 31 | } 32 | 33 | onBlackFound(domain, meta); 34 | 35 | set.push('.' + domain); 36 | } 37 | 38 | export function processHosts( 39 | span: Span, 40 | hostsUrl: string, mirrors: string[] | null, includeAllSubDomain = false 41 | ) { 42 | const cb = includeAllSubDomain ? hostsLineCbIncludeAllSubdomain : hostsLineCb; 43 | 44 | return span.traceChildAsync(`process hosts: ${hostsUrl}`, async (span) => { 45 | const filterRules = await span.traceChild('download').traceAsyncFn(() => fetchAssets(hostsUrl, mirrors, true)); 46 | 47 | const domainSets: string[] = []; 48 | 49 | span.traceChild('parse hosts').traceSyncFn(() => { 50 | for (let i = 0, len = filterRules.length; i < len; i++) { 51 | cb(filterRules[i], domainSets, hostsUrl); 52 | } 53 | }); 54 | 55 | return domainSets; 56 | }); 57 | } 58 | 59 | export function processHostsWithPreload(hostsUrl: string, mirrors: string[] | null, includeAllSubDomain = false, allowEmptyRemote = false) { 60 | const downloadPromise = fetchAssets(hostsUrl, mirrors, true, allowEmptyRemote); 61 | const cb = includeAllSubDomain ? hostsLineCbIncludeAllSubdomain : hostsLineCb; 62 | 63 | return (span: Span) => span.traceChildAsync(`process hosts: ${hostsUrl}`, async (span) => { 64 | const filterRules = await span.traceChild('download').tracePromise(downloadPromise); 65 | 66 | const domainSets: string[] = []; 67 | 68 | span.traceChild('parse hosts').traceSyncFn(() => { 69 | for (let i = 0, len = filterRules.length; i < len; i++) { 70 | cb(filterRules[i], domainSets, hostsUrl); 71 | } 72 | }); 73 | 74 | return domainSets; 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /Build/validate-domain-alive.ts: -------------------------------------------------------------------------------- 1 | import { SOURCE_DIR } from './constants/dir'; 2 | import path from 'node:path'; 3 | import { getMethods } from './lib/is-domain-alive'; 4 | import { fdir as Fdir } from 'fdir'; 5 | import runAgainstSourceFile from './lib/run-against-source-file'; 6 | 7 | import cliProgress from 'cli-progress'; 8 | import { newQueue } from '@henrygd/queue'; 9 | 10 | const queue = newQueue(32); 11 | 12 | const deadDomains: string[] = []; 13 | 14 | (async () => { 15 | const [ 16 | { isDomainAlive, isRegisterableDomainAlive }, 17 | domainSets, 18 | domainRules 19 | ] = await Promise.all([ 20 | getMethods(), 21 | new Fdir() 22 | .withFullPaths() 23 | .filter((filePath, isDirectory) => { 24 | if (isDirectory) return false; 25 | const extname = path.extname(filePath); 26 | return extname === '.txt' || extname === '.conf'; 27 | }) 28 | .crawl(SOURCE_DIR + path.sep + 'domainset') 29 | .withPromise(), 30 | new Fdir() 31 | .withFullPaths() 32 | .filter((filePath, isDirectory) => { 33 | if (isDirectory) return false; 34 | const extname = path.extname(filePath); 35 | return extname === '.txt' || extname === '.conf'; 36 | }) 37 | .crawl(SOURCE_DIR + path.sep + 'non_ip') 38 | .withPromise() 39 | ]); 40 | 41 | const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); 42 | bar.start(0, 0); 43 | 44 | await Promise.all([ 45 | ...domainRules, 46 | ...domainSets 47 | ].map(filepath => runAgainstSourceFile( 48 | filepath, 49 | (domain: string, includeAllSubdomain: boolean) => { 50 | bar.setTotal(bar.getTotal() + 1); 51 | 52 | return queue.add(async () => { 53 | let registerableDomainAlive, registerableDomain, alive: boolean | undefined; 54 | 55 | if (includeAllSubdomain) { 56 | // we only need to check apex domain, because we don't know if there is any stripped subdomain 57 | ({ alive: registerableDomainAlive, registerableDomain } = await isRegisterableDomainAlive(domain)); 58 | } else { 59 | ({ alive, registerableDomainAlive, registerableDomain } = await isDomainAlive(domain)); 60 | } 61 | 62 | bar.increment(); 63 | 64 | if (!registerableDomainAlive) { 65 | if (registerableDomain) { 66 | deadDomains.push('.' + registerableDomain); 67 | } 68 | } else if (!includeAllSubdomain && alive != null && !alive) { 69 | deadDomains.push(domain); 70 | } 71 | }); 72 | } 73 | ).then(() => console.log('[crawl]', filepath)))); 74 | 75 | await queue.done(); 76 | 77 | bar.stop(); 78 | 79 | console.log(); 80 | console.log(); 81 | console.log(JSON.stringify(deadDomains)); 82 | })(); 83 | -------------------------------------------------------------------------------- /Build/lib/fetch-assets.ts: -------------------------------------------------------------------------------- 1 | import picocolors from 'picocolors'; 2 | import { $$fetch, defaultRequestInit, ResponseError } from './fetch-retry'; 3 | import { waitWithAbort } from 'foxts/wait'; 4 | import { nullthrow } from 'foxts/guard'; 5 | import { TextLineStream } from 'foxts/text-line-stream'; 6 | import { ProcessLineStream } from './process-line'; 7 | import { AdGuardFilterIgnoreUnsupportedLinesStream } from './parse-filter/filters'; 8 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 9 | 10 | import { newQueue } from '@henrygd/queue'; 11 | import { AbortError } from 'foxts/abort-error'; 12 | 13 | const reusedCustomAbortError = new AbortError(); 14 | 15 | const queue = newQueue(16); 16 | 17 | export async function fetchAssets( 18 | url: string, fallbackUrls: null | undefined | string[] | readonly string[], 19 | processLine = false, allowEmpty = false, filterAdGuardUnsupportedLines = false 20 | ) { 21 | const controller = new AbortController(); 22 | 23 | const createFetchFallbackPromise = async (url: string, index: number) => { 24 | if (index >= 0) { 25 | // To avoid wasting bandwidth, we will wait for a few time before downloading from the fallback URL. 26 | try { 27 | await waitWithAbort(1800 + (index + 1) * 1200, controller.signal); 28 | } catch { 29 | throw reusedCustomAbortError; 30 | } 31 | } 32 | if (controller.signal.aborted) { 33 | throw reusedCustomAbortError; 34 | } 35 | if (index >= 0) { 36 | console.log(picocolors.yellowBright('[fetch fallback begin]'), picocolors.gray(url)); 37 | } 38 | 39 | // we don't queue add here 40 | const res = await $$fetch(url, { signal: controller.signal, ...defaultRequestInit }); 41 | 42 | let stream = nullthrow(res.body, url + ' has an empty body') 43 | .pipeThrough(new TextDecoderStream()) 44 | .pipeThrough(new TextLineStream({ skipEmptyLines: processLine })); 45 | if (processLine) { 46 | stream = stream.pipeThrough(new ProcessLineStream()); 47 | } 48 | if (filterAdGuardUnsupportedLines) { 49 | stream = stream.pipeThrough(new AdGuardFilterIgnoreUnsupportedLinesStream()); 50 | } 51 | 52 | // we does queue during downloading 53 | const arr = await queue.add(() => Array.fromAsync(stream)); 54 | 55 | if (arr.length < 1 && !allowEmpty) { 56 | throw new ResponseError(res, url, 'empty response w/o 304'); 57 | } 58 | 59 | controller.abort(); 60 | return arr; 61 | }; 62 | 63 | const primaryPromise = createFetchFallbackPromise(url, -1); 64 | 65 | if (!fallbackUrls || fallbackUrls.length === 0) { 66 | return primaryPromise; 67 | } 68 | return Promise.any( 69 | appendArrayInPlace( 70 | [primaryPromise], 71 | fallbackUrls.map(createFetchFallbackPromise) 72 | ) 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /Source/non_ip/microsoft.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Microsoft Domains 2 | # $ meta_description This file contains domains of Microsoft. 3 | 4 | DOMAIN,officecdn-microsoft-com.akamaized.net 5 | DOMAIN-KEYWORD,1drv 6 | DOMAIN-KEYWORD,microsoft 7 | DOMAIN-SUFFIX,1drv.com 8 | DOMAIN-SUFFIX,aadrm.com 9 | DOMAIN-SUFFIX,acompli.com 10 | DOMAIN-SUFFIX,acompli.net 11 | DOMAIN-SUFFIX,aka.ms 12 | DOMAIN-SUFFIX,appcenter.ms 13 | DOMAIN-SUFFIX,aria.ms 14 | DOMAIN-SUFFIX,asp.net 15 | DOMAIN-SUFFIX,aspnetcdn.com 16 | DOMAIN-SUFFIX,assets-yammer.com 17 | DOMAIN-SUFFIX,azure.com 18 | DOMAIN-SUFFIX,azure.net 19 | DOMAIN-SUFFIX,azureedge.net 20 | DOMAIN-SUFFIX,azurerms.com 21 | DOMAIN-SUFFIX,bing.com 22 | DOMAIN-SUFFIX,clarity.ms 23 | DOMAIN-SUFFIX,cloudapp.net 24 | DOMAIN-SUFFIX,cloudappsecurity.com 25 | DOMAIN-SUFFIX,docs.com 26 | DOMAIN-SUFFIX,edgesuite.net 27 | DOMAIN-SUFFIX,gfx.ms 28 | DOMAIN-KEYWORD,hotmail 29 | DOMAIN-SUFFIX,hotmail.com 30 | DOMAIN-SUFFIX,live.com 31 | DOMAIN-SUFFIX,live.net 32 | DOMAIN-SUFFIX,lync.com 33 | DOMAIN-SUFFIX,microsoft.com 34 | DOMAIN-SUFFIX,microsoftonline.com 35 | DOMAIN-SUFFIX,msappproxy.net 36 | DOMAIN-SUFFIX,msauth.net 37 | DOMAIN-SUFFIX,msauthimages.net 38 | DOMAIN-SUFFIX,msecnd.net 39 | DOMAIN-SUFFIX,msedge.net 40 | DOMAIN-SUFFIX,msft.net 41 | DOMAIN-SUFFIX,msftauth.net 42 | DOMAIN-SUFFIX,msftauthimages.net 43 | DOMAIN-SUFFIX,msftidentity.com 44 | DOMAIN-SUFFIX,msidentity.com 45 | DOMAIN-SUFFIX,msn.com 46 | DOMAIN-SUFFIX,msocdn.com 47 | DOMAIN-SUFFIX,msocsp.com 48 | DOMAIN-SUFFIX,mstea.ms 49 | DOMAIN-SUFFIX,o365weve.com 50 | DOMAIN-SUFFIX,oaspapps.com 51 | DOMAIN-SUFFIX,office.com 52 | DOMAIN-SUFFIX,office.net 53 | DOMAIN-SUFFIX,office365.com 54 | DOMAIN-SUFFIX,officeppe.net 55 | DOMAIN-SUFFIX,omniroot.com 56 | DOMAIN-SUFFIX,onedrive.com 57 | DOMAIN-SUFFIX,onenote.com 58 | DOMAIN-SUFFIX,onenote.net 59 | DOMAIN-SUFFIX,onestore.ms 60 | DOMAIN-SUFFIX,outlook.com 61 | DOMAIN-SUFFIX,outlookmobile.com 62 | DOMAIN-SUFFIX,phonefactor.net 63 | DOMAIN-SUFFIX,public-trust.com 64 | DOMAIN-SUFFIX,sfbassets.com 65 | DOMAIN-SUFFIX,sfx.ms 66 | DOMAIN-SUFFIX,sharepoint.com 67 | DOMAIN-SUFFIX,sharepointonline.com 68 | DOMAIN-SUFFIX,skype.com 69 | DOMAIN-SUFFIX,skypeassets.com 70 | DOMAIN-SUFFIX,skypeforbusiness.com 71 | DOMAIN-SUFFIX,staffhub.ms 72 | DOMAIN-SUFFIX,svc.ms 73 | DOMAIN-SUFFIX,sway-cdn.com 74 | DOMAIN-SUFFIX,sway-extensions.com 75 | DOMAIN-SUFFIX,sway.com 76 | DOMAIN-SUFFIX,trafficmanager.net 77 | DOMAIN-SUFFIX,uservoice.com 78 | DOMAIN-SUFFIX,virtualearth.net 79 | DOMAIN-SUFFIX,visualstudio.com 80 | DOMAIN-SUFFIX,windows-ppe.net 81 | DOMAIN-SUFFIX,windows.com 82 | DOMAIN-SUFFIX,windows.net 83 | DOMAIN-SUFFIX,windowsazure.com 84 | DOMAIN-SUFFIX,windowsupdate.com 85 | DOMAIN-SUFFIX,wunderlist.com 86 | DOMAIN-SUFFIX,xbox.com 87 | DOMAIN-SUFFIX,xboxlive.com 88 | DOMAIN-SUFFIX,xboxservices.com 89 | DOMAIN-SUFFIX,yammer.com 90 | DOMAIN-SUFFIX,yammerusercontent.com 91 | -------------------------------------------------------------------------------- /Source/domainset/speedtest.conf: -------------------------------------------------------------------------------- 1 | # $ custom_build_script 2 | 3 | # speedtest.net 4 | .speedtest.net 5 | .speedtestcustom.com 6 | .ooklaserver.net 7 | .speed.misaka.one 8 | .speedtest.rt.ru 9 | .speedtest.aptg.com.tw 10 | .speedtest.gslnetworks.com 11 | .speedtest.jsinfo.net 12 | .speedtest.i3d.net 13 | .speedtest.telus.com 14 | .speedtest.telstra.net 15 | .speedtest.clouvider.net 16 | .speedtest.idv.tw 17 | .speedtest.frontier.com 18 | .speedtest.orange.fr 19 | .speedtest.centurylink.net 20 | .srvr.bell.ca 21 | .speedtest.contabo.net 22 | speedtest.hk.chinamobile.com 23 | speedtestbb.hk.chinamobile.com 24 | .hizinitestet.com 25 | .linknetspeedtest.net.br 26 | speedtest.rit.edu 27 | speedtest.ropa.de 28 | speedtest.sits.su 29 | speedtest.tigo.cr 30 | .speedtest.pni.tw 31 | .speed.pfm.gg 32 | .speedtest.faelix.net 33 | .speedtest.labixe.net 34 | .speedtest.warian.net 35 | .speedtest.starhub.com 36 | .speedtest.gibir.net.tr 37 | .speedtest.ozarksgo.net 38 | .speedtest.exetel.com.au 39 | .speedtest.sbcglobal.net 40 | .speedtest.leaptel.com.au 41 | .speedtest.windstream.net 42 | .speedtest.vodafone.com.au 43 | .speedtest.rascom.ru 44 | .speedtest.dchost.com 45 | .speedtest.highnet.com 46 | .speedtest.seattle.wa.limewave.net 47 | .speedtest.optitel.com.au 48 | .speednet.net.tr 49 | .speedtest.angolacables.co.ao 50 | .ookla-speedtest.fsr.com 51 | .speedtest.comnet.com.tr 52 | .speedtest.gslnetworks.com.au 53 | .test.gslnetworks.com.au 54 | .speedtestunonet.com.br 55 | .speedtest.alagas.net 56 | speedtest.surfshark.com 57 | .speedtest.aarnet.net.au 58 | .ookla.rcp.net 59 | .ookla-speedtests.e2ro.com 60 | .speedtest.com.sg 61 | .ookla.ddnsgeek.com 62 | .speedtest.cmcnetworks.net 63 | .speedtest.moack.co.kr 64 | speedtest.mtnetworks.mn 65 | .speedtest.waicore.com 66 | .speedtest.love 67 | .speedtest.6667890.xyz 68 | .speedtest.kansuiun.com 69 | .speedtest.tjokas.com 70 | .speedtest.timeweb.ru 71 | .speedtest.vianet.ca 72 | .speedtest.vipnetconexao.com.br 73 | # Cloudflare 74 | .speed.cloudflare.com 75 | # Wi-Fi Man 76 | .wifiman.com 77 | .wifiman.me 78 | .wifiman.ubncloud.com 79 | .wifiman-stability-test.ubncloud.com 80 | # Fast.com 81 | .fast.com 82 | # MacPaw 83 | speedtest.macpaw.com 84 | # speedtestmaster 85 | .netspeedtestmaster.com 86 | # Google Search Result of "speedtest", powered by this 87 | .measurement-lab.org 88 | .measurementlab.net 89 | # Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com) 90 | .speed.googlefiber.net 91 | # librespeed 92 | .backend.librespeed.org 93 | # Apple (From netQuality command) 94 | mensura.cdn-apple.com 95 | # OpenSpeedtest (This is also used for openspeedtest server download) 96 | open.cachefly.net 97 | # Linode 98 | .speedtest.linode.com 99 | # www.bandwidthplace.com 100 | .mlab-oti.measurement-lab.org 101 | # nperf 102 | .nperf.net 103 | # HiNet 104 | .speed.hinet.net 105 | -------------------------------------------------------------------------------- /Build/build-speedtest-domainset.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import tldts from 'tldts-experimental'; 4 | import { task } from './trace'; 5 | import { SHARED_DESCRIPTION } from './constants/description'; 6 | import { readFileIntoProcessedArray } from './lib/fetch-text-by-line'; 7 | 8 | import { DomainsetOutput } from './lib/rules/domainset'; 9 | import { OUTPUT_SURGE_DIR, SOURCE_DIR } from './constants/dir'; 10 | import { $$fetch } from './lib/fetch-retry'; 11 | 12 | import { fastUri } from 'fast-uri'; 13 | 14 | interface SpeedTestServer { 15 | url: string, 16 | lat: string, 17 | lon: string, 18 | distance: number, 19 | name: string, 20 | country: string, 21 | cc: string, 22 | sponsor: string, 23 | id: string, 24 | preferred: number, 25 | https_functional: number, 26 | host: string 27 | } 28 | 29 | const getSpeedtestHostsGroupsPromise = $$fetch('https://speedtest-net-servers.cdn.skk.moe/servers.json') 30 | .then(res => res.json() as Promise) 31 | .then((data) => data.reduce((prev, cur) => { 32 | let hn: string | null | undefined = null; 33 | if (cur.host) { 34 | hn = tldts.getHostname(cur.host, { detectIp: false, validateHostname: true }); 35 | if (hn) { 36 | prev.push(hn); 37 | } 38 | } 39 | if (cur.url) { 40 | hn = fastUri.parse(cur.url).host; 41 | if (hn) { 42 | prev.push(hn); 43 | } 44 | } 45 | return prev; 46 | }, [])); 47 | 48 | interface LibreSpeedServerInfo { 49 | name: string, 50 | dlURL: string, 51 | ulURL: string, 52 | pingURL: string, 53 | getIpURL: string, 54 | server: string, 55 | sponsorName: string 56 | } 57 | 58 | const getLibrespeedBackendsPromise = $$fetch('https://speedtest-net-servers.cdn.skk.moe/librespeed-servers.json') 59 | .then(res => res.json() as Promise) 60 | .then((data) => data.reduce((prev, cur) => { 61 | let hn: string | null | undefined = null; 62 | if (cur.server) { 63 | hn = fastUri.parse(cur.server).host; 64 | if (hn) { 65 | prev.push(hn); 66 | } 67 | } 68 | 69 | return prev; 70 | }, [])); 71 | 72 | export const buildSpeedtestDomainSet = task(require.main === module, __filename)( 73 | async (span) => new DomainsetOutput(span, 'speedtest') 74 | .withTitle('Sukka\'s Ruleset - Speedtest Domains') 75 | .appendDescription( 76 | SHARED_DESCRIPTION, 77 | '', 78 | 'This file contains common speedtest endpoints.' 79 | ) 80 | .addFromDomainset(readFileIntoProcessedArray(path.resolve(SOURCE_DIR, 'domainset/speedtest.conf'))) 81 | .addFromDomainset(readFileIntoProcessedArray(path.resolve(OUTPUT_SURGE_DIR, 'domainset/speedtest.conf'))) 82 | .bulkAddDomain(await span.traceChildPromise('get speedtest.test servers', getSpeedtestHostsGroupsPromise)) 83 | .bulkAddDomain(await span.traceChildPromise('get librespeed backends', getLibrespeedBackendsPromise)) 84 | .write() 85 | ); 86 | -------------------------------------------------------------------------------- /Build/tools-migrate-domains.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars -- some unused methods */ 2 | import path from 'node:path'; 3 | import { processFilterRulesWithPreload } from './lib/parse-filter/filters'; 4 | import { processHosts } from './lib/parse-filter/hosts'; 5 | import { HostnameSmolTrie } from './lib/trie'; 6 | import { dummySpan } from './trace'; 7 | import { SOURCE_DIR } from './constants/dir'; 8 | import { PREDEFINED_WHITELIST } from './constants/reject-data-source'; 9 | import runAgainstSourceFile from './lib/run-against-source-file'; 10 | 11 | (async () => { 12 | const trie = new HostnameSmolTrie(); 13 | 14 | await writeHostsToTrie(trie, 'https://cdn.jsdelivr.net/gh/jerryn70/GoodbyeAds@master/Extension/GoodbyeAds-Xiaomi-Extension.txt', true); 15 | // const { whiteDomainSuffixes, whiteDomains } = await writeFiltersToTrie(trie, 'https://cdn.jsdelivr.net/gh/Perflyst/PiHoleBlocklist@master/SmartTV-AGH.txt', true); 16 | 17 | const callback = (domain: string, includeAllSubDomain: boolean) => { 18 | trie.whitelist(domain, includeAllSubDomain); 19 | }; 20 | 21 | await runAgainstSourceFile(path.join(SOURCE_DIR, 'domainset', 'reject.conf'), callback, 'domainset'); 22 | await runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip', 'reject.conf'), callback, 'ruleset'); 23 | 24 | for (let i = 0, len = PREDEFINED_WHITELIST.length; i < len; i++) { 25 | trie.whitelist(PREDEFINED_WHITELIST[i]); 26 | } 27 | 28 | console.log('---------------------------'); 29 | console.log(trie.dump().join('\n')); 30 | console.log('---------------------------'); 31 | // console.log('whitelist domain suffixes:'); 32 | // console.log(whiteDomainSuffixes.join('\n')); 33 | // console.log('---------------------------'); 34 | // console.log('whitelist domains:'); 35 | // console.log(whiteDomains.join('\n')); 36 | })(); 37 | 38 | async function writeHostsToTrie(trie: HostnameSmolTrie, hostsUrl: string, includeAllSubDomain = false) { 39 | const hosts = await processHosts(dummySpan, hostsUrl, [], includeAllSubDomain); 40 | 41 | for (let i = 0, len = hosts.length; i < len; i++) { 42 | trie.add(hosts[i]); 43 | } 44 | } 45 | 46 | async function writeFiltersToTrie(trie: HostnameSmolTrie, filterUrl: string, includeThirdParty = false) { 47 | const { whiteDomainSuffixes, whiteDomains, blackDomainSuffixes, blackDomains } = await processFilterRulesWithPreload(filterUrl, [], includeThirdParty)(dummySpan); 48 | for (let i = 0, len = blackDomainSuffixes.length; i < len; i++) { 49 | trie.add(blackDomainSuffixes[i], true); 50 | } 51 | for (let i = 0, len = blackDomains.length; i < len; i++) { 52 | trie.add(blackDomains[i], false); 53 | } 54 | for (let i = 0, len = whiteDomainSuffixes.length; i < len; i++) { 55 | trie.whitelist(whiteDomainSuffixes[i], true); 56 | } 57 | for (let i = 0, len = whiteDomains.length; i < len; i++) { 58 | trie.whitelist(whiteDomains[i], false); 59 | } 60 | 61 | return { whiteDomainSuffixes, whiteDomains }; 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/check-source-domain.yml: -------------------------------------------------------------------------------- 1 | name: Check Domain Availability 2 | on: 3 | # manual trigger only 4 | workflow_dispatch: 5 | 6 | jobs: 7 | check: 8 | name: Check 9 | runs-on: ubuntu-24.04-arm 10 | 11 | steps: 12 | # - name: Tune GitHub-hosted runner network 13 | # # https://github.com/actions/runner-images/issues/1187 14 | # uses: smorimoto/tune-github-hosted-runner-network@v1 15 | - uses: actions/checkout@v5 16 | with: 17 | persist-credentials: false 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | run_install: false 21 | - uses: actions/setup-node@v5 22 | with: 23 | node-version-file: ".node-version" 24 | cache: "pnpm" 25 | - name: Get current date 26 | id: date 27 | run: | 28 | echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT 29 | echo "year=$(date +'%Y')" >> $GITHUB_OUTPUT 30 | echo "month=$(date +'%m')" >> $GITHUB_OUTPUT 31 | echo "day=$(date +'%d')" >> $GITHUB_OUTPUT 32 | echo "hour=$(date +'%H')" >> $GITHUB_OUTPUT 33 | echo "minute=$(date +'%M')" >> $GITHUB_OUTPUT 34 | echo "second=$(date +'%S')" >> $GITHUB_OUTPUT 35 | - name: Restore cache.db 36 | uses: actions/cache/restore@v4 37 | id: cache-db-restore 38 | with: 39 | path: | 40 | .cache 41 | key: ${{ runner.os }}-v3-${{ steps.date.outputs.year }}-${{ steps.date.outputs.month }}-${{ steps.date.outputs.day }} ${{ steps.date.outputs.hour }}:${{ steps.date.outputs.minute }}:${{ steps.date.outputs.second }} 42 | # If source files changed but packages didn't, rebuild from a prior cache. 43 | restore-keys: | 44 | ${{ runner.os }}-v3-${{ steps.date.outputs.year }}-${{ steps.date.outputs.month }}-${{ steps.date.outputs.day }} ${{ steps.date.outputs.hour }}:${{ steps.date.outputs.minute }}: 45 | ${{ runner.os }}-v3-${{ steps.date.outputs.year }}-${{ steps.date.outputs.month }}-${{ steps.date.outputs.day }} ${{ steps.date.outputs.hour }}: 46 | ${{ runner.os }}-v3-${{ steps.date.outputs.year }}-${{ steps.date.outputs.month }}-${{ steps.date.outputs.day }} 47 | ${{ runner.os }}-v3-${{ steps.date.outputs.year }}-${{ steps.date.outputs.month }}- 48 | ${{ runner.os }}-v3-${{ steps.date.outputs.year }}- 49 | ${{ runner.os }}-v3- 50 | - run: pnpm install 51 | - run: pnpm run node Build/validate-domain-alive.ts 52 | env: 53 | DEBUG: domain-alive:dead-domain,domain-alive:error:* 54 | - name: Cache cache.db 55 | if: always() 56 | uses: actions/cache/save@v4 57 | with: 58 | path: | 59 | .cache 60 | key: ${{ runner.os }}-v3-${{ steps.date.outputs.year }}-${{ steps.date.outputs.month }}-${{ steps.date.outputs.day }} ${{ steps.date.outputs.hour }}:${{ steps.date.outputs.minute }}:${{ steps.date.outputs.second }} 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ruleset.skk.moe", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/SukkaW/Surge.git" 9 | }, 10 | "type": "commonjs", 11 | "scripts": { 12 | "node": "SWC_NODE_IGNORE_DYNAMIC=true node -r @swc-node/register", 13 | "dexnode": "SWC_NODE_IGNORE_DYNAMIC=true dexnode -r @swc-node/register", 14 | "build": "pnpm run node ./Build/index.ts", 15 | "build-profile": "pnpm run dexnode -r @swc-node/register ./Build/index.ts", 16 | "lint": "eslint --format=sukka .", 17 | "test": "SWC_NODE_IGNORE_DYNAMIC=true SWC_NODE_PROJECT=tsconfig.test.json mocha --require @swc-node/register --watch-extensions ts,tsx" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "@ghostery/adblocker": "^2.13.1", 23 | "@henrygd/queue": "^1.2.0", 24 | "@mitata/counters": "^0.0.8", 25 | "better-sqlite3": "^12.5.0", 26 | "ci-info": "^4.3.1", 27 | "cli-progress": "^3.12.0", 28 | "csv-parse": "^6.1.0", 29 | "dns2": "github:lsongdev/node-dns#e4fa035aca0b8eb730bde3431fbf0c60a31a09c9", 30 | "domain-alive": "^0.1.11", 31 | "fast-cidr-tools": "^0.3.4", 32 | "fast-escape-regexp": "^1.0.1", 33 | "fast-uri": "^3.1.0", 34 | "fdir": "^6.5.0", 35 | "foxts": "^5.0.4", 36 | "hash-wasm": "^4.12.0", 37 | "json-stringify-pretty-compact": "4.0.0", 38 | "null-prototype-object": "^1.2.5", 39 | "picocolors": "^1.1.1", 40 | "tar-fs": "^3.1.1", 41 | "telegram": "^2.26.22", 42 | "tinyglobby": "^0.2.15", 43 | "tldts": "^7.0.19", 44 | "tldts-experimental": "^7.0.19", 45 | "undici": "^7.16.0", 46 | "undici-cache-store-better-sqlite3": "^1.0.0", 47 | "why-is-node-running": "^3.2.2", 48 | "worktank": "^3.0.2", 49 | "xbits": "^0.2.0", 50 | "yaml": "^2.8.2", 51 | "yauzl-promise": "^4.0.0" 52 | }, 53 | "devDependencies": { 54 | "@eslint-sukka/node": "^8.0.6", 55 | "@swc-node/register": "^1.11.1", 56 | "@swc/core": "1.13.5", 57 | "@types/better-sqlite3": "^7.6.13", 58 | "@types/cli-progress": "^3.11.6", 59 | "@types/dns2": "^2.0.10", 60 | "@types/mocha": "^10.0.10", 61 | "@types/node": "^24.10.4", 62 | "@types/tar-fs": "^2.0.4", 63 | "@types/yauzl-promise": "^4.0.1", 64 | "eslint": "^9.39.2", 65 | "eslint-config-sukka": "^8.0.6", 66 | "eslint-formatter-sukka": "^8.0.6", 67 | "expect": "^30.2.0", 68 | "mitata": "^1.0.34", 69 | "mocha": "^11.7.5", 70 | "tinyexec": "^1.0.2", 71 | "typescript": "^5.9.3" 72 | }, 73 | "packageManager": "pnpm@10.26.1", 74 | "pnpm": { 75 | "onlyBuiltDependencies": [ 76 | "@swc/core", 77 | "better-sqlite3", 78 | "oxc-resolver", 79 | "unrs-resolver" 80 | ], 81 | "overrides": { 82 | "eslint>chalk": "npm:picocolors@^1.1.1", 83 | "globalthis": "npm:@nolyfill/globalthis@^1.0.44", 84 | "has": "npm:@nolyfill/has@^1.0.44", 85 | "safe-buffer": "npm:@nolyfill/safe-buffer@^1.0.44" 86 | }, 87 | "ignoredBuiltDependencies": [ 88 | "bufferutil", 89 | "es5-ext", 90 | "utf-8-validate" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Build/lib/is-domain-alive.ts: -------------------------------------------------------------------------------- 1 | import { createDomainAliveChecker, createRegisterableDomainAliveChecker } from 'domain-alive'; 2 | import { $$fetch } from './fetch-retry'; 3 | 4 | const dnsServers = [ 5 | '8.8.8.8', '8.8.4.4', 6 | '1.0.0.1', '1.1.1.1', 7 | '162.159.36.1', '162.159.46.1', 8 | 'dns.cloudflare.com', // Cloudflare DoH that uses different IPs: 172.64.41.8,162.159.61.8 9 | 'cloudflare-dns.com', // Cloudflare DoH that uses different IPs: 104.16.249.249,104.16.248.249 10 | 'mozilla.cloudflare-dns.com', // Cloudflare DoH that uses different IPs: 162.159.61.4,172.64.41.4 11 | // one.one.one.one // Cloudflare DoH that uses 1.1.1.1 and 1.0.0.1 12 | // '101.101.101.101', 'dns.twnic.tw' // TWNIC, has DNS pollution, e.g. t66y.com 13 | // 'dns.hinet.net' // HiNet DoH, has DNS pollution, e.g. t66y.com 14 | '185.222.222.222', '45.11.45.11', // DNS.SB 15 | // 'doh.dns.sb', // DNS.SB, Unicast PoPs w/ GeoDNS 16 | 'us-chi.doh.sb', // DNS.SB Chicago PoP 17 | 'us-nyc.doh.sb', // DNS.SB New York City PoP 18 | 'us-sjc.doh.sb', // DNS.SB San Jose PoP 19 | // 'doh.sb', // DNS.SB xTom Anycast IP 20 | // 'dns.sb', // DNS.SB use same xTom Anycast IP as doh.sb 21 | // 'dns10.quad9.net', // Quad9 unfiltered 22 | '9.9.9.10', '149.112.112.10', // Quad9 unfiltered 23 | 'doh.sandbox.opendns.com', // OpenDNS sandbox (unfiltered) 24 | 'unfiltered.adguard-dns.com', // AdGuard unfiltered 25 | // 'v.recipes', // Proxy Cloudflare, too many HTTP 503 26 | // '76.76.2.0', // ControlD unfiltered, path not /dns-query 27 | // '76.76.10.0', // ControlD unfiltered, path not /dns-query 28 | // 'dns.bebasid.com', // BebasID, path not /dns-query but /unfiltered 29 | // '193.110.81.0', // dns0.eu 30 | // '185.253.5.0', // dns0.eu 31 | // 'zero.dns0.eu', 32 | 'dns.nextdns.io', 33 | 'anycast.dns.nextdns.io', 34 | 'wikimedia-dns.org', 35 | // 'ordns.he.net', 36 | // 'dns.mullvad.net', empty HTTP body a lot 37 | 'basic.rethinkdns.com', 38 | 'dns.surfsharkdns.com', 39 | // 'private.canadianshield.cira.ca', enforce HTTP/2 40 | // 'unfiltered.joindns4.eu', // too many ECONNRESET on GitHub Actions 41 | 'public.dns.iij.jp', 42 | // 'common.dot.dns.yandex.net', // too many ECONNRESET on GitHub Actions 43 | 'safeservedns.com' // NameCheap DNS, supports DoT, DoH, UDP53 44 | // 'ada.openbld.net', 45 | // 'dns.rabbitdns.org' 46 | ].map(dns => 'https://' + dns + '/dns-query'); 47 | 48 | const resultCache = new Map(); 49 | const registerableDomainResultCache = new Map(); 50 | 51 | export async function getMethods() { 52 | const customWhoisServersMapping = await (await ($$fetch('https://cdn.jsdelivr.net/npm/whois-servers-list@latest/list.json'))).json() as any; 53 | 54 | const isRegisterableDomainAlive = createRegisterableDomainAliveChecker({ 55 | dns: { 56 | dnsServers, 57 | maxAttempts: 6 58 | }, 59 | registerableDomainResultCache, 60 | whois: { 61 | customWhoisServersMapping 62 | } 63 | }); 64 | 65 | const isDomainAlive = createDomainAliveChecker({ 66 | dns: { 67 | dnsServers, 68 | maxAttempts: 6 69 | }, 70 | registerableDomainResultCache, 71 | resultCache, 72 | whois: { 73 | customWhoisServersMapping 74 | } 75 | }); 76 | 77 | return { isRegisterableDomainAlive, isDomainAlive }; 78 | }; 79 | -------------------------------------------------------------------------------- /Source/non_ip/ai.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - AIGC Domains 2 | # $ meta_description This file contains domains of OpenAI, Claude. 3 | 4 | # >> OpenAI / ChatGPT 5 | # OpenAI A/B Flag, no geo blocking 6 | # DOMAIN,api.statsig.com 7 | DOMAIN-SUFFIX,openai.com 8 | DOMAIN-SUFFIX,oaistatic.com 9 | DOMAIN-SUFFIX,sora.com 10 | DOMAIN-SUFFIX,chatgpt.com 11 | DOMAIN-SUFFIX,chat.com 12 | DOMAIN-SUFFIX,ai.com 13 | DOMAIN-KEYWORD,openai 14 | # DOMAIN-SUFFIX,openaiapi-site.azureedge.net 15 | # DOMAIN,openaicom-api-bdcpf8c6d2e9atf6.z01.azurefd.net 16 | # DOMAIN,openaicomproductionae4b.blob.core.windows.net 17 | # DOMAIN,production-openaicom-storage.azureedge.net 18 | 19 | # >> Apple Intelligence 20 | # has been moved to a dedicated file: /non_ip/apple_inteligence.conf 21 | # DOMAIN gspe1-ssl.ls.apple.com (which is part of com.apple.geod) is crucial for unlocking 22 | # Apple Intelligence. However, this domain (along side com.apple.geod) also affects other 23 | # Apple services like Map, Compass, Weather, etc. Therefore, we can't really include everything 24 | # in this general "ai.conf" here. 25 | 26 | # >> Perplexity 27 | DOMAIN-SUFFIX,perplexity.ai 28 | 29 | # >> Claude 30 | DOMAIN-SUFFIX,anthropic.com 31 | DOMAIN-SUFFIX,claude.ai 32 | DOMAIN-SUFFIX,claude.com 33 | 34 | # >> Google 35 | # Gemini 36 | DOMAIN-SUFFIX,bard.google.com 37 | DOMAIN-SUFFIX,gemini.google 38 | DOMAIN-SUFFIX,gemini.google.com 39 | DOMAIN,aisandbox-pa.googleapis.com 40 | DOMAIN,robinfrontend-pa.googleapis.com 41 | # DeepMind 42 | DOMAIN-SUFFIX,deepmind.com 43 | DOMAIN-SUFFIX,deepmind.google 44 | # Generative Language API 45 | DOMAIN-SUFFIX,generativelanguage.googleapis.com 46 | DOMAIN-SUFFIX,geller-pa.googleapis.com 47 | DOMAIN-SUFFIX,proactivebackend-pa.googleapis.com 48 | DOMAIN-SUFFIX,cloudcode-pa.googleapis.com 49 | # Google AI Studio 50 | DOMAIN,ai.google.dev 51 | DOMAIN-SUFFIX,aistudio.google.com 52 | DOMAIN-SUFFIX,makersuite.google.com 53 | DOMAIN,alkalicore-pa.clients6.google.com 54 | DOMAIN-KEYWORD,alkalimakersuite-pa.clients6.google.com 55 | DOMAIN-SUFFIX,generativeai.google 56 | # NotebookLM 57 | DOMAIN-SUFFIX,notebooklm.google 58 | DOMAIN-SUFFIX,notebooklm.google.com 59 | # Jules 60 | DOMAIN,jules.google.com 61 | # Android Studio 62 | DOMAIN,aida.googleapis.com 63 | # Antigravity 64 | DOMAIN,antigravity.google 65 | 66 | # >> POE 67 | DOMAIN-SUFFIX,poe.com 68 | 69 | # >> Meta AI 70 | DOMAIN-SUFFIX,meta.ai 71 | 72 | # >> Cloudflare AI Gateway 73 | DOMAIN,gateway.ai.cloudflare.com 74 | 75 | # >> Dify.AI 76 | DOMAIN-SUFFIX,dify.ai 77 | 78 | # >> Jasper 79 | DOMAIN-SUFFIX,clipdrop.co 80 | DOMAIN-SUFFIX,jasper.ai 81 | 82 | # >> OpenArt 83 | DOMAIN-SUFFIX,openart.ai 84 | 85 | # >> GitHub Copilot 86 | # api.*.githubcopilot.com is not required, since the ip & asn information are coming from api.github.com 87 | # see https://github.com/SukkaW/Surge/pull/46#discussion_r1849641627 88 | DOMAIN,api.github.com 89 | 90 | # >> Grok 91 | DOMAIN-SUFFIX,grok.com 92 | DOMAIN-SUFFIX,x.ai 93 | 94 | # >> groq 95 | DOMAIN-SUFFIX,groq.com 96 | 97 | # >> JetBrains AI 98 | DOMAIN,api.jetbrains.ai 99 | 100 | # >> OpenRouter AI - Unified Relay 101 | # see https://github.com/SukkaW/Surge/pull/86 for reason that only root domain get proxied 102 | DOMAIN,openrouter.ai 103 | -------------------------------------------------------------------------------- /Build/lib/normalize-domain.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/remusao/tldts/issues/2121 2 | // In short, single label domain suffix is ignored due to the size optimization, so no isIcann 3 | // import tldts from 'tldts-experimental'; 4 | import tldts from 'tldts'; 5 | import { normalizeTldtsOpt } from '../constants/loose-tldts-opt'; 6 | import { isProbablyIpv4, isProbablyIpv6 } from 'foxts/is-probably-ip'; 7 | 8 | export type TldTsParsed = ReturnType; 9 | 10 | /** 11 | * Skipped the input non-empty check, the `domain` should not be empty. 12 | */ 13 | function fastNormalizeDomainWithoutWwwNoIP(domain: string, parsed: TldTsParsed | null = null) { 14 | // We don't want tldts to call its own "extractHostname" on ip, bail out ip first. 15 | // This function won't run with IP, we can safely set normalizeTldtsOpt.detectIp to false. 16 | 17 | parsed ??= tldts.parse(domain, normalizeTldtsOpt); 18 | // Private invalid domain (things like .tor, .dn42, etc) 19 | if (!parsed.isIcann && !parsed.isPrivate) return null; 20 | 21 | if (parsed.subdomain) { 22 | if ( 23 | parsed.subdomain === 'www' 24 | || parsed.subdomain === 'xml-v4' 25 | || parsed.subdomain === 'xml-eu' 26 | || parsed.subdomain === 'xml-eu-v4' 27 | || (parsed.subdomain.length === 4 && parsed.subdomain.startsWith('www')) 28 | ) { 29 | return parsed.domain; 30 | } 31 | if (parsed.subdomain.startsWith('www.')) { 32 | return parsed.subdomain.slice(4) + '.' + parsed.domain; 33 | } 34 | } 35 | 36 | return parsed.hostname; 37 | } 38 | 39 | /** 40 | * Skipped the input non-empty check, the `domain` should not be empty. 41 | */ 42 | export function fastNormalizeDomainWithoutWww(domain: string, parsed: TldTsParsed | null = null) { 43 | // We don't want tldts to call its own "extractHostname" on ip, bail out ip first. 44 | // Now ip has been bailed out, we can safely set normalizeTldtsOpt.detectIp to false. 45 | if (isProbablyIpv4(domain) || isProbablyIpv6(domain)) { 46 | return null; 47 | } 48 | 49 | return fastNormalizeDomainWithoutWwwNoIP(domain, parsed); 50 | } 51 | 52 | /** 53 | * Skipped the input non-empty check, the `domain` should not be empty. 54 | */ 55 | export function fastNormalizeDomain(domain: string, parsed: TldTsParsed | null = null) { 56 | // We don't want tldts to call its own "extractHostname" on ip, bail out ip first. 57 | // Now ip has been bailed out, we can safely set normalizeTldtsOpt.detectIp to false. 58 | if (isProbablyIpv4(domain) || isProbablyIpv6(domain)) { 59 | return null; 60 | } 61 | 62 | parsed ??= tldts.parse(domain, normalizeTldtsOpt); 63 | // Private invalid domain (things like .tor, .dn42, etc) 64 | if (!parsed.isIcann && !parsed.isPrivate) return null; 65 | 66 | return parsed.hostname; 67 | } 68 | 69 | export function normalizeDomain(domain: string, parsed: TldTsParsed | null = null) { 70 | if (domain.length === 0) return null; 71 | 72 | if (isProbablyIpv4(domain) || isProbablyIpv6(domain)) { 73 | return null; 74 | } 75 | 76 | parsed ??= tldts.parse(domain, normalizeTldtsOpt); 77 | // Private invalid domain (things like .tor, .dn42, etc) 78 | if (!parsed.isIcann && !parsed.isPrivate) return null; 79 | 80 | // const h = parsed.hostname; 81 | // if (h === null) return null; 82 | 83 | return parsed.hostname; 84 | } 85 | -------------------------------------------------------------------------------- /Build/lib/writing-strategy/base.ts: -------------------------------------------------------------------------------- 1 | import type { Span } from '../../trace'; 2 | import { compareAndWriteFile } from '../create-file'; 3 | 4 | /** 5 | * The class is not about holding rule data, instead it determines how the 6 | * date is written to a file. 7 | */ 8 | export abstract class BaseWriteStrategy { 9 | public abstract readonly name: string; 10 | 11 | /** 12 | * Sometimes a ruleset will create extra files (e.g. reject-url-regex w/ mitm.sgmodule), 13 | * and doesn't share the same filename and id. This property is used to overwrite the filename. 14 | */ 15 | public overwriteFilename: string | null = null; 16 | public withFilename(filename: string) { 17 | this.overwriteFilename = filename; 18 | return this; 19 | } 20 | 21 | public abstract readonly type: 'domainset' | 'non_ip' | 'ip' | (string & {}); 22 | 23 | abstract readonly fileExtension: 'conf' | 'txt' | 'json' | 'sgmodule'; /* | (string & {}) */ 24 | 25 | constructor(public readonly outputDir: string) {} 26 | 27 | protected abstract result: string[] | null; 28 | 29 | abstract writeDomain(domain: string): void; 30 | abstract writeDomainSuffix(domain: string): void; 31 | abstract writeDomainKeywords(keyword: Set): void; 32 | abstract writeDomainWildcard(wildcard: string): void; 33 | abstract writeUserAgents(userAgent: Set): void; 34 | abstract writeProcessNames(processName: Set): void; 35 | abstract writeProcessPaths(processPath: Set): void; 36 | abstract writeUrlRegexes(urlRegex: Set): void; 37 | abstract writeIpCidrs(ipCidr: string[], noResolve: boolean): void; 38 | abstract writeIpCidr6s(ipCidr6: string[], noResolve: boolean): void; 39 | abstract writeGeoip(geoip: Set, noResolve: boolean): void; 40 | abstract writeIpAsns(asns: Set, noResolve: boolean): void; 41 | abstract writeSourceIpCidrs(sourceIpCidr: string[]): void; 42 | abstract writeSourcePorts(port: Set): void; 43 | abstract writeDestinationPorts(port: Set): void; 44 | abstract writeProtocols(protocol: Set): void; 45 | abstract writeOtherRules(rule: string[]): void; 46 | 47 | protected abstract withPadding(title: string, description: string[] | readonly string[], date: Date, content: string[]): string[]; 48 | 49 | static readonly domainWildCardToRegex = (domain: string) => { 50 | let result = '^'; 51 | for (let i = 0, len = domain.length; i < len; i++) { 52 | switch (domain[i]) { 53 | case '.': 54 | result += String.raw`\.`; 55 | break; 56 | case '*': 57 | result += String.raw`[\w.-]*?`; 58 | break; 59 | case '?': 60 | result += String.raw`[\w.-]`; 61 | break; 62 | default: 63 | result += domain[i]; 64 | } 65 | } 66 | result += '$'; 67 | return result; 68 | }; 69 | 70 | public output( 71 | span: Span, 72 | title: string, 73 | description: string[] | readonly string[], 74 | date: Date, 75 | filePath: string 76 | ): void | Promise { 77 | if (!this.result) { 78 | return; 79 | } 80 | 81 | return compareAndWriteFile( 82 | span, 83 | this.withPadding( 84 | title, 85 | description, 86 | date, 87 | this.result 88 | ), 89 | filePath 90 | ); 91 | }; 92 | 93 | public get content() { 94 | return this.result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Build/constants/phishing-score-source.ts: -------------------------------------------------------------------------------- 1 | import { createRetrieKeywordFilter as createKeywordFilter } from 'foxts/retrie'; 2 | 3 | export const BLACK_TLD = new Set([ 4 | 'accountant', 'art', 'autos', 5 | 'bar', 'beauty', 'bid', 'bio', 'biz', 'bond', 'business', 'buzz', 6 | 'casa', 'cc', 'cf', 'cfd', 'click', 'cloud', 'club', 'cn', 'codes', 7 | 'co.uk', 'co.in', 'com.br', 'com.cn', 'com.pl', 'com.vn', 8 | 'cool', 'cricket', 'cyou', 9 | 'date', 'design', 'digital', 'download', 10 | 'email', 11 | 'faith', 'fit', 'fun', 12 | 'ga', 'games', 'gd', 'gives', 'gq', 'group', 13 | 'help', 'host', 14 | 'icu', 'id', 'info', 'ink', 15 | 'lat', 'life', 'live', 'link', 'loan', 'lol', 'love', 'ltd', 16 | 'me', 'media', 'men', 'ml', 'mobi', 'movie', 'mom', 'monster', 17 | 'net.pl', 'ninja', 18 | 'one', 'online', 19 | 'party', 'pro', 'pl', 'pw', 20 | 'qpon', 'quest', 21 | 'racing', 'rest', 'review', 'rf.gd', 22 | 'sa.com', 'sbs', 'science', 'shop', 'site', 'skin', 'space', 'store', 'stream', 'su', 'support', 'surf', 23 | 'tech', 'tk', 'tokyo', 'top', 'trade', 24 | 'vip', 'vn', 25 | 'webcam', 'website', 'win', 26 | 'xyz', 27 | 'za.com' 28 | ]); 29 | 30 | export const WHITELIST_MAIN_DOMAINS = new Set([ 31 | // 'w3s.link', // ipfs gateway 32 | // 'dweb.link', // ipfs gateway 33 | // 'nftstorage.link', // ipfs gateway 34 | 'fleek.cool', // ipfs gateway 35 | 'flk-ipfs.xyz', // ipfs gateway 36 | 'business.site', // Drag'n'Drop site building platform 37 | 'page.link', // Firebase URL Shortener 38 | // 'notion.site', d 39 | // 'vercel.app', 40 | 'gitbook.io', 41 | 'zendesk.com', 42 | 'ipfs.eth.aragon.network', 43 | 'wordpress.com', 44 | 'cloud.microsoft', // actually owned by Microsoft 45 | 'windows.net', // Microsoft refuses to add web.core.windows.net to the Public Suffix List 46 | 'myqcloud.com', // curben phishing-filter contains many entries 47 | 'surge.sh' // caused by phishing-filter, also no public suffix 48 | ]); 49 | 50 | export const leathalKeywords = createKeywordFilter([ 51 | 'vinted-', 52 | 'inpost-pl', 53 | 'vlnted-', 54 | 'allegrolokalnie', 55 | 'thetollroads', 56 | 'getipass', 57 | '.secure.txtag', 58 | 59 | '00lx.', 60 | '0-lx.', 61 | '01-x', 62 | '0llx', 63 | '0lx.', 64 | 'olx.o', 65 | 66 | // Fake TLD 67 | '.pl-', 68 | '-pl.', 69 | '.com-', 70 | '-com.', 71 | '.net-', 72 | '.org-', 73 | '.gov-', 74 | '-gov.', 75 | '.au-', 76 | '.co.uk-', 77 | '.de-', 78 | '.eu-', 79 | '.us-', 80 | '.uk-', 81 | '.ru-' 82 | ]); 83 | 84 | export const sensitiveKeywords = createKeywordFilter([ 85 | 'amazon', 86 | 'fb-com', 87 | 'focebaak', 88 | 'facebook', 89 | 'metamask', 90 | 'apple', 91 | 'icloud', 92 | 'coinbase', 93 | 'booking.', 94 | 'booking-', 95 | 'vinted.', 96 | 'vinted-', 97 | 'inpost-pl', 98 | 'microsoft', 99 | 'google', 100 | 'minecraft', 101 | 'staemco', 102 | 'oferta', 103 | 'txtag', 104 | 'paypal', 105 | 'dropbox', 106 | 'payment', 107 | 'instagram' 108 | ]); 109 | 110 | export const lowKeywords = createKeywordFilter([ 111 | 'transactions', 112 | 'wallet', 113 | '-faceb', // facebook fake 114 | '.faceb', // facebook fake 115 | 'virus-', 116 | '-roblox', 117 | '-co-jp', 118 | 'customer.', 119 | 'customer-', 120 | '.www-', 121 | '.www.', 122 | '.www2', 123 | 'microsof', 124 | 'password', 125 | 'recover', 126 | 'banking', 127 | 'shop' 128 | ]); 129 | -------------------------------------------------------------------------------- /Build/build-microsoft-cdn.ts: -------------------------------------------------------------------------------- 1 | import { task } from './trace'; 2 | import { SHARED_DESCRIPTION } from './constants/description'; 3 | import { RulesetOutput } from './lib/rules/ruleset'; 4 | import Worktank from 'worktank'; 5 | import { RULES } from './constants/microsoft-cdn'; 6 | import { wait } from 'foxts/wait'; 7 | 8 | const pool = new Worktank({ 9 | pool: { 10 | name: 'get-microsoft-cdn', 11 | size: 1 // The number of workers to keep in the pool, if more workers are needed they will be spawned up to this limit 12 | }, 13 | worker: { 14 | autoAbort: 10000, 15 | autoTerminate: 30000, // The interval of milliseconds at which to check if the pool can be automatically terminated, to free up resources, workers will be spawned up again if needed 16 | autoInstantiate: true, 17 | methods: { 18 | // eslint-disable-next-line object-shorthand -- workertank 19 | getMicrosoftCdnRuleset: async function (__filename: string): Promise<[domains: string[], domainSuffixes: string[]]> { 20 | // TODO: createRequire is a temporary workaround for https://github.com/nodejs/node/issues/51956 21 | const { default: module } = await import('node:module'); 22 | const __require = module.createRequire(__filename); 23 | 24 | const { HostnameSmolTrie } = __require('./lib/trie'); 25 | const { PROBE_DOMAINS, DOMAINS, DOMAIN_SUFFIXES, BLACKLIST } = __require('./constants/microsoft-cdn') as typeof import('./constants/microsoft-cdn'); 26 | const { fetchRemoteTextByLine } = __require('./lib/fetch-text-by-line') as typeof import('./lib/fetch-text-by-line'); 27 | const { appendArrayInPlace } = __require('foxts/append-array-in-place') as typeof import('foxts/append-array-in-place'); 28 | const { extractDomainsFromFelixDnsmasq } = __require('./lib/parse-dnsmasq') as typeof import('./lib/parse-dnsmasq'); 29 | 30 | const trie = new HostnameSmolTrie(); 31 | 32 | for await (const line of await fetchRemoteTextByLine('https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf')) { 33 | const domain = extractDomainsFromFelixDnsmasq(line); 34 | if (domain) { 35 | trie.add(domain); 36 | } 37 | } 38 | 39 | // remove blacklist domain from trie, to prevent them from being included in the later dump 40 | BLACKLIST.forEach(black => trie.whitelist(black)); 41 | 42 | const domains: string[] = DOMAINS; 43 | const domainSuffixes = appendArrayInPlace(PROBE_DOMAINS.flatMap(domain => trie.find(domain)), DOMAIN_SUFFIXES); 44 | 45 | return [domains, domainSuffixes] as const; 46 | } 47 | } 48 | } 49 | }); 50 | 51 | export const getMicrosoftCdnRulesetPromise = wait(0).then(() => pool.exec( 52 | 'getMicrosoftCdnRuleset', 53 | [__filename] 54 | )).finally(() => pool.terminate()); 55 | 56 | export const buildMicrosoftCdn = task(require.main === module, __filename)(async (span) => { 57 | const [domains, domainSuffixes] = await span.traceChildPromise('get microsoft cdn domains', getMicrosoftCdnRulesetPromise); 58 | 59 | return new RulesetOutput(span, 'microsoft_cdn', 'non_ip') 60 | .withTitle('Sukka\'s Ruleset - Microsoft CDN') 61 | .appendDescription(SHARED_DESCRIPTION) 62 | .appendDescription( 63 | '', 64 | 'This file contains Microsoft\'s domains using their China mainland CDN servers.' 65 | ) 66 | .addFromRuleset(RULES) 67 | .appendDataSource('https://github.com/felixonmars/dnsmasq-china-list') 68 | .bulkAddDomain(domains) 69 | .bulkAddDomainSuffix(domainSuffixes) 70 | .write(); 71 | }); 72 | -------------------------------------------------------------------------------- /Build/download-previous-build.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import { pipeline } from 'node:stream/promises'; 4 | import { task } from './trace'; 5 | import { extract as tarExtract } from 'tar-fs'; 6 | import type { Headers as TarEntryHeaders } from 'tar-fs'; 7 | import zlib from 'node:zlib'; 8 | import undici from 'undici'; 9 | import picocolors from 'picocolors'; 10 | import { PUBLIC_DIR } from './constants/dir'; 11 | import { requestWithLog } from './lib/fetch-retry'; 12 | import { isDirectoryEmptySync } from './lib/misc'; 13 | import { isCI } from 'ci-info'; 14 | 15 | const GITHUB_CODELOAD_URL = 'https://codeload.github.com/sukkalab/ruleset.skk.moe/tar.gz/master'; 16 | const GITLAB_CODELOAD_URL = 'https://gitlab.com/SukkaW/ruleset.skk.moe/-/archive/master/ruleset.skk.moe-master.tar.gz'; 17 | 18 | export const downloadPreviousBuild = task(require.main === module, __filename)(async (span) => { 19 | if (fs.existsSync(PUBLIC_DIR) && !isDirectoryEmptySync(PUBLIC_DIR)) { 20 | console.log(picocolors.blue('Public directory exists, skip downloading previous build')); 21 | return; 22 | } 23 | 24 | // we uses actions/checkout to download the previous build now, so we should throw if the directory is empty 25 | if (isCI) { 26 | throw new Error('CI environment detected, but public directory is empty'); 27 | } 28 | 29 | const tarGzUrl = await span.traceChildAsync('get tar.gz url', async () => { 30 | const resp = await requestWithLog(GITHUB_CODELOAD_URL, { method: 'HEAD' }); 31 | if (resp.statusCode !== 200) { 32 | console.warn('Download previous build from GitHub failed! Status:', resp.statusCode); 33 | console.warn('Switch to GitLab'); 34 | return GITLAB_CODELOAD_URL; 35 | } 36 | return GITHUB_CODELOAD_URL; 37 | }); 38 | 39 | return span.traceChildAsync('download & extract previoud build', async () => { 40 | const respBody = undici.pipeline( 41 | tarGzUrl, 42 | { 43 | method: 'GET', 44 | headers: { 45 | 'User-Agent': 'curl/8.12.1', 46 | // https://github.com/unjs/giget/issues/97 47 | // https://gitlab.com/gitlab-org/gitlab/-/commit/50c11f278d18fe1f3fb12eb595067216bb58ade2 48 | 'sec-fetch-mode': 'same-origin' 49 | } 50 | }, 51 | ({ statusCode, body }) => { 52 | if (statusCode !== 200) { 53 | console.warn('Download previous build failed! Status:', statusCode); 54 | if (statusCode === 404) { 55 | throw new Error('Download previous build failed! 404'); 56 | } 57 | } 58 | 59 | return body; 60 | } 61 | // by default, undici.pipeline returns a duplex stream (for POST/PUT) 62 | // Since we are using GET, we need to end the write immediately 63 | ).end(); 64 | 65 | const pathPrefix = 'ruleset.skk.moe-master/'; 66 | 67 | return pipeline( 68 | respBody, 69 | zlib.createGunzip(), 70 | tarExtract( 71 | PUBLIC_DIR, 72 | { 73 | ignore(_: string, header?: TarEntryHeaders) { 74 | if (header) { 75 | if (header.type !== 'file' && header.type !== 'directory') { 76 | return true; 77 | } 78 | if (header.type === 'file' && path.extname(header.name) === '.ts') { 79 | return true; 80 | } 81 | } 82 | 83 | return false; 84 | }, 85 | map(header) { 86 | header.name = header.name.replace(pathPrefix, ''); 87 | return header; 88 | } 89 | } 90 | ) 91 | ); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /Build/lib/writing-strategy/adguardhome.ts: -------------------------------------------------------------------------------- 1 | import { BaseWriteStrategy } from './base'; 2 | import { noop } from 'foxts/noop'; 3 | import { notSupported } from '../misc'; 4 | import { escapeRegexp } from 'fast-escape-regexp'; 5 | 6 | export class AdGuardHome extends BaseWriteStrategy { 7 | public readonly name = 'adguardhome'; 8 | 9 | // readonly type = 'domainset'; 10 | readonly fileExtension = 'txt'; 11 | readonly type = ''; 12 | 13 | protected result: string[] = []; 14 | 15 | // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- abstract method 16 | withPadding(title: string, description: string[] | readonly string[], date: Date, content: string[]): string[] { 17 | return [ 18 | `! Title: ${title}`, 19 | '! Last modified: ' + date.toUTCString(), 20 | '! Expires: 6 hours', 21 | '! License: https://github.com/SukkaW/Surge/blob/master/LICENSE', 22 | '! Homepage: https://github.com/SukkaW/Surge', 23 | `! Description: ${description.join(' ')}`, 24 | '!', 25 | ...content, 26 | '! EOF' 27 | ]; 28 | } 29 | 30 | writeDomain(domain: string): void { 31 | this.result.push(`|${domain}^`); 32 | } 33 | 34 | // const whitelistArray = sortDomains(Array.from(whitelist)); 35 | // for (let i = 0, len = whitelistArray.length; i < len; i++) { 36 | // const domain = whitelistArray[i]; 37 | // if (domain[0] === '.') { 38 | // results.push(`@@||${domain.slice(1)}^`); 39 | // } else { 40 | // results.push(`@@|${domain}^`); 41 | // } 42 | // } 43 | 44 | writeDomainSuffix(domain: string): void { 45 | this.result.push(`||${domain}^`); 46 | } 47 | 48 | writeDomainKeywords(keywords: Set): void { 49 | for (const keyword of keywords) { 50 | // Use regex to match keyword 51 | this.result.push(`/${escapeRegexp(keyword, false)}/`); 52 | } 53 | } 54 | 55 | writeDomainWildcard(wildcard: string): void { 56 | const processed = wildcard.replaceAll('?', '*'); 57 | if (processed.startsWith('*.')) { 58 | this.result.push(`||${processed.slice(2)}^`); 59 | } else { 60 | this.result.push(`|${processed}^`); 61 | } 62 | } 63 | 64 | writeUserAgents = noop; 65 | writeProcessNames = noop; 66 | writeProcessPaths = noop; 67 | writeUrlRegexes = noop; 68 | writeIpCidrs(ipGroup: string[], noResolve: boolean): void { 69 | if (noResolve) { 70 | // When IP is provided to AdGuardHome, any domain resolve to those IP will be blocked 71 | // So we can't do noResolve 72 | return; 73 | } 74 | for (const ipcidr of ipGroup) { 75 | if (ipcidr.endsWith('/32')) { 76 | this.result.push(`||${ipcidr.slice(0, -3)}`); 77 | /* else if (ipcidr.endsWith('.0/24')) { 78 | results.push(`||${ipcidr.slice(0, -6)}.*`); 79 | } */ 80 | } else { 81 | this.result.push(`||${ipcidr}^`); 82 | } 83 | } 84 | } 85 | 86 | writeIpCidr6s(ipGroup: string[], noResolve: boolean): void { 87 | if (noResolve) { 88 | // When IP is provided to AdGuardHome, any domain resolve to those IP will be blocked 89 | // So we can't do noResolve 90 | return; 91 | } 92 | for (const ipcidr of ipGroup) { 93 | if (ipcidr.endsWith('/128')) { 94 | this.result.push(`||${ipcidr.slice(0, -4)}`); 95 | } else { 96 | this.result.push(`||${ipcidr}`); 97 | } 98 | } 99 | }; 100 | 101 | writeGeoip = notSupported('writeGeoip'); 102 | writeIpAsns = notSupported('writeIpAsns'); 103 | writeSourceIpCidrs = notSupported('writeSourceIpCidrs'); 104 | writeSourcePorts = notSupported('writeSourcePorts'); 105 | writeDestinationPorts = noop; 106 | writeProtocols = noop; 107 | writeOtherRules = noop; 108 | } 109 | -------------------------------------------------------------------------------- /Build/tools-dedupe-src.ts: -------------------------------------------------------------------------------- 1 | import { fdir as Fdir } from 'fdir'; 2 | import path from 'node:path'; 3 | import fsp from 'node:fs/promises'; 4 | import { SOURCE_DIR } from './constants/dir'; 5 | import { readFileByLine } from './lib/fetch-text-by-line'; 6 | import { processLine } from './lib/process-line'; 7 | import { HostnameSmolTrie } from './lib/trie'; 8 | import { task } from './trace'; 9 | 10 | const ENFORCED_WHITELIST = [ 11 | 'hola.sk', 12 | 'hola.org', 13 | 'hola-shopping.com', 14 | 'mynextphone.io', 15 | 'iadmatapk.nosdn.127.net', 16 | 'httpdns.bilivideo.com', 17 | 'httpdns-v6.gslb.yy.com', 18 | 'twemoji.maxcdn.com', 19 | 'samsungcloudsolution.com', 20 | 'samsungcloudsolution.net', 21 | 'samsungqbe.com', 22 | 'ntp.api.bz', 23 | 'cdn.tuk.dev', 24 | 'vocadb-analytics.fly.dev', 25 | 'img.vim-cn.com' 26 | ]; 27 | 28 | const WHITELIST: string[] = ['ntp.api.bz', 'httpdns.bilivideo.com', 'httpdns.platform.dbankcloud.cn', 'dns.iqiyi.com', 'dns.qiyipic.iqiyi.com', 'img.vim-cn.com', 'chat-content.beanfun.com', 'archive.mirror.ba', 'ctan.imsc.res.in', 'gnu.freemirror.org', 'probe.whatismyipaddress.com', 'sdkrec.tf.360.cn', 'iadmatapk.nosdn.127.net', 'gamecenter.iqiyi.com', 'tracking.klickthru.com', 'm.shilian168.cn', 'm.zdjgj.cn', 'gcpool.ddns.net', 'radpool.ddns.net', 's9.maxstream.org', 's10.maxstream.org', 's11.maxstream.org', 'statics.erothots.co', 'mcdn.tubi.tv']; 29 | 30 | task(require.main === module, __filename)(async (span) => { 31 | const files = await span.traceChildAsync('crawl thru all files', () => new Fdir() 32 | .withFullPaths() 33 | .filter((filepath, isDirectory) => { 34 | if (isDirectory) return true; 35 | 36 | const extname = path.extname(filepath); 37 | 38 | return extname !== '.js' && extname !== '.ts'; 39 | }) 40 | .crawl(SOURCE_DIR) 41 | .withPromise()); 42 | 43 | const whiteTrie = span.traceChildSync('build whitelist trie', () => { 44 | const trie = new HostnameSmolTrie(WHITELIST); 45 | ENFORCED_WHITELIST.forEach((item) => trie.whitelist(item)); 46 | return trie; 47 | }); 48 | 49 | await Promise.all(files.map(file => span.traceChildAsync('dedupe ' + file, () => dedupeFile(file, whiteTrie)))); 50 | }); 51 | 52 | async function dedupeFile(file: string, whitelist: HostnameSmolTrie) { 53 | const result: string[] = []; 54 | 55 | const trie = new HostnameSmolTrie(); 56 | 57 | let line: string | null = ''; 58 | 59 | // eslint-disable-next-line @typescript-eslint/unbound-method -- .call 60 | let trieHasOrContains = HostnameSmolTrie.prototype.has; 61 | 62 | for await (const l of readFileByLine(file)) { 63 | line = processLine(l); 64 | 65 | if (!line) { 66 | if (l.startsWith('# $ skip_dedupe_src')) { 67 | return; 68 | } 69 | if (l.startsWith('# $ dedupe_use_trie_contains')) { 70 | // eslint-disable-next-line @typescript-eslint/unbound-method -- .call 71 | trieHasOrContains = HostnameSmolTrie.prototype.contains; 72 | } 73 | 74 | result.push(l); // keep all comments and blank lines 75 | continue; 76 | } 77 | 78 | if (trieHasOrContains.call(trie, line)) { 79 | continue; // drop duplicate 80 | } 81 | 82 | if (whitelist.has(line)) { 83 | continue; // drop whitelisted items 84 | } 85 | 86 | trie.add(line); 87 | result.push(line); 88 | } 89 | 90 | return fsp.writeFile(file, result.join('\n') + '\n'); 91 | } 92 | 93 | // function isDomainSuffix(whiteItem: string, incomingItem: string) { 94 | // const whiteIncludeDomain = whiteItem[0] === '.'; 95 | // whiteItem = whiteItem[0] === '.' ? whiteItem.slice(1) : whiteItem; 96 | 97 | // if (whiteItem === incomingItem) { 98 | // return true; // as long as exact match, we don't care if subdomain is included or not 99 | // } 100 | // if (whiteIncludeDomain) { 101 | // return incomingItem.endsWith('.' + whiteItem); 102 | // } 103 | // return false; 104 | // } 105 | -------------------------------------------------------------------------------- /Build/build-sgmodule-always-realip.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { task } from './trace'; 3 | import { compareAndWriteFile } from './lib/create-file'; 4 | import { DIRECTS, LAN } from '../Source/non_ip/direct'; 5 | import type { DNSMapping } from '../Source/non_ip/direct'; 6 | import { DOMESTICS, DOH_BOOTSTRAP } from '../Source/non_ip/domestic'; 7 | import * as yaml from 'yaml'; 8 | import { OUTPUT_INTERNAL_DIR, OUTPUT_MODULES_DIR } from './constants/dir'; 9 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 10 | import { SHARED_DESCRIPTION } from './constants/description'; 11 | import { createGetDnsMappingRule } from './build-domestic-direct-lan-ruleset-dns-mapping-module'; 12 | import { ClashDomainSet } from './lib/writing-strategy/clash'; 13 | import { FileOutput } from './lib/rules/base'; 14 | 15 | const HOSTNAMES = [ 16 | // Network Detection, Captive Portal 17 | 'dns.msftncsi.com', 18 | // '*.msftconnecttest.com', 19 | // 'network-test.debian.org', 20 | // 'detectportal.firefox.com', 21 | // Handle SNAT conversation properly 22 | '*.srv.nintendo.net', 23 | '*.stun.playstation.net', 24 | 'xbox.*.microsoft.com', 25 | '*.xboxlive.com', 26 | '*.turn.twilio.com', 27 | '*.stun.twilio.com', 28 | 'stun.syncthing.net', 29 | 'stun.*', 30 | // 'controlplane.tailscale.com', 31 | // NTP 32 | // 'time.*.com', 'time.*.gov', 'time.*.edu.cn', 'time.*.apple.com', 'time?.*.com', 'ntp.*.com', 'ntp?.*.com', '*.time.edu.cn', '*.ntp.org.cn', '*.pool.ntp.org' 33 | // 'time*.cloud.tencent.com', 'ntp?.aliyun.com', 34 | // QQ Login 35 | // 'localhost.*.qq.com' 36 | // 'localhost.ptlogin2.qq.com 37 | // 'localhost.sec.qq.com', 38 | // 'localhost.work.weixin.qq.com', 39 | '127.*.*.*.sslip.io', 40 | '127-*-*-*.sslip.io', 41 | '*.127.*.*.*.sslip.io', 42 | '*-127-*-*-*.sslip.io', 43 | '127.*.*.*.nip.io', 44 | '127-*-*-*.nip.io', 45 | '*.127.*.*.*.nip.io', 46 | '*-127-*-*-*.nip.io' 47 | ]; 48 | 49 | export const buildAlwaysRealIPModule = task(require.main === module, __filename)(async (span) => { 50 | const surge: string[] = []; 51 | const clashFakeIpFilter = new FileOutput(span, 'clash_fake_ip_filter') 52 | .withTitle('Sukka\'s Ruleset - Always Real IP Plus') 53 | .withDescription([ 54 | ...SHARED_DESCRIPTION, 55 | '', 56 | 'Clash.Meta fake-ip-filter as ruleset' 57 | ]) 58 | .withStrategies([ 59 | new ClashDomainSet('domainset') 60 | ]); 61 | 62 | // Intranet, Router Setup, and mant more 63 | const dataset = [DIRECTS, LAN, DOMESTICS, DOH_BOOTSTRAP].reduce((acc, item) => { 64 | Object.values(item).forEach((i: DNSMapping) => { 65 | if (i.realip) { 66 | acc.push(i); 67 | } 68 | }); 69 | 70 | return acc; 71 | }, []); 72 | 73 | const getDnsMappingRuleWithoutWildcard = createGetDnsMappingRule(false); 74 | 75 | for (const { domains } of dataset) { 76 | clashFakeIpFilter.addFromRuleset(domains.flatMap(getDnsMappingRuleWithoutWildcard)); 77 | } 78 | 79 | return Promise.all([ 80 | compareAndWriteFile( 81 | span, 82 | [ 83 | '#!name=[Sukka] Always Real IP Plus', 84 | `#!desc=Last Updated: ${new Date().toISOString()}`, 85 | '', 86 | '[General]', 87 | `always-real-ip = %APPEND% ${HOSTNAMES.concat(surge).join(', ')}` 88 | ], 89 | path.resolve(OUTPUT_MODULES_DIR, 'sukka_common_always_realip.sgmodule') 90 | ), 91 | compareAndWriteFile( 92 | span, 93 | yaml.stringify( 94 | { 95 | dns: { 96 | 'fake-ip-filter': appendArrayInPlace( 97 | /** clash */ 98 | dataset.flatMap(({ domains }) => domains.map((domain) => { 99 | switch (domain[0]) { 100 | case '$': 101 | return domain.slice(1); 102 | case '+': 103 | return '+.' + domain.slice(1); 104 | default: 105 | return domain; 106 | } 107 | })), 108 | HOSTNAMES 109 | ) 110 | } 111 | }, 112 | { version: '1.1' } 113 | ).split('\n'), 114 | path.join(OUTPUT_INTERNAL_DIR, 'clash_fake_ip_filter.yaml') 115 | ) 116 | ]); 117 | }); 118 | -------------------------------------------------------------------------------- /Source/non_ip/cdn.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - CDN Domains 2 | # $ meta_description This file contains object storage and static assets CDN domains. 3 | 4 | # >> GitBook 5 | DOMAIN-KEYWORD,-files.gitbook.io 6 | # >> Vimeo 7 | DOMAIN-KEYWORD,vod-adaptive.akamaized.net 8 | # >> Zendesk 9 | DOMAIN-KEYWORD,web-assets.zendesk 10 | # >> Cloudinary 11 | DOMAIN-KEYWORD,-res.cloudinary.com 12 | # >> Algolia 13 | DOMAIN-KEYWORD,dsn.algolia.net 14 | # Microsoft 15 | DOMAIN-WILDCARD,cdn.*.office.net 16 | DOMAIN-WILDCARD,cdn.*.microsoft.com 17 | DOMAIN-WILDCARD,cdn-*.microsoft.com 18 | # >> AppCenter Download 19 | DOMAIN-WILDCARD,appcenter-filemanagement-*.cloudapp.net 20 | # >> Daily Motion 21 | DOMAIN-WILDCARD,proxy-*.dailymotion.com 22 | # >> tgstat.ru 23 | DOMAIN-WILDCARD,static?.tgstat.ru 24 | DOMAIN-WILDCARD,static??.tgstat.ru 25 | # >> Amazon Trust OCSP 26 | DOMAIN-WILDCARD,ocsp.*.amazontrust.com 27 | # >> Object Storage 28 | DOMAIN-WILDCARD,s3.*.wasabisys.com 29 | DOMAIN-WILDCARD,object-storage-*.vexxhost.net 30 | # >> PornHost 31 | # nhentai 32 | DOMAIN-WILDCARD,i?.nhentai.net 33 | DOMAIN-WILDCARD,t?.nhentai.net 34 | # PornHosts 35 | DOMAIN-KEYWORD,-thumbs.pornhost.com 36 | DOMAIN-WILDCARD,cdn?-dl.pornhost.com 37 | # 18Comic / JMComic 38 | DOMAIN,cdn-msp.18comic.vip 39 | DOMAIN,cdn-msp.jmcomic.me 40 | DOMAIN,cdn-msp.jmcomic-zzz.one 41 | DOMAIN,cdn-msp.18comic.org 42 | DOMAIN-WILDCARD,cdn-msp?.18comic.vip 43 | DOMAIN-WILDCARD,cdn-msp?.jmcomic.me 44 | DOMAIN-WILDCARD,cdn-msp?.jmcomic-zzz.one 45 | DOMAIN-WILDCARD,cdn-msp?.18comic.org 46 | # EPorner 47 | DOMAIN-KEYWORD,-cdn.eporner.com 48 | # Mikufuns 49 | DOMAIN-WILDCARD,file?.mikuclub.fun 50 | DOMAIN-WILDCARD,cdn?.mikuclub.fun 51 | # FreshDesk 52 | DOMAIN-WILDCARD,assets*.freshdesk.com 53 | # >> Embed 54 | DOMAIN-WILDCARD,cdns.*.gigya.com 55 | # >> Braze SDK 56 | # sdk.fra-01.braze.eu 57 | DOMAIN-WILDCARD,sdk.*.braze.eu 58 | # hotmovies.com 59 | DOMAIN-KEYWORD,cdn.adultempire.com 60 | # porngo.com 61 | DOMAIN-WILDCARD,img?.porngo.com 62 | DOMAIN-WILDCARD,img??.porngo.com 63 | # pornone.com 64 | DOMAIN-WILDCARD,th-*.pornone.com 65 | DOMAIN-WILDCARD,s???.pornone.com 66 | # Datahog API 67 | DOMAIN-WILDCARD,browser-intake-*-datadoghq.com 68 | # FC2 69 | DOMAIN-WILDCARD,video-thumbnail2.fc2.com 70 | DOMAIN-WILDCARD,video?-thumbnail?.fc2.com 71 | DOMAIN-WILDCARD,video-thumbnail?.liaoningmovie.net 72 | DOMAIN-WILDCARD,video?-thumbnail?.liaoningmovie.net 73 | DOMAIN-WILDCARD,vip-videoprem*-thumbnail?.fc2.com 74 | DOMAIN-WILDCARD,blog-imgs-??.2nt.com 75 | DOMAIN-WILDCARD,blog-imgs-??.fc2.com 76 | # ouo.si 77 | DOMAIN-WILDCARD,rr?---??.ouo.si 78 | # Figma 79 | DOMAIN-WILDCARD,s3-*.figma.com 80 | # Video 81 | DOMAIN-WILDCARD,*.kuaikan-cdn?.com 82 | DOMAIN-WILDCARD,*.fsvod?.com 83 | DOMAIN-WILDCARD,image*.cloudokyo.cloud 84 | DOMAIN-WILDCARD,video*.cloudokyo.cloud 85 | DOMAIN-WILDCARD,v.rn???.xyz 86 | DOMAIN-WILDCARD,s?.maxstream.org 87 | DOMAIN-WILDCARD,s??.maxstream.org 88 | DOMAIN-WILDCARD,stream?.kissjav.com 89 | DOMAIN-WILDCARD,stream??.kissjav.com 90 | DOMAIN-WILDCARD,chunk-*.mux.com 91 | DOMAIN-WILDCARD,*.skyearth?.xyz 92 | 93 | # TrustPilot 94 | # images-static.trustpilot.com 95 | DOMAIN-KEYWORD,static.trustpilot.com 96 | # ecommplugins-scripts.trustpilot.com 97 | DOMAIN-KEYWORD,scripts.trustpilot.com 98 | # user-images.trustpilot.com 99 | DOMAIN-KEYWORD,images.trustpilot.com 100 | # categoriespages-cdn.trustpilot.net 101 | DOMAIN-KEYWORD,cdn.trustpilot.net 102 | # consumersite-assets.trustpilot.net 103 | DOMAIN-KEYWORD,assets.trustpilot.net 104 | # consumersiteimages.trustpilot.net 105 | DOMAIN-KEYWORD,images.trustpilot.net 106 | 107 | # >> E-Mail Image Hosting 108 | DOMAIN-WILDCARD,*.img.*.sendibm?.com 109 | # >> Misc 110 | DOMAIN-WILDCARD,cdn?.18girlssex.com 111 | DOMAIN-KEYWORD,-assets.worldsex.com 112 | DOMAIN-KEYWORD,-thumbs.worldsex.com 113 | DOMAIN-KEYWORD,99avcdn 114 | DOMAIN-KEYWORD,-images.disco-api.com 115 | DOMAIN-WILDCARD,uploads-*.insided.com 116 | DOMAIN-KEYWORD,vdownload.hanime1 117 | DOMAIN-KEYWORD,fonts.hanime1 118 | DOMAIN-WILDCARD,s*.hanime1 119 | DOMAIN-WILDCARD,img-*.now.com 120 | DOMAIN-WILDCARD,assets.*.imaga.co 121 | DOMAIN-KEYWORD,static.grammarly.com 122 | DOMAIN-WILDCARD,cdn*.pigav.com 123 | DOMAIN-WILDCARD,fastly-*.allmovie.com 124 | DOMAIN-WILDCARD,cdn*.porn87.com 125 | DOMAIN-WILDCARD,cdn*.pornhost.com 126 | DOMAIN-WILDCARD,cdn*.boysfood.com 127 | -------------------------------------------------------------------------------- /Source/non_ip/global.ts: -------------------------------------------------------------------------------- 1 | import type { DNSMapping } from './direct'; 2 | 3 | export const GLOBAL: Record = { 4 | GOOGLE: { 5 | hosts: {}, 6 | // This DNS doesn't include in sukka_local_dns_mapping sgmodule 7 | // TODO: make a smartdns/adguardhome config for proxy servers 8 | dns: 'https://8.8.4.4/dns-query', 9 | realip: false, 10 | ruleset: false, 11 | domains: [ 12 | 'abc.xyz', 13 | 'ampproject.org', 14 | 'android.com', 15 | 'androidify.com', 16 | 'appspot.com', 17 | 'autodraw.com', 18 | 'blogger.com', 19 | 'blogblog.com', 20 | 'blogspot.com', 21 | 'capitalg.com', 22 | 'certificate-transparency.org', 23 | 'chrome.com', 24 | 'chromeexperiments.com', 25 | 'chromestatus.com', 26 | 'chromium.org', 27 | 'circle.ms', 28 | 'cloudfunctions.net', 29 | 'creativelab5.com', 30 | 'debug.com', 31 | 'deepmind.com', 32 | 'dialogflow.com', 33 | 'feedburner.com', 34 | 'forms.gle', 35 | 'firebaseio.com', 36 | 'g.co', 37 | 'gcr.io', 38 | 'getmdl.io', 39 | 'getoutline.org', 40 | 'ggpht.com', 41 | 'glitch.com', 42 | 'gmail.com', 43 | 'gmodules.com', 44 | 'godoc.org', 45 | 'golang.org', 46 | 'goo.gl', 47 | 'googl.com', 48 | 'google.com', 49 | 'google.hk', 50 | 'google.com.hk', 51 | 'google.tw', 52 | 'google.com.tw', 53 | 'google.com.sg', 54 | 'google.jp', 55 | 'google.co.jp', 56 | 'googleapis.com', 57 | 'googleapis.cn', 58 | 'googlesource.com', 59 | 'gstatic.com', 60 | 'gv.com', 61 | 'gvt0.com', 62 | 'gvt1.com', 63 | 'gvt2.com', 64 | 'gvt3.com', 65 | 'gvt5.com', 66 | 'gvt6.com', 67 | 'gvt7.com', 68 | 'gvt9.com', 69 | 'gwtproject.org', 70 | 'html5rocks.com', 71 | 'itasoftware.com', 72 | 'madewithcode.com', 73 | 'material.io', 74 | 'page.link', 75 | 'polymer-project.org', 76 | 'recaptcha.net', 77 | 'shattered.io', 78 | 'synergyse.com', 79 | 'tensorflow.org', 80 | 'tiltbrush.com', 81 | 'translate.goog', 82 | 'waveprotocol.org', 83 | 'waymo.com', 84 | 'web.dev', 85 | 'webmproject.org', 86 | 'webpkgcache.com', 87 | 'webrtc.org', 88 | 'whatbrowser.org', 89 | 'widevine.com', 90 | 'x.company', 91 | 'xn--ngstr-lra8j.com', 92 | 'youtu.be', 93 | 'youtube.com', 94 | 'yt.be', 95 | 'ytimg.com', 96 | 'telephony.goog', 97 | '1e100.net', 98 | 'nest.com', 99 | 'googlezip.net', 100 | 'dns.google', 101 | '$pki.goog', // ocsp.pki.goog and o.pki.goog is available in Mainland China 102 | 'fish.audio', 103 | // 104 | 'gooogle.com', 105 | 'firebase.com', 106 | // GAE 107 | 'run.app', 108 | // TLDs 109 | 'google', 110 | 'goog', 111 | 112 | // special 113 | 'cloudflarestatus.com' 114 | ] 115 | }, 116 | CLOUDFLARE: { 117 | hosts: {}, 118 | // This DNS doesn't include in sukka_local_dns_mapping sgmodule 119 | // TODO: make a smartdns/adguardhome config for proxy servers 120 | dns: 'https://1.0.0.1/dns-query', 121 | realip: false, 122 | ruleset: false, 123 | domains: [ 124 | 'cloudflare-dns.com', 125 | 'cloudflare.com', 126 | 'cloudflare.dev', 127 | 'cloudflareresolve.com', 128 | // 'cloudflarestatus.com', intentionally excluded from cloudflare section and moved to google 129 | 'cloudflareaccess.com' 130 | ] 131 | }, 132 | WIKIMEDIA: { 133 | hosts: {}, 134 | dns: 'https://wikimedia-dns.org/dns-query', 135 | realip: false, 136 | ruleset: false, 137 | domains: [ 138 | 'mediawiki.org', 139 | 'wikibooks.org', 140 | 'wikidata.org', 141 | 'wikifunctions.org', 142 | 'wikimedia.org', 143 | 'wikimediafoundation.org', 144 | 'wikinews.org', 145 | 'wikipedia.com', 146 | 'wikipedia.org', 147 | 'wikiquote.org', 148 | 'wikisource.org', 149 | 'wikiversity.org', 150 | 'wikivoyage.org', 151 | 'wiktionary.org', 152 | 'wmfusercontent.org', 153 | 'wmflabs.org', 154 | 'w.wiki' 155 | ] 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /Mock/securepubads-g-doubleclick-net_tag_js_gpt.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | // https://developers.google.com/doubleclick-gpt/reference 5 | const noopfn = () => { 6 | // noop 7 | }; 8 | const noopthisfn = function () { 9 | return this; 10 | }; 11 | const noopnullfn = function () { 12 | return null; 13 | }; 14 | const nooparrayfn = function () { 15 | return []; 16 | }; 17 | const noopstrfn = function () { 18 | return ''; 19 | }; 20 | // 21 | const companionAdsService = { 22 | addEventListener: noopthisfn, 23 | enableSyncLoading: noopfn, 24 | setRefreshUnfilledSlots: noopfn 25 | }; 26 | const contentService = { 27 | addEventListener: noopthisfn, 28 | setContent: noopfn 29 | }; 30 | const PassbackSlot = noopfn; 31 | let p = PassbackSlot.prototype; 32 | p.display = noopfn; 33 | p.get = noopnullfn; 34 | p.set = noopthisfn; 35 | p.setClickUrl = noopthisfn; 36 | p.setTagForChildDirectedTreatment = noopthisfn; 37 | p.setTargeting = noopthisfn; 38 | p.updateTargetingFromMap = noopthisfn; 39 | const pubAdsService = { 40 | addEventListener: noopthisfn, 41 | clear: noopfn, 42 | clearCategoryExclusions: noopthisfn, 43 | clearTagForChildDirectedTreatment: noopthisfn, 44 | clearTargeting: noopthisfn, 45 | collapseEmptyDivs: noopfn, 46 | defineOutOfPagePassback() { return new PassbackSlot(); }, 47 | definePassback() { return new PassbackSlot(); }, 48 | disableInitialLoad: noopfn, 49 | display: noopfn, 50 | enableAsyncRendering: noopfn, 51 | enableLazyLoad: noopfn, 52 | enableSingleRequest: noopfn, 53 | enableSyncRendering: noopfn, 54 | enableVideoAds: noopfn, 55 | get: noopnullfn, 56 | getAttributeKeys: nooparrayfn, 57 | getTargeting: nooparrayfn, 58 | getTargetingKeys: nooparrayfn, 59 | getSlots: nooparrayfn, 60 | refresh: noopfn, 61 | removeEventListener: noopfn, 62 | set: noopthisfn, 63 | setCategoryExclusion: noopthisfn, 64 | setCentering: noopfn, 65 | setCookieOptions: noopthisfn, 66 | setForceSafeFrame: noopthisfn, 67 | setLocation: noopthisfn, 68 | setPublisherProvidedId: noopthisfn, 69 | setPrivacySettings: noopthisfn, 70 | setRequestNonPersonalizedAds: noopthisfn, 71 | setSafeFrameConfig: noopthisfn, 72 | setTagForChildDirectedTreatment: noopthisfn, 73 | setTargeting: noopthisfn, 74 | setVideoContent: noopthisfn, 75 | updateCorrelator: noopfn 76 | }; 77 | const SizeMappingBuilder = noopfn; 78 | p = SizeMappingBuilder.prototype; 79 | p.addSize = noopthisfn; 80 | p.build = noopnullfn; 81 | const Slot = noopfn; 82 | p = Slot.prototype; 83 | p.addService = noopthisfn; 84 | p.clearCategoryExclusions = noopthisfn; 85 | p.clearTargeting = noopthisfn; 86 | p.defineSizeMapping = noopthisfn; 87 | p.get = noopnullfn; 88 | p.getAdUnitPath = nooparrayfn; 89 | p.getAttributeKeys = nooparrayfn; 90 | p.getCategoryExclusions = nooparrayfn; 91 | p.getDomId = noopstrfn; 92 | p.getResponseInformation = noopnullfn; 93 | p.getSlotElementId = noopstrfn; 94 | p.getSlotId = noopthisfn; 95 | p.getTargeting = nooparrayfn; 96 | p.getTargetingKeys = nooparrayfn; 97 | p.set = noopthisfn; 98 | p.setCategoryExclusion = noopthisfn; 99 | p.setClickUrl = noopthisfn; 100 | p.setCollapseEmptyDiv = noopthisfn; 101 | p.setTargeting = noopthisfn; 102 | p.updateTargetingFromMap = noopthisfn; 103 | // 104 | const gpt = window.googletag || {}; 105 | const cmd = gpt.cmd || []; 106 | gpt.apiReady = true; 107 | gpt.cmd = []; 108 | gpt.cmd.push = function (a) { 109 | try { 110 | a(); 111 | } catch { 112 | } 113 | return 1; 114 | }; 115 | gpt.companionAds = function () { return companionAdsService; }; 116 | gpt.content = function () { return contentService; }; 117 | gpt.defineOutOfPageSlot = function () { return new Slot(); }; 118 | gpt.defineSlot = function () { return new Slot(); }; 119 | gpt.destroySlots = noopfn; 120 | gpt.disablePublisherConsole = noopfn; 121 | gpt.display = noopfn; 122 | gpt.enableServices = noopfn; 123 | gpt.getVersion = noopstrfn; 124 | gpt.pubads = function () { return pubAdsService; }; 125 | gpt.pubadsReady = true; 126 | gpt.setAdIframeTitle = noopfn; 127 | gpt.sizeMapping = function () { return new SizeMappingBuilder(); }; 128 | window.googletag = gpt; 129 | while (cmd.length !== 0) { 130 | gpt.cmd.push(cmd.shift()); 131 | } 132 | })(); 133 | -------------------------------------------------------------------------------- /Build/build-common.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import * as path from 'node:path'; 4 | import { readFileByLine } from './lib/fetch-text-by-line'; 5 | import { processLine } from './lib/process-line'; 6 | import type { Span } from './trace'; 7 | import { task } from './trace'; 8 | import { SHARED_DESCRIPTION } from './constants/description'; 9 | import { fdir as Fdir } from 'fdir'; 10 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 11 | import { SOURCE_DIR } from './constants/dir'; 12 | import { DomainsetOutput } from './lib/rules/domainset'; 13 | import { RulesetOutput } from './lib/rules/ruleset'; 14 | 15 | const MAGIC_COMMAND_SKIP = '# $ custom_build_script'; 16 | const MAGIC_COMMAND_TITLE = '# $ meta_title '; 17 | const MAGIC_COMMAND_DESCRIPTION = '# $ meta_description '; 18 | const MAGIC_COMMAND_SGMODULE_MITM_HOSTNAMES = '# $ sgmodule_mitm_hostnames '; 19 | 20 | const clawSourceDirPromise = new Fdir() 21 | .withRelativePaths() 22 | .filter((filepath, isDirectory) => { 23 | if (isDirectory) return true; 24 | 25 | const extname = path.extname(filepath); 26 | 27 | return extname !== '.js' && extname !== '.ts'; 28 | }) 29 | .crawl(SOURCE_DIR) 30 | .withPromise(); 31 | 32 | export const buildCommon = task(require.main === module, __filename)(async (span) => { 33 | const promises: Array> = []; 34 | 35 | const paths = await clawSourceDirPromise; 36 | 37 | for (let i = 0, len = paths.length; i < len; i++) { 38 | const relativePath = paths[i]; 39 | const fullPath = SOURCE_DIR + path.sep + relativePath; 40 | 41 | // if ( 42 | // relativePath.startsWith('ip/') 43 | // || relativePath.startsWith('non_ip/') 44 | // ) { 45 | promises.push(transform(span, fullPath, relativePath)); 46 | // continue; 47 | // } 48 | 49 | // console.error(picocolors.red(`[build-comman] Unknown file: ${relativePath}`)); 50 | } 51 | 52 | return Promise.all(promises); 53 | }); 54 | 55 | const $skip = Symbol('skip'); 56 | 57 | function processFile(span: Span, sourcePath: string) { 58 | return span.traceChildAsync(`process file: ${sourcePath}`, async () => { 59 | const lines: string[] = []; 60 | 61 | let title = ''; 62 | const descriptions: string[] = []; 63 | let sgmodulePathname: string | null = null; 64 | 65 | try { 66 | let l: string | null = ''; 67 | for await (const line of readFileByLine(sourcePath)) { 68 | if (line.startsWith(MAGIC_COMMAND_SKIP)) { 69 | return $skip; 70 | } 71 | 72 | if (line.startsWith(MAGIC_COMMAND_TITLE)) { 73 | title = line.slice(MAGIC_COMMAND_TITLE.length).trim(); 74 | continue; 75 | } 76 | 77 | if (line.startsWith(MAGIC_COMMAND_DESCRIPTION)) { 78 | descriptions.push(line.slice(MAGIC_COMMAND_DESCRIPTION.length).trim()); 79 | continue; 80 | } 81 | 82 | if (line.startsWith(MAGIC_COMMAND_SGMODULE_MITM_HOSTNAMES)) { 83 | sgmodulePathname = line.slice(MAGIC_COMMAND_SGMODULE_MITM_HOSTNAMES.length).trim(); 84 | continue; 85 | } 86 | 87 | l = processLine(line); 88 | if (l) { 89 | lines.push(l); 90 | } 91 | } 92 | } catch (e) { 93 | console.error('Error processing', sourcePath); 94 | console.trace(e); 95 | } 96 | 97 | return [title, descriptions, lines, sgmodulePathname] as const; 98 | }); 99 | } 100 | 101 | async function transform(parentSpan: Span, sourcePath: string, relativePath: string) { 102 | const extname = path.extname(sourcePath); 103 | const id = path.basename(sourcePath, extname); 104 | 105 | return parentSpan 106 | .traceChild(`transform ruleset: ${id}`) 107 | .traceAsyncFn(async (span) => { 108 | const type = relativePath.slice(0, relativePath.indexOf(path.sep)); 109 | 110 | if (type !== 'ip' && type !== 'non_ip' && type !== 'domainset') { 111 | throw new TypeError(`Invalid type: ${type}`); 112 | } 113 | 114 | const res = await processFile(span, sourcePath); 115 | if (res === $skip) return; 116 | 117 | const [title, descriptions, lines, sgmoduleName] = res; 118 | 119 | let finalDescriptions: string[]; 120 | if (descriptions.length) { 121 | finalDescriptions = SHARED_DESCRIPTION.slice(); 122 | finalDescriptions.push(''); 123 | appendArrayInPlace(finalDescriptions, descriptions); 124 | } else { 125 | finalDescriptions = SHARED_DESCRIPTION; 126 | } 127 | 128 | if (type === 'domainset') { 129 | return new DomainsetOutput(span, id) 130 | .withTitle(title) 131 | .withDescription(finalDescriptions) 132 | .addFromDomainset(lines) 133 | .write(); 134 | } 135 | 136 | return new RulesetOutput(span, id, type) 137 | .withTitle(title) 138 | .withDescription(finalDescriptions) 139 | .withMitmSgmodulePath(sgmoduleName) 140 | .addFromRuleset(lines) 141 | .write(); 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /Build/build-cdn-download-conf.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { readFileIntoProcessedArray } from './lib/fetch-text-by-line'; 3 | import { task } from './trace'; 4 | import { SHARED_DESCRIPTION } from './constants/description'; 5 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 6 | import { SOURCE_DIR } from './constants/dir'; 7 | import { DomainsetOutput } from './lib/rules/domainset'; 8 | import { CRASHLYTICS_WHITELIST } from './constants/reject-data-source'; 9 | import Worktank from 'worktank'; 10 | 11 | const cdnDomainsListPromise = readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/cdn.conf')); 12 | const downloadDomainSetPromise = readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/download.conf')); 13 | const steamDomainSetPromise = readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/game-download.conf')); 14 | 15 | const pool = new Worktank({ 16 | pool: { 17 | name: 'extract-s3-from-publicssuffix', 18 | size: 1 // The number of workers to keep in the pool, if more workers are needed they will be spawned up to this limit 19 | }, 20 | worker: { 21 | autoAbort: 10000, 22 | autoTerminate: 20000, // The interval of milliseconds at which to check if the pool can be automatically terminated, to free up resources, workers will be spawned up again if needed 23 | autoInstantiate: true, 24 | methods: { 25 | // eslint-disable-next-line object-shorthand -- workertank 26 | getS3OSSDomains: async function (__filename: string): Promise { 27 | // TODO: createRequire is a temporary workaround for https://github.com/nodejs/node/issues/51956 28 | const { default: module } = await import('node:module'); 29 | const __require = module.createRequire(__filename); 30 | 31 | const { HostnameTrie } = __require('./lib/trie') as typeof import('./lib/trie'); 32 | const { fetchRemoteTextByLine } = __require('./lib/fetch-text-by-line') as typeof import('./lib/fetch-text-by-line'); 33 | 34 | const trie = new HostnameTrie(); 35 | 36 | for await (const line of await fetchRemoteTextByLine('https://publicsuffix.org/list/public_suffix_list.dat', true)) { 37 | trie.add(line); 38 | } 39 | 40 | /** 41 | * Extract OSS domain from publicsuffix list 42 | */ 43 | const S3OSSDomains: string[] = []; 44 | 45 | trie.find('.amazonaws.com').forEach((line: string) => { 46 | if ( 47 | (line.startsWith('s3-') || line.startsWith('s3.')) 48 | && !line.includes('cn-') 49 | ) { 50 | S3OSSDomains.push('.' + line); 51 | } 52 | }); 53 | trie.find('.scw.cloud').forEach((line: string) => { 54 | if ( 55 | (line.startsWith('s3-') || line.startsWith('s3.')) 56 | // && !line.includes('cn-') 57 | ) { 58 | S3OSSDomains.push('.' + line); 59 | } 60 | }); 61 | trie.find('sakurastorage.jp').forEach((line: string) => { 62 | if ( 63 | (line.startsWith('s3-') || line.startsWith('s3.')) 64 | ) { 65 | S3OSSDomains.push('.' + line); 66 | } 67 | }); 68 | 69 | return S3OSSDomains; 70 | } 71 | } 72 | } 73 | }); 74 | 75 | export const buildCdnDownloadConf = task(require.main === module, __filename)(async (span) => { 76 | const [ 77 | S3OSSDomains, 78 | cdnDomainsList, 79 | downloadDomainSet, 80 | steamDomainSet 81 | ] = await Promise.all([ 82 | span.traceChildAsync( 83 | 'download public suffix list for s3', 84 | () => pool.exec( 85 | 'getS3OSSDomains', 86 | [__filename] 87 | ).finally(() => pool.terminate()) 88 | ), 89 | cdnDomainsListPromise, 90 | downloadDomainSetPromise, 91 | steamDomainSetPromise 92 | ]); 93 | 94 | // Move S3 domains to download domain set, since S3 files may be large 95 | appendArrayInPlace(downloadDomainSet, S3OSSDomains); 96 | appendArrayInPlace(downloadDomainSet, steamDomainSet); 97 | 98 | // we have whitelisted the crashlytics domain, and we also want to put it in CDN policy 99 | appendArrayInPlace(cdnDomainsList, CRASHLYTICS_WHITELIST); 100 | 101 | return Promise.all([ 102 | new DomainsetOutput(span, 'cdn') 103 | .withTitle('Sukka\'s Ruleset - CDN Domains') 104 | .appendDescription(SHARED_DESCRIPTION) 105 | .appendDescription( 106 | '', 107 | 'This file contains object storage and static assets CDN domains.' 108 | ) 109 | .addFromDomainset(cdnDomainsList) 110 | .write(), 111 | 112 | new DomainsetOutput(span, 'download') 113 | .withTitle('Sukka\'s Ruleset - Large Files Hosting Domains') 114 | .appendDescription(SHARED_DESCRIPTION) 115 | .appendDescription( 116 | '', 117 | 'This file contains domains for software updating & large file hosting.' 118 | ) 119 | .addFromDomainset(downloadDomainSet) 120 | .write() 121 | ]); 122 | }); 123 | -------------------------------------------------------------------------------- /Build/lib/writing-strategy/singbox.ts: -------------------------------------------------------------------------------- 1 | import { BaseWriteStrategy } from './base'; 2 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 3 | import { noop } from 'foxts/noop'; 4 | import { withIdentityContent } from '../misc'; 5 | import stringify from 'json-stringify-pretty-compact'; 6 | import { OUTPUT_SINGBOX_DIR } from '../../constants/dir'; 7 | import { MARKER_DOMAIN } from '../../constants/description'; 8 | 9 | interface SingboxHeadlessRule { 10 | domain: string[], 11 | domain_suffix: string[], 12 | domain_keyword?: string[], 13 | domain_regex?: string[], 14 | source_ip_cidr?: string[], 15 | ip_cidr?: string[], 16 | source_port?: number[], 17 | source_port_range?: string[], 18 | port?: number[], 19 | port_range?: string[], 20 | process_name?: string[], 21 | process_path?: string[], 22 | network?: string[] 23 | } 24 | 25 | export interface SingboxSourceFormat { 26 | version: 2 | number & {}, 27 | rules: SingboxHeadlessRule[] 28 | } 29 | 30 | export class SingboxSource extends BaseWriteStrategy { 31 | public readonly name = 'singbox'; 32 | 33 | readonly fileExtension = 'json'; 34 | 35 | static readonly jsonToLines = (json: unknown): string[] => stringify(json).split('\n'); 36 | 37 | private readonly singbox: SingboxHeadlessRule = { 38 | domain: [MARKER_DOMAIN], 39 | domain_suffix: [MARKER_DOMAIN] 40 | }; 41 | 42 | protected get result() { 43 | return SingboxSource.jsonToLines({ 44 | version: 2, 45 | rules: [this.singbox] 46 | }); 47 | } 48 | 49 | constructor( 50 | /** Since sing-box only have one format that does not reflect type, we need to specify it */ 51 | public type: 'domainset' | 'non_ip' | 'ip' /* | (string & {}) */, 52 | public readonly outputDir = OUTPUT_SINGBOX_DIR 53 | ) { 54 | super(outputDir); 55 | } 56 | 57 | withPadding = withIdentityContent; 58 | 59 | writeDomain(domain: string): void { 60 | this.singbox.domain.push(domain); 61 | } 62 | 63 | writeDomainSuffix(domain: string): void { 64 | this.singbox.domain_suffix.push(domain); 65 | } 66 | 67 | writeDomainKeywords(keyword: Set): void { 68 | appendArrayInPlace( 69 | this.singbox.domain_keyword ??= [], 70 | Array.from(keyword) 71 | ); 72 | } 73 | 74 | writeDomainWildcard(wildcard: string): void { 75 | this.singbox.domain_regex ??= []; 76 | this.singbox.domain_regex.push(SingboxSource.domainWildCardToRegex(wildcard)); 77 | } 78 | 79 | writeUserAgents = noop; 80 | 81 | writeProcessNames = noop; 82 | // writeProcessNames(processName: Set): void { 83 | // appendArrayInPlace( 84 | // this.singbox.process_name ??= [], 85 | // Array.from(processName) 86 | // ); 87 | // } 88 | 89 | writeProcessPaths = noop; 90 | // writeProcessPaths(processPath: Set): void { 91 | // appendArrayInPlace( 92 | // this.singbox.process_path ??= [], 93 | // Array.from(processPath) 94 | // ); 95 | // } 96 | 97 | writeUrlRegexes = noop; 98 | 99 | writeIpCidrs(ipCidr: string[]): void { 100 | appendArrayInPlace( 101 | this.singbox.ip_cidr ??= [], 102 | ipCidr 103 | ); 104 | } 105 | 106 | writeIpCidr6s(ipCidr6: string[]): void { 107 | appendArrayInPlace( 108 | this.singbox.ip_cidr ??= [], 109 | ipCidr6 110 | ); 111 | } 112 | 113 | writeGeoip = noop; 114 | 115 | writeIpAsns = noop; 116 | 117 | writeSourceIpCidrs = noop; 118 | // writeSourceIpCidrs(sourceIpCidr: string[]): void { 119 | // this.singbox.source_ip_cidr ??= []; 120 | // for (let i = 0, len = sourceIpCidr.length; i < len; i++) { 121 | // const value = sourceIpCidr[i]; 122 | // if (value.includes('/')) { 123 | // this.singbox.source_ip_cidr.push(value); 124 | // continue; 125 | // } 126 | // const v = fastIpVersion(value); 127 | // if (v === 4) { 128 | // this.singbox.source_ip_cidr.push(`${value}/32`); 129 | // continue; 130 | // } 131 | // if (v === 6) { 132 | // this.singbox.source_ip_cidr.push(`${value}/128`); 133 | // continue; 134 | // } 135 | // } 136 | // } 137 | 138 | writeSourcePorts = noop; 139 | // writeSourcePorts(port: Set): void { 140 | // this.singbox.source_port ??= []; 141 | 142 | // for (const i of port) { 143 | // const tmp = Number(i); 144 | // if (!Number.isNaN(tmp)) { 145 | // this.singbox.source_port.push(tmp); 146 | // } 147 | // } 148 | // } 149 | 150 | writeDestinationPorts = noop; 151 | // writeDestinationPorts(port: Set): void { 152 | // this.singbox.port ??= []; 153 | 154 | // for (const i of port) { 155 | // const tmp = Number(i); 156 | // if (!Number.isNaN(tmp)) { 157 | // this.singbox.port.push(tmp); 158 | // } 159 | // } 160 | // } 161 | 162 | writeProtocols = noop; 163 | // writeProtocols(protocol: Set): void { 164 | // this.singbox.network ??= []; 165 | // // protocol has already be normalized and will only be uppercase 166 | // if (protocol.has('UDP')) { 167 | // this.singbox.network.push('udp'); 168 | // } 169 | // if (protocol.has('TCP')) { 170 | // this.singbox.network.push('tcp'); 171 | // } 172 | // } 173 | 174 | writeOtherRules = noop; 175 | } 176 | -------------------------------------------------------------------------------- /Build/index.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import os from 'node:os'; 3 | import fs from 'node:fs'; 4 | 5 | import { downloadPreviousBuild } from './download-previous-build'; 6 | import { buildCommon } from './build-common'; 7 | import { buildRejectIPList } from './build-reject-ip-list'; 8 | import { buildAppleCdn } from './build-apple-cdn'; 9 | import { buildCdnDownloadConf } from './build-cdn-download-conf'; 10 | import { buildRejectDomainSet } from './build-reject-domainset'; 11 | import { buildTelegramCIDR } from './build-telegram-cidr'; 12 | import { buildChnCidr } from './build-chn-cidr'; 13 | import { buildSpeedtestDomainSet } from './build-speedtest-domainset'; 14 | import { buildDomesticRuleset } from './build-domestic-direct-lan-ruleset-dns-mapping-module'; 15 | import { buildGlobalRuleset } from './build-global-server-dns-mapping'; 16 | import { buildStreamService } from './build-stream-service'; 17 | 18 | import { buildRedirectModule } from './build-sgmodule-redirect'; 19 | import { buildAlwaysRealIPModule } from './build-sgmodule-always-realip'; 20 | 21 | import { buildMicrosoftCdn } from './build-microsoft-cdn'; 22 | import { buildSSPanelUIMAppProfile } from './build-sspanel-appprofile'; 23 | 24 | import { buildPublic } from './build-public'; 25 | import { downloadMockAssets } from './download-mock-assets'; 26 | 27 | import { buildCloudMounterRules } from './build-cloudmounter-rules'; 28 | 29 | import { createSpan, printTraceResult, whyIsNodeRunning } from './trace'; 30 | import { buildDeprecateFiles } from './build-deprecate-files'; 31 | import path from 'node:path'; 32 | import { ROOT_DIR } from './constants/dir'; 33 | import { isCI } from 'ci-info'; 34 | 35 | process.on('uncaughtException', (error) => { 36 | console.error('Uncaught exception:', error); 37 | process.exit(1); 38 | }); 39 | process.on('unhandledRejection', (reason) => { 40 | console.error('Unhandled rejection:', reason); 41 | process.exit(1); 42 | }); 43 | 44 | const buildFinishedLock = path.join(ROOT_DIR, '.BUILD_FINISHED'); 45 | 46 | (async () => { 47 | console.log('Version:', process.version); 48 | 49 | console.log(`OS: ${os.type()} ${os.release()} ${os.arch()}`); 50 | console.log(`Node.js: ${process.versions.node}`); 51 | console.log(`V8: ${process.versions.v8}`); 52 | 53 | const cpus = os.cpus() 54 | .reduce>((o, cpu) => { 55 | o[cpu.model] = (o[cpu.model] || 0) + 1; 56 | return o; 57 | }, {}); 58 | 59 | console.log(`CPU: ${ 60 | Object.keys(cpus) 61 | .map((key) => `${key} x ${cpus[key]}`) 62 | .join('\n') 63 | }`); 64 | if ('availableParallelism' in os) { 65 | console.log(`Available parallelism: ${os.availableParallelism()}`); 66 | } 67 | 68 | console.log(`Memory: ${os.totalmem() / (1024 * 1024)} MiB`); 69 | 70 | const rootSpan = createSpan('root'); 71 | 72 | if (fs.existsSync(buildFinishedLock)) { 73 | fs.unlinkSync(buildFinishedLock); 74 | } 75 | 76 | try { 77 | // only enable why-is-node-running in GitHub Actions debug mode 78 | if (isCI && process.env.RUNNER_DEBUG === '1') { 79 | await import('why-is-node-running'); 80 | } 81 | 82 | const downloadPreviousBuildPromise = downloadPreviousBuild(rootSpan); 83 | const buildCommonPromise = downloadPreviousBuildPromise.then(() => buildCommon(rootSpan)); 84 | 85 | await Promise.all([ 86 | downloadPreviousBuildPromise, 87 | buildCommonPromise, 88 | downloadPreviousBuildPromise.then(() => buildRejectIPList(rootSpan)), 89 | downloadPreviousBuildPromise.then(() => buildAppleCdn(rootSpan)), 90 | downloadPreviousBuildPromise.then(() => buildCdnDownloadConf(rootSpan)), 91 | downloadPreviousBuildPromise.then(() => buildRejectDomainSet(rootSpan)), 92 | downloadPreviousBuildPromise.then(() => buildTelegramCIDR(rootSpan)), 93 | downloadPreviousBuildPromise.then(() => buildChnCidr(rootSpan)), 94 | downloadPreviousBuildPromise.then(() => buildSpeedtestDomainSet(rootSpan)), 95 | downloadPreviousBuildPromise.then(() => buildDomesticRuleset(rootSpan)), 96 | downloadPreviousBuildPromise.then(() => buildGlobalRuleset(rootSpan)), 97 | downloadPreviousBuildPromise.then(() => buildRedirectModule(rootSpan)), 98 | downloadPreviousBuildPromise.then(() => buildAlwaysRealIPModule(rootSpan)), 99 | downloadPreviousBuildPromise.then(() => buildStreamService(rootSpan)), 100 | downloadPreviousBuildPromise.then(() => buildMicrosoftCdn(rootSpan)), 101 | Promise.all([ 102 | downloadPreviousBuildPromise, 103 | buildCommonPromise 104 | ]).then(() => buildSSPanelUIMAppProfile(rootSpan)), 105 | downloadPreviousBuildPromise.then(() => buildCloudMounterRules(rootSpan)), 106 | downloadMockAssets(rootSpan) 107 | ]); 108 | 109 | await buildDeprecateFiles(rootSpan); 110 | await buildPublic(rootSpan); 111 | 112 | rootSpan.stop(); 113 | 114 | printTraceResult(rootSpan.traceResult); 115 | 116 | // write a file to demonstrate that the build is finished 117 | fs.writeFileSync(buildFinishedLock, 'BUILD_FINISHED\n'); 118 | 119 | // Finish the build to avoid leaking timer/fetch ref 120 | await whyIsNodeRunning(); 121 | process.exit(0); 122 | } catch (e) { 123 | console.error('Something went wrong!'); 124 | console.trace(e); 125 | process.exit(1); 126 | } 127 | })(); 128 | -------------------------------------------------------------------------------- /Source/non_ip/direct.ts: -------------------------------------------------------------------------------- 1 | export interface DNSMapping { 2 | hosts: { 3 | [domain: string]: string[] 4 | }, 5 | /** which also disallows wildcard */ 6 | realip: boolean, 7 | /** should convert to ruleset */ 8 | ruleset: boolean, 9 | dns: string | null, 10 | /** 11 | * domain[0] 12 | * 13 | * + subdomain only 14 | * $ domain only exact match 15 | * [none] domain and subdomain 16 | */ 17 | domains: string[] 18 | } 19 | 20 | export const DIRECTS = { 21 | HOTSPOT_CAPTIVE_PORTAL: { 22 | dns: 'system', 23 | hosts: {}, 24 | realip: false, 25 | ruleset: true, 26 | domains: [ 27 | 'securelogin.com.cn', 28 | '$captive.apple.com', 29 | '$hotspot.cslwifi.com' 30 | ] 31 | }, 32 | SYSTEM: { 33 | dns: 'system', 34 | hosts: {}, 35 | realip: true, 36 | ruleset: false, 37 | domains: [ 38 | '+m2m', 39 | // '+ts.net', // TailScale Magic DNS 40 | // AdGuard -- needs to be real ip otherwise AdGuard App will not recognize it, mustn't be fake ip 41 | '$injections.adguard.org', 42 | '$local.adguard.org', 43 | // Auto Discovery 44 | '+bogon' 45 | ] 46 | } 47 | } as const satisfies Record; 48 | 49 | export const LAN = { 50 | // By default, all hostnames with the suffix '.local' will be resolved by the system. 51 | // Some app like OrbStack uses mDNS and this TLD (orb.local) via mDNS. 52 | // Surge already handles .local with mDNS properly, we should not map to server:system 53 | LOCAL_SPECIAL: { 54 | dns: null, // disable DNS server for now. In the future we might wannna explicitly specify `server: force-syslib` 55 | hosts: {}, 56 | realip: true, 57 | ruleset: false, 58 | domains: [ 59 | '+local' 60 | ] 61 | }, 62 | LAN_WITHOUT_REAL_IP: { 63 | dns: 'system', 64 | hosts: { 65 | '127.0.0.1.sslip.io': ['127.0.0.1'], 66 | '127.atlas.skk.moe': ['127.0.0.1'] 67 | }, 68 | realip: false, 69 | ruleset: true, 70 | domains: [ 71 | // Common Router 72 | // 'zte.home', // ZTE CPE 73 | // 'airbox.home', 74 | // 'bthub.home', 75 | // 'bthomehub.home', 76 | // 'hitronhub.home', 77 | // 'web.setup.home' 78 | 79 | // Aruba Router 80 | '$instant.arubanetworks.com', 81 | '$setmeup.arubanetworks.com', 82 | // ASUS router 83 | '$router.asus.com', 84 | '$repeater.asus.com', 85 | 'asusrouter.com', 86 | // NetGear 87 | 'routerlogin.net', 88 | 'routerlogin.com', 89 | // Tenda WiFi 90 | // 'tendawifi.com', 91 | // TP-Link Router 92 | 'tplinkwifi.net', 93 | 'tplogin.cn', 94 | 'tplinkap.net', 95 | 'tplinkmodem.net', 96 | 'tplinkplclogin.net', 97 | 'tplinkrepeater.net', 98 | // UniFi 99 | '+ui.direct', 100 | '$unifi', 101 | // Other Router 102 | // '$router.com', 103 | '+huaweimobilewifi.com', 104 | '+router', 105 | // 'my.router', 106 | // 'samsung.router', 107 | // '$easy.box', // Vodafone EasyBox 108 | '$aterm.me', 109 | '$console.gl-inet.com', 110 | // '$fritz.box', 111 | // '$fritz.repeater', 112 | // '$myfritz.box', 113 | // '$speedport.ip', // Telekom 114 | // '$giga.cube', // Vodafone GigaCube 115 | '$homerouter.cpe', // Huawei LTE CPE 116 | '$mobile.hotspot', // T-Mobile Hotspot 117 | '$ntt.setup', 118 | '$pi.hole', 119 | '+plex.direct', 120 | // 'web.setup' 121 | // AS112 122 | '+home', 123 | '10.in-addr.arpa', 124 | '16.172.in-addr.arpa', 125 | '17.172.in-addr.arpa', 126 | '18.172.in-addr.arpa', 127 | '19.172.in-addr.arpa', 128 | // '2?.172.in-addr.arpa', 129 | '20.172.in-addr.arpa', 130 | '21.172.in-addr.arpa', 131 | '22.172.in-addr.arpa', 132 | '23.172.in-addr.arpa', 133 | '24.172.in-addr.arpa', 134 | '25.172.in-addr.arpa', 135 | '26.172.in-addr.arpa', 136 | '27.172.in-addr.arpa', 137 | '28.172.in-addr.arpa', 138 | '29.172.in-addr.arpa', 139 | '30.172.in-addr.arpa', 140 | '31.172.in-addr.arpa', 141 | '168.192.in-addr.arpa', 142 | '254.169.in-addr.arpa' 143 | ] 144 | }, 145 | LAN_WITH_REALIP: { 146 | dns: 'system', 147 | hosts: { 148 | // localhost: ['127.0.0.1'] 149 | }, 150 | realip: true, 151 | ruleset: true, 152 | domains: [ 153 | '+lan', 154 | // By default, all hostnames with the suffix '.local' will be resolved by the system. 155 | // Some app like OrbStack uses mDNS and this TLD (orb.local) via mDNS. 156 | // Surge already handles .local with mDNS properly, we should not map to server:system 157 | // '+local', 158 | '+internal', 159 | // 'amplifi.lan', 160 | // '$localhost', 161 | '+localdomain', 162 | 'home.arpa', 163 | '127.0.0.1.sslip.io', 164 | '127.atlas.skk.moe' 165 | ] 166 | } 167 | } as const satisfies Record; 168 | 169 | export const HOSTS = { 170 | HOSTS: { 171 | // not actually used, only for a placeholder 172 | dns: '', 173 | hosts: { 174 | 'cdn.jsdelivr.net': ['cdn.jsdelivr.net.cdn.cloudflare.net'] 175 | }, 176 | realip: false, 177 | ruleset: false, 178 | domains: [] as never[] 179 | } 180 | } as const satisfies Record; 181 | -------------------------------------------------------------------------------- /Source/non_ip/direct.conf: -------------------------------------------------------------------------------- 1 | # $ meta_title Sukka's Ruleset - Direct Rules 2 | # $ custom_build_script 3 | 4 | # >> Proxy Process 5 | PROCESS-NAME,v2ray 6 | PROCESS-NAME,xray 7 | PROCESS-NAME,ss-local 8 | PROCESS-NAME,clash 9 | PROCESS-NAME,ClashX 10 | PROCESS-NAME,trojan 11 | PROCESS-NAME,trojan-go 12 | PROCESS-NAME,privoxy 13 | 14 | # >> Cloudflare Tunnel 15 | PROCESS-NAME,cloudflared 16 | 17 | # >> Downloader 18 | PROCESS-NAME,aria2c 19 | PROCESS-NAME,fdm 20 | PROCESS-NAME,Folx 21 | PROCESS-NAME,NetTransport 22 | PROCESS-NAME,Thunder 23 | PROCESS-NAME,ThunderVIP 24 | PROCESS-NAME,Transmission 25 | PROCESS-NAME,transmission-daemon 26 | PROCESS-NAME,transmission-qt 27 | PROCESS-NAME,BitComet 28 | PROCESS-NAME,uTorrent 29 | PROCESS-NAME,qbittorrent* 30 | PROCESS-NAME,DownloadService 31 | PROCESS-NAME,qBittorrent 32 | PROCESS-NAME,qbittorrent-nox 33 | PROCESS-NAME,WebTorrent 34 | PROCESS-NAME,WebTorrent Helper 35 | PROCESS-NAME,amuled 36 | 37 | # >> LocalSend 38 | PROCESS-NAME,LocalSend 39 | 40 | # > UUBooster 41 | PROCESS-NAME,UUBooster 42 | 43 | # > Xunlei 44 | USER-AGENT,%E8%BF%85%E9%9B%B7 45 | DOMAIN-SUFFIX,xunlei.com 46 | 47 | # tailscale 48 | PROCESS-NAME,tailscaled 49 | # Parsec 50 | PROCESS-NAME,parsecd 51 | # 向日葵远程 52 | PROCESS-NAME,SunloginClient_Desktop 53 | PROCESS-NAME,SunloginClient_Helper 54 | # 百度网盘 55 | PROCESS-NAME,BaiduNetdisk_mac 56 | # 罗技Options 57 | PROCESS-NAME,Logi Options 58 | PROCESS-NAME,Logi Options Daemon 59 | 60 | # >> PT 61 | DOMAIN-SUFFIX,52pt.site 62 | DOMAIN-SUFFIX,acg.rip 63 | DOMAIN-SUFFIX,animebytes.tv 64 | DOMAIN-SUFFIX,aidoru-online.me 65 | DOMAIN-SUFFIX,alpharatio.cc 66 | DOMAIN-SUFFIX,anthelion.me 67 | DOMAIN-SUFFIX,asiancinema.me 68 | DOMAIN-SUFFIX,avgv.cc 69 | DOMAIN-SUFFIX,avistaz.to 70 | DOMAIN-SUFFIX,awesome-hd.me 71 | DOMAIN-SUFFIX,beitai.pt 72 | DOMAIN-SUFFIX,beyond-hd.me 73 | DOMAIN-SUFFIX,bibliotik.me 74 | DOMAIN-SUFFIX,blutopia.xyz 75 | DOMAIN-SUFFIX,broadcasthe.net 76 | DOMAIN-SUFFIX,bt.byr.cn 77 | DOMAIN-SUFFIX,bt.neu6.edu.cn 78 | DOMAIN-SUFFIX,btschool.club 79 | DOMAIN-SUFFIX,ccfbits.org 80 | DOMAIN-SUFFIX,cgpeers.com 81 | DOMAIN-SUFFIX,chdbits.co 82 | DOMAIN-SUFFIX,cinemageddon.net 83 | DOMAIN-SUFFIX,cinematik.net 84 | DOMAIN-SUFFIX,cinemaz.to 85 | DOMAIN-SUFFIX,classix-unlimited.co.uk 86 | DOMAIN-SUFFIX,comicat.org 87 | DOMAIN-SUFFIX,concertos.live 88 | DOMAIN-SUFFIX,dicmusic.club 89 | DOMAIN-SUFFIX,discfan.net 90 | DOMAIN-SUFFIX,dxdhd.com 91 | DOMAIN-SUFFIX,eastgame.org 92 | DOMAIN-SUFFIX,empornium.me 93 | DOMAIN-SUFFIX,et8.org 94 | DOMAIN-SUFFIX,exoticaz.to 95 | DOMAIN-SUFFIX,filelist.io 96 | DOMAIN-SUFFIX,gazellegames.net 97 | DOMAIN-SUFFIX,gfxpeers.net 98 | DOMAIN-SUFFIX,hd-space.org 99 | DOMAIN-SUFFIX,hd4.xyz 100 | DOMAIN-SUFFIX,hd4fans.org 101 | DOMAIN-SUFFIX,hdarea.co 102 | DOMAIN-SUFFIX,hdatmos.club 103 | DOMAIN-SUFFIX,hdbd.us 104 | DOMAIN-SUFFIX,hdbits.org 105 | DOMAIN-SUFFIX,hdchina.org 106 | DOMAIN-SUFFIX,hdcity.city 107 | DOMAIN-SUFFIX,hddolby.com 108 | DOMAIN-SUFFIX,hdfans.org 109 | DOMAIN-SUFFIX,hdhome.org 110 | DOMAIN-SUFFIX,hdpost.top 111 | DOMAIN-SUFFIX,hdroute.org 112 | DOMAIN-SUFFIX,hdsky.me 113 | DOMAIN-SUFFIX,hdtime.org 114 | DOMAIN-SUFFIX,hdupt.com 115 | DOMAIN-SUFFIX,hdzone.me 116 | DOMAIN-SUFFIX,hitpt.com 117 | DOMAIN-SUFFIX,hudbt.hust.edu.cn 118 | DOMAIN-SUFFIX,icetorrent.org 119 | DOMAIN-SUFFIX,j99.info 120 | DOMAIN-SUFFIX,joyhd.net 121 | DOMAIN-SUFFIX,jpopsuki.eu 122 | DOMAIN-SUFFIX,karagarga.in 123 | DOMAIN-SUFFIX,keepfrds.com 124 | DOMAIN-SUFFIX,leaguehd.com 125 | DOMAIN-SUFFIX,lztr.me 126 | DOMAIN-SUFFIX,m-team.cc 127 | DOMAIN-SUFFIX,madsrevolution.net 128 | DOMAIN-SUFFIX,moecat.best 129 | DOMAIN-SUFFIX,msg.vg 130 | DOMAIN-SUFFIX,myanonamouse.net 131 | DOMAIN-SUFFIX,nanyangpt.com 132 | DOMAIN-SUFFIX,ncore.cc 133 | DOMAIN-SUFFIX,nebulance.io 134 | DOMAIN-SUFFIX,nicept.net 135 | DOMAIN-SUFFIX,npupt.com 136 | DOMAIN-SUFFIX,open.cd 137 | DOMAIN-SUFFIX,oppaiti.me 138 | DOMAIN-SUFFIX,orpheus.network 139 | DOMAIN-SUFFIX,ourbits.club 140 | DOMAIN-SUFFIX,passthepopcorn.me 141 | DOMAIN-SUFFIX,pornbits.net 142 | DOMAIN-SUFFIX,privatehd.to 143 | DOMAIN-SUFFIX,pterclub.com 144 | DOMAIN-SUFFIX,pthome.net 145 | DOMAIN-SUFFIX,ptsbao.club 146 | DOMAIN-SUFFIX,redacted.ch 147 | DOMAIN-SUFFIX,sdbits.org 148 | DOMAIN-SUFFIX,skyey2.com 149 | DOMAIN-SUFFIX,soulvoice.club 150 | DOMAIN-SUFFIX,springsunday.net 151 | DOMAIN-SUFFIX,tjupt.org 152 | DOMAIN-SUFFIX,totheglory.im 153 | DOMAIN-SUFFIX,trontv.com 154 | DOMAIN-SUFFIX,u2.dmhy.org 155 | DOMAIN-SUFFIX,uhdbits.org 156 | 157 | # >> Academic 158 | 159 | # 中国知网 160 | DOMAIN-KEYWORD,cnki.net 161 | 162 | # 万方 163 | DOMAIN-KEYWORD,wanfangdata 164 | 165 | # 法律数据库 166 | DOMAIN-KEYWORD,pkulaw 167 | DOMAIN-KEYWORD,westlawchina 168 | 169 | # 查找 DOI 170 | DOMAIN-SUFFIX,doi.org 171 | 172 | # SCI 综合 173 | DOMAIN-SUFFIX,researchgate.net 174 | DOMAIN-SUFFIX,springer.com 175 | DOMAIN-SUFFIX,blackwell-synergy.com 176 | DOMAIN-SUFFIX,sciencemag.org 177 | DOMAIN-SUFFIX,jstor.org 178 | DOMAIN-SUFFIX,cabdirect.org 179 | DOMAIN-SUFFIX,ieee.org 180 | DOMAIN-SUFFIX,nature.com 181 | DOMAIN-SUFFIX,osapublishing.org 182 | 183 | ## Willey 旗下 184 | DOMAIN-SUFFIX,wiley.com 185 | DOMAIN-KEYWORD,readcube 186 | 187 | ## Elsevier 旗下 188 | DOMAIN-SUFFIX,scopus.com 189 | DOMAIN-KEYWORD,sciencedirect 190 | DOMAIN-KEYWORD,elsevier 191 | DOMAIN-KEYWORD,deepdyve 192 | DOMAIN-KEYWORD,els-cdn 193 | 194 | ## Oxford 出版系列 195 | DOMAIN-SUFFIX,oup.com 196 | 197 | ## Springer 旗下 198 | DOMAIN-SUFFIX,springernature.com 199 | 200 | # ACM (Association for Computing Machinery) 201 | DOMAIN-SUFFIX,acm.org 202 | -------------------------------------------------------------------------------- /Build/lib/writing-strategy/clash.ts: -------------------------------------------------------------------------------- 1 | import { appendSetElementsToArray } from 'foxts/append-set-elements-to-array'; 2 | import { BaseWriteStrategy } from './base'; 3 | import { noop } from 'foxts/noop'; 4 | import { notSupported, withBannerArray } from '../misc'; 5 | import { fastIpVersion } from 'foxts/fast-ip-version'; 6 | import { OUTPUT_CLASH_DIR } from '../../constants/dir'; 7 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 8 | import { MARKER_DOMAIN } from '../../constants/description'; 9 | 10 | export class ClashDomainSet extends BaseWriteStrategy { 11 | public readonly name = 'clash domainset'; 12 | 13 | // readonly type = 'domainset'; 14 | readonly fileExtension = 'txt'; 15 | readonly type = 'domainset'; 16 | 17 | protected result: string[] = [MARKER_DOMAIN]; 18 | 19 | constructor(public readonly outputDir = OUTPUT_CLASH_DIR) { 20 | super(outputDir); 21 | } 22 | 23 | withPadding = withBannerArray; 24 | 25 | writeDomain(domain: string): void { 26 | this.result.push(domain); 27 | } 28 | 29 | writeDomainSuffix(domain: string): void { 30 | this.result.push('+.' + domain); 31 | } 32 | 33 | writeDomainKeywords = noop; 34 | writeDomainWildcard = noop; 35 | writeUserAgents = noop; 36 | writeProcessNames = noop; 37 | writeProcessPaths = noop; 38 | writeUrlRegexes = noop; 39 | writeIpCidrs = noop; 40 | writeIpCidr6s = noop; 41 | writeGeoip = noop; 42 | writeIpAsns = noop; 43 | writeSourceIpCidrs = noop; 44 | writeSourcePorts = noop; 45 | writeDestinationPorts = noop; 46 | writeProtocols = noop; 47 | writeOtherRules = noop; 48 | } 49 | 50 | export class ClashIPSet extends BaseWriteStrategy { 51 | public readonly name = 'clash ipcidr'; 52 | 53 | // readonly type = 'domainset'; 54 | readonly fileExtension = 'txt'; 55 | readonly type = 'ip'; 56 | 57 | protected result: string[] = []; 58 | 59 | constructor(public readonly outputDir = OUTPUT_CLASH_DIR) { 60 | super(outputDir); 61 | } 62 | 63 | withPadding = withBannerArray; 64 | 65 | writeDomain = notSupported('writeDomain'); 66 | writeDomainSuffix = notSupported('writeDomainSuffix'); 67 | writeDomainKeywords = notSupported('writeDomainKeywords'); 68 | writeDomainWildcard = notSupported('writeDomainWildcards'); 69 | writeUserAgents = notSupported('writeUserAgents'); 70 | writeProcessNames = notSupported('writeProcessNames'); 71 | writeProcessPaths = notSupported('writeProcessPaths'); 72 | writeUrlRegexes = notSupported('writeUrlRegexes'); 73 | writeIpCidrs(ipCidr: string[]): void { 74 | appendArrayInPlace(this.result, ipCidr); 75 | } 76 | 77 | writeIpCidr6s(ipCidr6: string[]): void { 78 | appendArrayInPlace(this.result, ipCidr6); 79 | } 80 | 81 | writeGeoip = notSupported('writeGeoip'); 82 | writeIpAsns = notSupported('writeIpAsns'); 83 | writeSourceIpCidrs = notSupported('writeSourceIpCidrs'); 84 | writeSourcePorts = notSupported('writeSourcePorts'); 85 | writeDestinationPorts = noop; 86 | writeProtocols = noop; 87 | writeOtherRules = noop; 88 | } 89 | 90 | export class ClashClassicRuleSet extends BaseWriteStrategy { 91 | public readonly name: string = 'clash classic ruleset'; 92 | 93 | readonly fileExtension = 'txt'; 94 | 95 | protected result: string[] = [`DOMAIN,${MARKER_DOMAIN}`]; 96 | 97 | constructor(public readonly type: 'ip' | 'non_ip' | (string & {}), public readonly outputDir = OUTPUT_CLASH_DIR) { 98 | super(outputDir); 99 | } 100 | 101 | withPadding = withBannerArray; 102 | 103 | writeDomain(domain: string): void { 104 | this.result.push('DOMAIN,' + domain); 105 | } 106 | 107 | writeDomainSuffix(domain: string): void { 108 | this.result.push('DOMAIN-SUFFIX,' + domain); 109 | } 110 | 111 | writeDomainKeywords(keyword: Set): void { 112 | appendSetElementsToArray(this.result, keyword, i => `DOMAIN-KEYWORD,${i}`); 113 | } 114 | 115 | writeDomainWildcard(wildcard: string): void { 116 | this.result.push(`DOMAIN-WILDCARD,${wildcard}`); 117 | } 118 | 119 | writeUserAgents = noop; 120 | 121 | writeProcessNames(processName: Set): void { 122 | appendSetElementsToArray(this.result, processName, i => `PROCESS-NAME,${i}`); 123 | } 124 | 125 | writeProcessPaths(processPath: Set): void { 126 | appendSetElementsToArray(this.result, processPath, i => `PROCESS-PATH,${i}`); 127 | } 128 | 129 | writeUrlRegexes = noop; 130 | 131 | writeIpCidrs(ipCidr: string[], noResolve: boolean): void { 132 | for (let i = 0, len = ipCidr.length; i < len; i++) { 133 | this.result.push(`IP-CIDR,${ipCidr[i]}${noResolve ? ',no-resolve' : ''}`); 134 | } 135 | } 136 | 137 | writeIpCidr6s(ipCidr6: string[], noResolve: boolean): void { 138 | for (let i = 0, len = ipCidr6.length; i < len; i++) { 139 | this.result.push(`IP-CIDR6,${ipCidr6[i]}${noResolve ? ',no-resolve' : ''}`); 140 | } 141 | } 142 | 143 | writeGeoip(geoip: Set, noResolve: boolean): void { 144 | appendSetElementsToArray(this.result, geoip, i => `GEOIP,${i}${noResolve ? ',no-resolve' : ''}`); 145 | } 146 | 147 | writeIpAsns(asns: Set, noResolve: boolean): void { 148 | appendSetElementsToArray(this.result, asns, i => `IP-ASN,${i}${noResolve ? ',no-resolve' : ''}`); 149 | } 150 | 151 | writeSourceIpCidrs(sourceIpCidr: string[]): void { 152 | for (let i = 0, len = sourceIpCidr.length; i < len; i++) { 153 | const value = sourceIpCidr[i]; 154 | if (value.includes('/')) { 155 | this.result.push(`SRC-IP-CIDR,${value}`); 156 | continue; 157 | } 158 | const v = fastIpVersion(value); 159 | if (v === 4) { 160 | this.result.push(`SRC-IP-CIDR,${value}/32`); 161 | continue; 162 | } 163 | if (v === 6) { 164 | this.result.push(`SRC-IP-CIDR6,${value}/128`); 165 | continue; 166 | } 167 | } 168 | } 169 | 170 | writeSourcePorts(port: Set): void { 171 | appendSetElementsToArray(this.result, port, i => `SRC-PORT,${i}`); 172 | } 173 | 174 | writeDestinationPorts(port: Set): void { 175 | appendSetElementsToArray(this.result, port, i => `DST-PORT,${i}`); 176 | } 177 | 178 | writeProtocols(protocol: Set): void { 179 | // Mihomo only matches UDP/TCP: https://wiki.metacubex.one/en/config/rules/#network 180 | 181 | // protocol has already be normalized and will only contain upppercase 182 | if (protocol.has('UDP')) { 183 | this.result.push('NETWORK,UDP'); 184 | } 185 | if (protocol.has('TCP')) { 186 | this.result.push('NETWORK,TCP'); 187 | } 188 | } 189 | 190 | writeOtherRules = noop; 191 | } 192 | -------------------------------------------------------------------------------- /Build/validate-gfwlist.ts: -------------------------------------------------------------------------------- 1 | import { processLine } from './lib/process-line'; 2 | import { fastNormalizeDomain } from './lib/normalize-domain'; 3 | import { HostnameSmolTrie } from './lib/trie'; 4 | import yauzl from 'yauzl-promise'; 5 | import { fetchRemoteTextByLine } from './lib/fetch-text-by-line'; 6 | import path from 'node:path'; 7 | import { OUTPUT_SURGE_DIR, SOURCE_DIR } from './constants/dir'; 8 | import { createRetrieKeywordFilter as createKeywordFilter } from 'foxts/retrie'; 9 | import { $$fetch } from './lib/fetch-retry'; 10 | import runAgainstSourceFile from './lib/run-against-source-file'; 11 | import { nullthrow } from 'foxts/guard'; 12 | import { Buffer } from 'node:buffer'; 13 | import { GLOBAL } from '../Source/non_ip/global'; 14 | 15 | export async function getTopOneMillionDomains() { 16 | const { parse: csvParser } = await import('csv-parse'); 17 | 18 | const topDomainTrie = new HostnameSmolTrie(); 19 | const csvParse = csvParser({ columns: false, skip_empty_lines: true }); 20 | 21 | const topDomainsZipBody = await (await $$fetch('https://tranco-list.eu/top-1m.csv.zip', { 22 | headers: { 23 | accept: '*/*', 24 | 'user-agent': 'curl/8.12.1' 25 | } 26 | })).arrayBuffer(); 27 | let entry: yauzl.Entry | null = null; 28 | for await (const e of await yauzl.fromBuffer(Buffer.from(topDomainsZipBody))) { 29 | if (e.filename === 'top-1m.csv') { 30 | entry = e; 31 | break; 32 | } 33 | } 34 | 35 | const { promise, resolve, reject } = Promise.withResolvers(); 36 | 37 | const readable = await nullthrow(entry, 'top-1m.csv entry not found').openReadStream(); 38 | const parser = readable.pipe(csvParse); 39 | parser.on('readable', () => { 40 | let record; 41 | while ((record = parser.read()) !== null) { 42 | topDomainTrie.add(record[1]); 43 | } 44 | }); 45 | 46 | parser.on('end', () => { 47 | resolve(topDomainTrie); 48 | }); 49 | parser.on('error', (err) => { 50 | reject(err); 51 | }); 52 | 53 | return promise; 54 | } 55 | 56 | export async function parseGfwList() { 57 | const whiteSet = new Set(); 58 | const gfwListTrie = new HostnameSmolTrie(); 59 | 60 | let totalGfwSize = 0; 61 | 62 | const gfwlistIgnoreLineKwfilter = createKeywordFilter([ 63 | '.*', 64 | '*', 65 | '=', 66 | '[', 67 | '/', 68 | '?' 69 | ]); 70 | 71 | const text = await (await $$fetch('https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt')).text(); 72 | for (const l of atob(text).split('\n')) { 73 | const line = processLine(l); 74 | if (!line) continue; 75 | 76 | if (gfwlistIgnoreLineKwfilter(line)) { 77 | continue; 78 | } 79 | if (line.startsWith('@@||')) { 80 | whiteSet.add('.' + line.slice(4)); 81 | continue; 82 | } 83 | if (line.startsWith('@@|http://')) { 84 | whiteSet.add(line.slice(10)); 85 | continue; 86 | } 87 | if (line.startsWith('@@|https://')) { 88 | whiteSet.add(line.slice(11)); 89 | continue; 90 | } 91 | if (line.startsWith('||')) { 92 | gfwListTrie.add('.' + line.slice(2)); 93 | continue; 94 | } 95 | if (line.startsWith('|')) { 96 | gfwListTrie.add(line.slice(1)); 97 | continue; 98 | } 99 | if (line.startsWith('.')) { 100 | gfwListTrie.add(line); 101 | continue; 102 | } 103 | const d = fastNormalizeDomain(line); 104 | if (d) { 105 | totalGfwSize++; 106 | gfwListTrie.add(d); 107 | continue; 108 | } 109 | } 110 | for await (const l of await fetchRemoteTextByLine('https://raw.githubusercontent.com/Loyalsoldier/cn-blocked-domain/release/domains.txt', true)) { 111 | totalGfwSize++; 112 | gfwListTrie.add(l); 113 | } 114 | for await (const l of await fetchRemoteTextByLine('https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/gfw.txt', true)) { 115 | totalGfwSize++; 116 | gfwListTrie.add(l); 117 | } 118 | 119 | const topDomainTrie = await getTopOneMillionDomains(); 120 | 121 | const keywordSet = new Set(); 122 | 123 | const callback = (domain: string, includeAllSubdomain: boolean) => { 124 | gfwListTrie.whitelist(domain, includeAllSubdomain); 125 | topDomainTrie.whitelist(domain, includeAllSubdomain); 126 | }; 127 | await Promise.all([ 128 | runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip/global.conf'), callback, 'ruleset', keywordSet), 129 | // runAgainstSourceFile(path.join(OUTPUT_SURGE_DIR, 'non_ip/domestic.conf'), callback, 'ruleset', keywordSet), 130 | runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip/reject.conf'), callback, 'ruleset', keywordSet), 131 | runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip/telegram.conf'), callback, 'ruleset', keywordSet), 132 | runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'non_ip/stream.conf'), callback, 'ruleset', keywordSet), 133 | runAgainstSourceFile(path.resolve(SOURCE_DIR, 'non_ip/ai.conf'), callback, 'ruleset', keywordSet), 134 | runAgainstSourceFile(path.resolve(SOURCE_DIR, 'non_ip/microsoft.conf'), callback, 'ruleset', keywordSet), 135 | runAgainstSourceFile(path.resolve(SOURCE_DIR, 'non_ip/apple_services.conf'), callback, 'ruleset', keywordSet), 136 | runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'domainset/reject.conf'), callback, 'domainset'), 137 | runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'domainset/reject_extra.conf'), callback, 'domainset'), 138 | runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'domainset/cdn.conf'), callback, 'domainset') 139 | ]); 140 | 141 | Object.values(GLOBAL).forEach(({ domains }) => { 142 | domains.forEach(domain => { 143 | if (domain[0] === '$') { 144 | callback(domain.slice(1), false); 145 | } else if (domain[0] === '+') { 146 | callback(domain.slice(1), true); 147 | } else { 148 | callback(domain, true); 149 | } 150 | }); 151 | }); 152 | 153 | whiteSet.forEach(domain => gfwListTrie.whitelist(domain, true)); 154 | 155 | let gfwListSize = 0; 156 | gfwListTrie.dump(() => gfwListSize++); 157 | 158 | const kwfilter = createKeywordFilter([...keywordSet]); 159 | 160 | const missingTop10000Gfwed = new Set(); 161 | 162 | topDomainTrie.dump((domain) => { 163 | if (gfwListTrie.has(domain) && !kwfilter(domain)) { 164 | missingTop10000Gfwed.add(domain); 165 | } 166 | }); 167 | 168 | console.log(Array.from(missingTop10000Gfwed).join('\n')); 169 | console.log({ totalGfwSize, gfwListSize, missingSize: missingTop10000Gfwed.size }); 170 | 171 | return [ 172 | whiteSet, 173 | gfwListTrie, 174 | missingTop10000Gfwed 175 | ] as const; 176 | } 177 | 178 | if (require.main === module) { 179 | parseGfwList().catch(console.error); 180 | } 181 | 182 | // python.com waiting-for-sell 183 | -------------------------------------------------------------------------------- /Build/lib/fetch-retry.ts: -------------------------------------------------------------------------------- 1 | import picocolors from 'picocolors'; 2 | import undici, { 3 | interceptors, 4 | Agent 5 | // setGlobalDispatcher 6 | } from 'undici'; 7 | 8 | import type { 9 | Dispatcher, 10 | Response, 11 | RequestInit 12 | } from 'undici'; 13 | import { BetterSqlite3CacheStore } from 'undici-cache-store-better-sqlite3'; 14 | 15 | export type UndiciResponseData = Dispatcher.ResponseData; 16 | 17 | import { inspect } from 'node:util'; 18 | import path from 'node:path'; 19 | import fs from 'node:fs'; 20 | import { CACHE_DIR } from '../constants/dir'; 21 | import { isAbortErrorLike } from 'foxts/abort-error'; 22 | 23 | if (!fs.existsSync(CACHE_DIR)) { 24 | fs.mkdirSync(CACHE_DIR, { recursive: true }); 25 | } 26 | 27 | const agent = new Agent({ allowH2: false }); 28 | 29 | (agent.compose( 30 | interceptors.dns({ 31 | // disable IPv6 32 | dualStack: false, 33 | affinity: 4 34 | // TODO: proper cacheable-lookup, or even DoH 35 | }), 36 | interceptors.retry({ 37 | maxRetries: 5, 38 | minTimeout: 500, // The initial retry delay in milliseconds 39 | maxTimeout: 10 * 1000, // The maximum retry delay in milliseconds 40 | 41 | // TODO: this part of code is only for allow more errors to be retried by default 42 | // This should be removed once https://github.com/nodejs/undici/issues/3728 is implemented 43 | retry(err, { state, opts }, cb) { 44 | const errorCode = 'code' in err ? (err as NodeJS.ErrnoException).code : undefined; 45 | 46 | Object.defineProperty(err, '_url', { 47 | value: opts.method + ' ' + opts.origin?.toString() + opts.path 48 | }); 49 | 50 | // Any code that is not a Undici's originated and allowed to retry 51 | if ( 52 | errorCode === 'ERR_UNESCAPED_CHARACTERS' 53 | || err.message === 'Request path contains unescaped characters' 54 | || err.name === 'AbortError' 55 | ) { 56 | return cb(err); 57 | } 58 | 59 | const statusCode = 'statusCode' in err && typeof err.statusCode === 'number' ? err.statusCode : null; 60 | 61 | // bail out if the status code matches one of the following 62 | if ( 63 | statusCode != null 64 | && ( 65 | statusCode === 401 // Unauthorized, should check credentials instead of retrying 66 | || statusCode === 403 // Forbidden, should check permissions instead of retrying 67 | || statusCode === 404 // Not Found, should check URL instead of retrying 68 | || statusCode === 405 // Method Not Allowed, should check method instead of retrying 69 | ) 70 | ) { 71 | return cb(err); 72 | } 73 | 74 | // if (errorCode === 'UND_ERR_REQ_RETRY') { 75 | // return cb(err); 76 | // } 77 | 78 | const { 79 | maxRetries = 5, 80 | minTimeout = 500, 81 | maxTimeout = 10 * 1000, 82 | timeoutFactor = 2, 83 | methods = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'] 84 | } = opts.retryOptions || {}; 85 | 86 | // If we reached the max number of retries 87 | if (state.counter > maxRetries) { 88 | return cb(err); 89 | } 90 | 91 | // If a set of method are provided and the current method is not in the list 92 | if (Array.isArray(methods) && !methods.includes(opts.method)) { 93 | return cb(err); 94 | } 95 | 96 | const headers = ('headers' in err && typeof err.headers === 'object') ? err.headers : undefined; 97 | 98 | const retryAfterHeader = (headers as Record | null | undefined)?.['retry-after']; 99 | let retryAfter = -1; 100 | if (retryAfterHeader) { 101 | retryAfter = Number(retryAfterHeader); 102 | retryAfter = Number.isNaN(retryAfter) 103 | ? calculateRetryAfterHeader(retryAfterHeader) 104 | : retryAfter * 1e3; // Retry-After is in seconds 105 | } 106 | 107 | const retryTimeout = retryAfter > 0 108 | ? Math.min(retryAfter, maxTimeout) 109 | : Math.min(minTimeout * (timeoutFactor ** (state.counter - 1)), maxTimeout); 110 | 111 | console.log('[fetch retry]', 'schedule retry', { statusCode, retryTimeout, errorCode, url: opts.origin }); 112 | // eslint-disable-next-line sukka/prefer-timer-id -- won't leak 113 | setTimeout(() => cb(null), retryTimeout); 114 | } 115 | // errorCodes: ['UND_ERR_HEADERS_TIMEOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'ETIMEDOUT'] 116 | }), 117 | interceptors.redirect({ 118 | maxRedirections: 5 119 | }), 120 | interceptors.cache({ 121 | store: new BetterSqlite3CacheStore({ 122 | loose: true, 123 | location: path.join(CACHE_DIR, 'undici-better-sqlite3-cache-store.db'), 124 | maxEntrySize: 1024 * 1024 * 100 // 100 MiB 125 | }), 126 | cacheByDefault: 600 // 10 minutes 127 | }) 128 | )); 129 | 130 | function calculateRetryAfterHeader(retryAfter: string) { 131 | const current = Date.now(); 132 | return new Date(retryAfter).getTime() - current; 133 | } 134 | 135 | export class ResponseError extends Error { 136 | readonly code: number; 137 | readonly statusCode: number; 138 | 139 | constructor(public readonly res: T, public readonly url: string, ...args: any[]) { 140 | const statusCode = 'statusCode' in res ? res.statusCode : res.status; 141 | super('HTTP ' + statusCode + ' ' + args.map(_ => inspect(_)).join(' ')); 142 | 143 | // eslint-disable-next-line sukka/unicorn/custom-error-definition -- deliberatly use previous name 144 | this.name = this.constructor.name; 145 | this.res = res; 146 | this.code = statusCode; 147 | this.statusCode = statusCode; 148 | } 149 | } 150 | 151 | export const defaultRequestInit = { 152 | headers: { 153 | 'User-Agent': 'node-fetch' 154 | } 155 | }; 156 | 157 | export async function $$fetch(url: string, init: RequestInit = defaultRequestInit) { 158 | try { 159 | const res = await undici.fetch(url, init); 160 | if (res.status >= 400) { 161 | throw new ResponseError(res, url); 162 | } 163 | 164 | if ((res.status < 200 || res.status > 299) && res.status !== 304) { 165 | throw new ResponseError(res, url); 166 | } 167 | 168 | return res; 169 | } catch (err: unknown) { 170 | if (isAbortErrorLike(err)) { 171 | console.log(picocolors.gray('[fetch abort]'), url); 172 | } else { 173 | console.log(picocolors.gray('[fetch fail]'), url, { name: (err as any).name }, err); 174 | } 175 | 176 | throw err; 177 | } 178 | } 179 | 180 | export const fetch = $$fetch; 181 | 182 | /** @deprecated -- undici.requests doesn't support gzip/br/deflate, and has difficulty w/ undidi cache */ 183 | export async function requestWithLog(url: string, opt?: Parameters[1]) { 184 | try { 185 | const res = await undici.request(url, opt); 186 | if (res.statusCode >= 400) { 187 | throw new ResponseError(res, url); 188 | } 189 | 190 | if ((res.statusCode < 200 || res.statusCode > 299) && res.statusCode !== 304) { 191 | throw new ResponseError(res, url); 192 | } 193 | 194 | return res; 195 | } catch (err: unknown) { 196 | if (isAbortErrorLike(err)) { 197 | console.log(picocolors.gray('[fetch abort]'), url); 198 | } else { 199 | console.log(picocolors.gray('[fetch fail]'), url, { name: (err as any).name }, err); 200 | } 201 | 202 | throw err; 203 | } 204 | } 205 | --------------------------------------------------------------------------------