├── .github ├── dependabot.yml └── workflows │ └── sing-box-rule-set-update.yml ├── .gitignore ├── README.md ├── generate_rule_set.py └── requirements.txt /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/sing-box-rule-set-update.yml: -------------------------------------------------------------------------------- 1 | name: "Update sing-box rule-set" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-rule-set: 10 | name: "Update sing-box rule-set" 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: "Checkout" 16 | uses: actions/checkout@v4 17 | - name: "Setup sing-box" 18 | env: 19 | SING_BOX_DEB_URL: "https://github.com/SagerNet/sing-box/releases/download/v1.11.0/sing-box_1.11.0_linux_amd64.deb" 20 | run: | 21 | set -Eeuo pipefail 22 | wget -O sing-box.deb $SING_BOX_DEB_URL 23 | sudo dpkg -i sing-box.deb 24 | - name: "Setup python venv" 25 | run: | 26 | set -Eeuo pipefail 27 | python3 -m venv venv 28 | source venv/bin/activate 29 | pip3 install -r requirements.txt 30 | - name: "Update rule-set" 31 | run: | 32 | set -Eeuo pipefail 33 | source venv/bin/activate 34 | python3 generate_rule_set.py 35 | - name: "Commit and push" 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | set -Eeuo pipefail 40 | cd rule-set 41 | git init 42 | git config --local user.name "github-actions[bot]" 43 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 44 | git add . 45 | git commit --allow-empty-message --no-edit 46 | git branch -M rule-set 47 | git remote add origin https://github-action:$GITHUB_TOKEN@github.com/Dreista/sing-box-rule-set-cn.git 48 | git push -u origin rule-set --force 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | *~ 3 | 4 | # temporary files which can be created if a process still has a handle open of a deleted file 5 | .fuse_hidden* 6 | 7 | # KDE directory preferences 8 | .directory 9 | 10 | # Linux trash folder which might appear on any partition or disk 11 | .Trash-* 12 | 13 | # .nfs files are created when an open file is removed but is still being accessed 14 | .nfs* 15 | 16 | ### macOS ### 17 | # General 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | 22 | # Icon must end with two \r 23 | Icon 24 | 25 | 26 | # Thumbnails 27 | ._* 28 | 29 | # Files that might appear in the root of a volume 30 | .DocumentRevisions-V100 31 | .fseventsd 32 | .Spotlight-V100 33 | .TemporaryItems 34 | .Trashes 35 | .VolumeIcon.icns 36 | .com.apple.timemachine.donotpresent 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | ### macOS Patch ### 46 | # iCloud generated files 47 | *.icloud 48 | 49 | ### Python ### 50 | # Byte-compiled / optimized / DLL files 51 | __pycache__/ 52 | *.py[cod] 53 | *$py.class 54 | 55 | # C extensions 56 | *.so 57 | 58 | # Distribution / packaging 59 | .Python 60 | build/ 61 | develop-eggs/ 62 | dist/ 63 | downloads/ 64 | eggs/ 65 | .eggs/ 66 | lib/ 67 | lib64/ 68 | parts/ 69 | sdist/ 70 | var/ 71 | wheels/ 72 | share/python-wheels/ 73 | *.egg-info/ 74 | .installed.cfg 75 | *.egg 76 | MANIFEST 77 | 78 | # PyInstaller 79 | # Usually these files are written by a python script from a template 80 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 81 | *.manifest 82 | *.spec 83 | 84 | # Installer logs 85 | pip-log.txt 86 | pip-delete-this-directory.txt 87 | 88 | # Unit test / coverage reports 89 | htmlcov/ 90 | .tox/ 91 | .nox/ 92 | .coverage 93 | .coverage.* 94 | .cache 95 | nosetests.xml 96 | coverage.xml 97 | *.cover 98 | *.py,cover 99 | .hypothesis/ 100 | .pytest_cache/ 101 | cover/ 102 | 103 | # Translations 104 | *.mo 105 | *.pot 106 | 107 | # Django stuff: 108 | *.log 109 | local_settings.py 110 | db.sqlite3 111 | db.sqlite3-journal 112 | 113 | # Flask stuff: 114 | instance/ 115 | .webassets-cache 116 | 117 | # Scrapy stuff: 118 | .scrapy 119 | 120 | # Sphinx documentation 121 | docs/_build/ 122 | 123 | # PyBuilder 124 | .pybuilder/ 125 | target/ 126 | 127 | # Jupyter Notebook 128 | .ipynb_checkpoints 129 | 130 | # IPython 131 | profile_default/ 132 | ipython_config.py 133 | 134 | # pyenv 135 | # For a library or package, you might want to ignore these files since the code is 136 | # intended to run in multiple environments; otherwise, check them in: 137 | # .python-version 138 | 139 | # pipenv 140 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 141 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 142 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 143 | # install all needed dependencies. 144 | #Pipfile.lock 145 | 146 | # poetry 147 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 148 | # This is especially recommended for binary packages to ensure reproducibility, and is more 149 | # commonly ignored for libraries. 150 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 151 | #poetry.lock 152 | 153 | # pdm 154 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 155 | #pdm.lock 156 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 157 | # in version control. 158 | # https://pdm.fming.dev/#use-with-ide 159 | .pdm.toml 160 | 161 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 162 | __pypackages__/ 163 | 164 | # Celery stuff 165 | celerybeat-schedule 166 | celerybeat.pid 167 | 168 | # SageMath parsed files 169 | *.sage.py 170 | 171 | # Environments 172 | .env 173 | .venv 174 | env/ 175 | venv/ 176 | ENV/ 177 | env.bak/ 178 | venv.bak/ 179 | 180 | # Spyder project settings 181 | .spyderproject 182 | .spyproject 183 | 184 | # Rope project settings 185 | .ropeproject 186 | 187 | # mkdocs documentation 188 | /site 189 | 190 | # mypy 191 | .mypy_cache/ 192 | .dmypy.json 193 | dmypy.json 194 | 195 | # Pyre type checker 196 | .pyre/ 197 | 198 | # pytype static type analyzer 199 | .pytype/ 200 | 201 | # Cython debug symbols 202 | cython_debug/ 203 | 204 | # PyCharm 205 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 206 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 207 | # and can be added to the global gitignore or merged into this file. For a more nuclear 208 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 209 | #.idea/ 210 | 211 | ### Python Patch ### 212 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 213 | poetry.toml 214 | 215 | # ruff 216 | .ruff_cache/ 217 | 218 | # LSP config files 219 | pyrightconfig.json 220 | 221 | ### Windows ### 222 | # Windows thumbnail cache files 223 | Thumbs.db 224 | Thumbs.db:encryptable 225 | ehthumbs.db 226 | ehthumbs_vista.db 227 | 228 | # Dump file 229 | *.stackdump 230 | 231 | # Folder config file 232 | [Dd]esktop.ini 233 | 234 | # Recycle Bin used on file shares 235 | $RECYCLE.BIN/ 236 | 237 | # Windows Installer files 238 | *.cab 239 | *.msi 240 | *.msix 241 | *.msm 242 | *.msp 243 | 244 | # Windows shortcuts 245 | *.lnk 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sing-box rule-set for China 2 | 3 | Generated & aggregated daily from mulitple sources. Rule sets available at `rule-set` branch. 4 | 5 | ```json 6 | { 7 | "route": { 8 | "rule_set": [ 9 | { 10 | "tag": "GeoSite-CN", 11 | "type": "remote", 12 | "format": "binary", 13 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/accelerated-domains.china.conf.srs" 14 | }, 15 | { 16 | "tag": "GeoSite-Apple-CN", 17 | "type": "remote", 18 | "format": "binary", 19 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/apple.china.conf.srs" 20 | }, 21 | { 22 | "tag": "GeoSite-Google-CN", 23 | "type": "remote", 24 | "format": "binary", 25 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/google.china.conf.srs" 26 | }, 27 | { 28 | "tag": "GeoIP-APNIC-CN-IPv4", 29 | "type": "remote", 30 | "format": "binary", 31 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/apnic-cn-ipv4.srs" 32 | }, 33 | { 34 | "tag": "GeoIP-APNIC-CN-IPv6", 35 | "type": "remote", 36 | "format": "binary", 37 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/apnic-cn-ipv6.srs" 38 | }, 39 | { 40 | "tag": "GeoIP-MaxMind-CN-IPv4", 41 | "type": "remote", 42 | "format": "binary", 43 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/maxmind-cn-ipv4.srs" 44 | }, 45 | { 46 | "tag": "GeoIP-MaxMind-CN-IPv6", 47 | "type": "remote", 48 | "format": "binary", 49 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/maxmind-cn-ipv6.srs" 50 | }, 51 | { 52 | "tag": "GeoIP-ChnRoutes2-CN-IPv4", 53 | "type": "remote", 54 | "format": "binary", 55 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/chnroutes.txt.srs" 56 | }, 57 | { 58 | "tag": "AdGuard-DNS-Filter", 59 | "type": "remote", 60 | "format": "binary", 61 | "url": "https://raw.githubusercontent.com/Dreista/sing-box-rule-set-cn/rule-set/filter.txt.srs" 62 | } 63 | ] 64 | } 65 | } 66 | ``` 67 | 68 | ## felixonmars/dnsmasq-china-list 69 | 70 | Source: https://github.com/felixonmars/dnsmasq-china-list 71 | 72 | ### accelerated-domains.china.conf 73 | 74 | [Rule set](/../../raw/rule-set/accelerated-domains.china.conf.srs) ([source](/../../raw/rule-set/accelerated-domains.china.conf.json)) 75 | 76 | ### apple.china.conf 77 | 78 | [Rule set](/../../raw/rule-set/apple.china.conf.srs) ([source](/../../raw/rule-set/apple.china.conf.json)) 79 | 80 | ### google.china.conf 81 | 82 | [Rule set](/../../raw/rule-set/google.china.conf.srs) ([source](/../../raw/rule-set/google.china.conf.json)) 83 | 84 | ## misakaio/chnroutes2 85 | 86 | Source: https://github.com/misakaio/chnroutes2 87 | 88 | Uses BGP feed from various sources. Seems to be IPv4 only. 89 | 90 | ### chnroutes.txt 91 | 92 | [Rule set](/../../raw/rule-set/chnroutes.txt.srs) ([source](/../../raw/rule-set/chnroutes.txt.json)) 93 | 94 | ## delegated-apnic-latest 95 | 96 | Source: https://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest 97 | 98 | ### apnic-cn-ipv4 99 | 100 | [Rule set](/../../raw/rule-set/apnic-cn-ipv4.srs) ([source](/../../raw/rule-set/apnic-cn-ipv4.json)) 101 | 102 | ### apnic-cn-ipv6 103 | 104 | [Rule set](/../../raw/rule-set/apnic-cn-ipv6.srs) ([source](/../../raw/rule-set/apnic-cn-ipv6.json)) 105 | 106 | ## Dreamacro/maxmind-geoip 107 | 108 | Source: https://github.com/Dreamacro/maxmind-geoip 109 | 110 | ### maxmind-cn-ipv4 111 | 112 | [Rule set](/../../raw/rule-set/maxmind-cn-ipv4.srs) ([source](/../../raw/rule-set/maxmind-cn-ipv4.json)) 113 | 114 | ### maxmind-cn-ipv6 115 | 116 | [Rule set](/../../raw/rule-set/maxmind-cn-ipv6.srs) ([source](/../../raw/rule-set/maxmind-cn-ipv6.json)) 117 | 118 | ## AdguardTeam/AdGuardSDNSFilter 119 | 120 | Source: https://github.com/AdguardTeam/AdGuardSDNSFilter 121 | 122 | ### filter.txt 123 | 124 | [Rule set](/../../raw/rule-set/filter.txt.srs) ([source](/../../raw/rule-set/filter.txt)) 125 | -------------------------------------------------------------------------------- /generate_rule_set.py: -------------------------------------------------------------------------------- 1 | import math 2 | import re 3 | import maxminddb 4 | import requests 5 | import json 6 | import os 7 | from aggregate6 import aggregate 8 | 9 | dnsmasq_china_list = [ 10 | "https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf", 11 | "https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/apple.china.conf", 12 | "https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/google.china.conf" 13 | ] 14 | 15 | chnroutes2 = [ 16 | "https://raw.githubusercontent.com/misakaio/chnroutes2/master/chnroutes.txt" 17 | ] 18 | 19 | apnic = [ 20 | "https://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest" 21 | ] 22 | 23 | maxmind = [ 24 | "https://raw.githubusercontent.com/Dreamacro/maxmind-geoip/release/Country.mmdb" 25 | ] 26 | 27 | adguard = [ 28 | "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt" 29 | ] 30 | 31 | output_dir = "./rule-set" 32 | 33 | 34 | def convert_dnsmasq(url: str) -> str: 35 | r = requests.get(url) 36 | domain_suffix_list = [] 37 | if r.status_code == 200: 38 | lines = r.text.splitlines() 39 | for line in lines: 40 | if not line.startswith("#"): 41 | domain = re.match(r"server=\/(.*)\/(.*)", line) 42 | if domain: 43 | domain_suffix_list.append(domain.group(1)) 44 | result = { 45 | "version": 1, 46 | "rules": [ 47 | { 48 | "domain_suffix": [] 49 | } 50 | ] 51 | } 52 | result["rules"][0]["domain_suffix"] = domain_suffix_list 53 | filepath = os.path.join(output_dir, url.split("/")[-1] + ".json") 54 | with open(filepath, "w") as f: 55 | f.write(json.dumps(result, indent=4)) 56 | return filepath 57 | 58 | 59 | def convert_chnroutes2(url: str) -> str: 60 | r = requests.get(url) 61 | ip_cidr_list = [] 62 | if r.status_code == 200: 63 | lines = r.text.splitlines() 64 | for line in lines: 65 | if not line.startswith("#"): 66 | ip_cidr_list.append(line) 67 | result = { 68 | "version": 1, 69 | "rules": [ 70 | { 71 | "ip_cidr": [] 72 | } 73 | ] 74 | } 75 | result["rules"][0]["ip_cidr"] = ip_cidr_list 76 | filepath = os.path.join(output_dir, url.split("/")[-1] + ".json") 77 | with open(filepath, "w") as f: 78 | f.write(json.dumps(result, indent=4)) 79 | return filepath 80 | 81 | 82 | def convert_apnic(url: str, country_code: str, ip_version: str) -> str: 83 | r = requests.get(url) 84 | ip_cidr_list = [] 85 | if r.status_code == 200: 86 | lines = r.text.splitlines() 87 | for line in lines: 88 | if not line.startswith("#"): 89 | if line.split("|")[1] == country_code and line.split("|")[2] == ip_version: 90 | if ip_version == "ipv4": 91 | ip_cidr_list.append(line.split( 92 | "|")[3] + "/" + str(32 - int(math.log2(int(line.split("|")[4]))))) 93 | else: 94 | ip_cidr_list.append(line.split( 95 | "|")[3] + "/" + line.split("|")[4]) 96 | result = { 97 | "version": 1, 98 | "rules": [ 99 | { 100 | "ip_cidr": [] 101 | } 102 | ] 103 | } 104 | result["rules"][0]["ip_cidr"] = aggregate(ip_cidr_list) 105 | filepath = os.path.join(output_dir, "apnic-" + 106 | country_code.lower() + "-" + ip_version + ".json") 107 | with open(filepath, "w") as f: 108 | f.write(json.dumps(result, indent=4)) 109 | return filepath 110 | 111 | 112 | def convert_maxmind(url: str, country_code: str, ip_version: str) -> str: 113 | r = requests.get(url) 114 | with open("Country.mmdb", "wb") as f: 115 | f.write(r.content) 116 | f.close() 117 | reader = maxminddb.open_database("Country.mmdb") 118 | ip_cidr_list = [] 119 | for cidr, info in reader.__iter__(): 120 | if info.get("country") is not None: 121 | if info["country"]["iso_code"] == country_code: 122 | if ip_version == "ipv4" and cidr.version == 4: 123 | ip_cidr_list.append(str(cidr)) 124 | elif ip_version == "ipv6" and cidr.version == 6: 125 | ip_cidr_list.append(str(cidr)) 126 | elif info.get("registered_country") is not None: 127 | if info["registered_country"]["iso_code"] == country_code: 128 | if ip_version == "ipv4" and cidr.version == 4: 129 | ip_cidr_list.append(str(cidr)) 130 | elif ip_version == "ipv6" and cidr.version == 6: 131 | ip_cidr_list.append(str(cidr)) 132 | reader.close() 133 | result = { 134 | "version": 1, 135 | "rules": [ 136 | { 137 | "ip_cidr": [] 138 | } 139 | ] 140 | } 141 | result["rules"][0]["ip_cidr"] = aggregate(ip_cidr_list) 142 | filepath = os.path.join(output_dir, "maxmind-" + 143 | country_code.lower() + "-" + ip_version + ".json") 144 | with open(filepath, "w") as f: 145 | f.write(json.dumps(result, indent=4)) 146 | return filepath 147 | 148 | 149 | def get_adguard(url: str) -> str: 150 | r = requests.get(url) 151 | filepath = os.path.join(output_dir, url.split("/")[-1]) 152 | with open(filepath, "wb") as f: 153 | f.write(r.content) 154 | return filepath 155 | 156 | 157 | def main(): 158 | files = [] 159 | files_adguard = [] 160 | os.mkdir(output_dir) 161 | for url in dnsmasq_china_list: 162 | filepath = convert_dnsmasq(url) 163 | files.append(filepath) 164 | for url in chnroutes2: 165 | filepath = convert_chnroutes2(url) 166 | files.append(filepath) 167 | for url in apnic: 168 | filepath = convert_apnic(url, "CN", "ipv4") 169 | files.append(filepath) 170 | filepath = convert_apnic(url, "CN", "ipv6") 171 | files.append(filepath) 172 | for url in maxmind: 173 | filepath = convert_maxmind(url, "CN", "ipv4") 174 | files.append(filepath) 175 | filepath = convert_maxmind(url, "CN", "ipv6") 176 | files.append(filepath) 177 | for url in adguard: 178 | filepath = get_adguard(url) 179 | files_adguard.append(filepath) 180 | print("rule-set source:") 181 | for filepath in files: 182 | print(filepath) 183 | for filepath in files_adguard: 184 | print(filepath) 185 | for filepath in files: 186 | srs_path = filepath.replace(".json", ".srs") 187 | os.system("sing-box rule-set compile --output " + 188 | srs_path + " " + filepath) 189 | for filepath in files_adguard: 190 | srs_path = filepath + ".srs" 191 | os.system("sing-box rule-set convert --type adguard --output " + 192 | srs_path + " " + filepath) 193 | 194 | 195 | if __name__ == "__main__": 196 | main() 197 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aggregate6==1.0.12 2 | requests 3 | maxminddb==2.6.1 4 | --------------------------------------------------------------------------------