diff --git a/.gitignore b/.gitignore index b946ebd..c06ca6f 100644 --- a/.gitignore +++ b/.gitignore @@ -171,5 +171,5 @@ poetry.toml # End of https://www.toptal.com/developers/gitignore/api/python /.idea -/custom_proxy_hosts.json - +/cache +/custom_proxy_hosts.json \ No newline at end of file diff --git a/dnscrypt_to_smartdns.py b/dnscrypt_to_smartdns.py new file mode 100644 index 0000000..1bd97c8 --- /dev/null +++ b/dnscrypt_to_smartdns.py @@ -0,0 +1,84 @@ +import subprocess +import logging +from urllib.request import urlopen + +from utils.dnsstamps import parse, DNSoverHTTPS + +SMARTDNS_GFW_CONF_FILE = '/etc/smartdns/conf.d/gfw.conf' + +# https://github.com/DNSCrypt/dnscrypt-resolvers +PUBLIC_RESOLVER_URL_LIST = [ + "https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md", + "https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md" + "https://dnsr.evilvibes.com/v3/public-resolvers.md" +] + + +def get_public_resolver_md() -> str: + for url in PUBLIC_RESOLVER_URL_LIST: + try: + logging.info('request {url}'.format(url=url)) + with urlopen(url, timeout=15) as responsee: + return responsee.read().decode('utf-8') + except: + pass + raise IOError("can't download public-resolvers.md") + + +def get_stamps(): + resolver_md: str = get_public_resolver_md() + lines = resolver_md.splitlines() + stamps = list( + map( + parse, + filter(lambda x: x.startswith('sdns://'), lines) + ) + ) + return stamps + + +def get_not_china_doh_list(): + stamps = get_stamps() + + def is_match(s) -> bool: + if isinstance(s, DNSoverHTTPS) is False: + return False + if s.nofilter is False: + return False + if s.dnssec is False: + return False + return True + + return list(filter( + is_match, + stamps + )) + + +def get_smartdns_config(): + stamps = get_not_china_doh_list() + lines = map( + lambda x: 'server-https https://' + x.hostname + x.path + ' -group GFW -exclude-default-group', + stamps + ) + return '\n'.join(lines) + + +def write_smartdns_config(): + conf_txt = get_smartdns_config() + with open(SMARTDNS_GFW_CONF_FILE, 'w') as f: + f.write(conf_txt) + + +def reload_smartdns(): + subprocess.run(["/etc/init.d/smartdns", "reload"]) + + +def main(): + write_smartdns_config() + reload_smartdns() + + +if __name__ == '__main__': + s = get_smartdns_config() + print(s) diff --git a/dnsmasq-china-list.sh b/dnsmasq-china-list.sh new file mode 100755 index 0000000..be4eead --- /dev/null +++ b/dnsmasq-china-list.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +PROXY="socks5h://127.0.0.1:1080" + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +CACHE_FOLDER="${SCRIPT_DIR}/cache" + +mkdir -p "${CACHE_FOLDER}" + +curl --proxy "${PROXY}" https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/refs/heads/master/accelerated-domains.china.conf -o "${CACHE_FOLDER}/accelerated-domains.china.conf" +curl --proxy "${PROXY}" https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/refs/heads/master/google.china.conf -o "${CACHE_FOLDER}/google.china.conf" +curl --proxy "${PROXY}" https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/refs/heads/master/apple.china.conf -o "${CACHE_FOLDER}/apple.china.conf" +curl --proxy "${PROXY}" https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/refs/heads/master/bogus-nxdomain.china.conf -o "${CACHE_FOLDER}/bogus-nxdomain.china.conf" + +cp "${CACHE_FOLDER}/*.conf" /tmp/dnsmasq.d/ +grep -v '^#' "${CACHE_FOLDER}/bogus-nxdomain.china.conf" | grep -v '^$' | sed -e 's/=/ /g' > /etc/smartdns/conf.d/bogus-nxdomain.conf \ No newline at end of file diff --git a/main.py b/gfwlist_to_dns.py similarity index 92% rename from main.py rename to gfwlist_to_dns.py index 3cf7000..75529c6 100644 --- a/main.py +++ b/gfwlist_to_dns.py @@ -10,7 +10,8 @@ from urllib.request import urlopen PROXY_DNS_IP = '127.0.0.1' PROXY_DNS_PORT = '5353' -DNSMASQ_RULES_FILE = '/tmp/dnsmasq.d/gfwlist' +DNSMASQ_RULES_FILE = '/tmp/dnsmasq.d/gfwlist.conf' +SMARTDNS_DOMAIN_SET_FILE = '/etc/smartdns/domain-set/gfwlist.conf' # https://github.com/gfwlist/gfwlist GFWLIST_URL_LIST = [ @@ -214,11 +215,33 @@ def get_dnsmasq_text() -> str: return '\n'.join(rule_list) -def main(): +def write_dnsmasq(): dnsmasq_text = get_dnsmasq_text() with open(DNSMASQ_RULES_FILE, 'w') as f: f.write(dnsmasq_text) - subprocess.run(["/etc/init.d/dnsmasq", "restart"]) + + +def reload_dnsmasq(): + subprocess.run(["/etc/init.d/dnsmasq", "reload"]) + + +def get_smartdns_domain_set() -> str: + return '\n'.join(get_proxy_hosts()) + + +def write_smartdns_domain_set(): + domain_set_text = get_smartdns_domain_set() + with open(SMARTDNS_DOMAIN_SET_FILE, 'w') as f: + f.write(domain_set_text) + + +def reload_smartdns(): + subprocess.run(["/etc/init.d/smartdns", "reload"]) + + +def main(): + write_smartdns_domain_set() + reload_smartdns() if __name__ == '__main__': diff --git a/update.sh b/update.sh deleted file mode 100755 index 57de546..0000000 --- a/update.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -cd /srv/app/gfw/ - -yq "." pac/config/custom.yaml > gfwlist-to-dnsmasq-rule/custom_proxy_hosts.json - -rsync --verbose gfwlist-to-dnsmasq-rule/{main.py,custom_proxy_hosts.json} root-openwrt.rel.bgme.org:/root/gfwlist-to-dnsmasq-rule -ssh root-openwrt.rel.bgme.org "/usr/bin/python /root/gfwlist-to-dnsmasq-rule/main.py" diff --git a/utils/dnsstamps.py b/utils/dnsstamps.py new file mode 100644 index 0000000..12311e8 --- /dev/null +++ b/utils/dnsstamps.py @@ -0,0 +1,537 @@ +import base64 + + +def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + if b[0] == 0x01: + return DNSCrypt.parse(stamp) + elif b[0] == 0x02: + return DNSoverHTTPS.parse(stamp) + elif b[0] == 0x03: + return DNSoverTLS.parse(stamp) + elif b[0] == 0x04: + return DNSoverQUIC.parse(stamp) + elif b[0] == 0x05: + return ObliviousDoH.parse(stamp) + elif b[0] == 0x81: + return DNSCryptRelay.parse(stamp) + elif b[0] == 0x85: + return ObliviousDoHrelay.parse(stamp) + elif b[0] == 0x00: + return PlainDNS.parse(stamp) + + +# Code from https://github.com/DNSCrypt/dnscrypt-resolvers/blob/21fcbaf858112c63fed2a504714cc829bd654483/utils/format.py#L101-L141 +class DNSCrypt: + dnssec: bool = False + nolog: bool = False + nofilter: bool = False + addr: str = None + pk: str = None + provider: str = None + + @staticmethod + def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x01 + i = 0 + if b[i] != 0x01: + raise ValueError() + i = i + 1 + + parsed = DNSCrypt() + + # props + props = b[i] + parsed.dnssec = not not ((props >> 0) & 1) + parsed.nolog = not not ((props >> 1) & 1) + parsed.nofilter = not not ((props >> 2) & 1) + i = i + 8 + + # LP(addr [:port]) + addr_len = b[i] + i = i + 1 + parsed.addr = b[i:i + addr_len].decode("utf-8") + i = i + addr_len + + # LP(pk) + pk_len = b[i] + i = i + 1 + if pk_len != 32: + raise ValueError() + hpk = b[i:i + pk_len].hex().upper() + hpks = [] + for j in range(0, 16): + hpks.append(hpk[j * 4: j * 4 + 4]) + parsed.pk = ":".join(hpks) + i = i + pk_len + + # LP(providerName) + provider_len = b[i] + i = i + 1 + parsed.provider = b[i:i + provider_len].decode("utf-8") + i = i + provider_len + + return parsed + + +class DNSoverHTTPS: + dnssec: bool = False + nolog: bool = False + nofilter: bool = False + addr: str = None + hashs: list[str] = [] + hostname: str = None + path: str = None + bootstrap_ipi: list[str] = [] + + def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x02 + i = 0 + if b[i] != 0x02: + raise ValueError() + i = i + 1 + + parsed = DNSoverHTTPS() + + # props + props = b[i] + parsed.dnssec = not not ((props >> 0) & 1) + parsed.nolog = not not ((props >> 1) & 1) + parsed.nofilter = not not ((props >> 2) & 1) + i = i + 8 + + # LP(addr) + addr_len = b[i] + i = i + 1 + parsed.addr = b[i:i + addr_len].decode("utf-8") + i = i + addr_len + + # VLP(hash1, hash2, ...hashn) + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + hashx_len = b[i] + else: + hashx_len = b[i] ^ 0x80 + if hashx_len != 0 and hashx_len != 32: + raise ValueError() + i = i + 1 + if hashx_len > 0: + hashx = b[i:i + hashx_len].hex() + parsed.hashs.append(hashx) + i = i + hashx_len + if last_element: + break + + # LP(hostname [:port]) + hostname_len = b[i] + i = i + 1 + parsed.hostname = b[i:i + hostname_len].decode("utf-8") + i = i + hostname_len + + # LP(path) + path_len = b[i] + i = i + 1 + parsed.path = b[i:i + path_len].decode("utf-8") + i = i + path_len + + # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional) + if i < len(b): + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + bootstrap_ipx_len = b[i] + else: + bootstrap_ipx_len = b[i] ^ 0x80 + i = i + 1 + if bootstrap_ipx_len > 0: + bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8") + parsed.bootstrap_ipi.append(bootstrap_ipx) + i = i + bootstrap_ipx_len + if last_element: + break + + return parsed + + +class DNSoverTLS: + dnssec: bool = False + nolog: bool = False + nofilter: bool = False + addr: str = None + hashs: list[str] = [] + hostname: str = None + bootstrap_ipi: list[str] = [] + + @staticmethod + def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x03 + i = 0 + if b[i] != 0x03: + raise ValueError() + i = i + 1 + + parsed = DNSoverTLS() + + # props + props = b[i] + parsed.dnssec = not not ((props >> 0) & 1) + parsed.nolog = not not ((props >> 1) & 1) + parsed.nofilter = not not ((props >> 2) & 1) + i = i + 8 + + # LP(addr) + addr_len = b[i] + i = i + 1 + parsed.addr = b[i:i + addr_len].decode("utf-8") + i = i + addr_len + + # VLP(hash1, hash2, ...hashn) + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + hashx_len = b[i] + else: + hashx_len = b[i] ^ 0x80 + if hashx_len != 0 and hashx_len != 32: + raise ValueError() + i = i + 1 + if hashx_len > 0: + hashx = b[i:i + hashx_len].hex() + parsed.hashs.append(hashx) + i = i + hashx_len + if last_element: + break + + # LP(hostname[:port]) + hostname_len = b[i] + i = i + 1 + parsed.hostname = b[i:i + hostname_len].decode("utf-8") + i = i + hostname_len + + # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional) + if i < len(b): + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + bootstrap_ipx_len = b[i] + else: + bootstrap_ipx_len = b[i] ^ 0x80 + i = i + 1 + if bootstrap_ipx_len > 0: + bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8") + parsed.bootstrap_ipi.append(bootstrap_ipx) + i = i + bootstrap_ipx_len + if last_element: + break + + return parsed + + +class DNSoverQUIC: + dnssec: bool = False + nolog: bool = False + nofilter: bool = False + addr: str = None + hashs: list[str] = [] + hostname: str = None + bootstrap_ipi: list[str] = [] + + @staticmethod + def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x04 + i = 0 + if b[i] != 0x04: + raise ValueError() + i = i + 1 + + parsed = DNSoverQUIC() + + # props + props = b[i] + parsed.dnssec = not not ((props >> 0) & 1) + parsed.nolog = not not ((props >> 1) & 1) + parsed.nofilter = not not ((props >> 2) & 1) + i = i + 8 + + # LP(addr) + addr_len = b[i] + i = i + 1 + parsed.addr = b[i:i + addr_len].decode("utf-8") + i = i + addr_len + + # VLP(hash1, hash2, ...hashn) + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + hashx_len = b[i] + else: + hashx_len = b[i] ^ 0x80 + if hashx_len != 0 and hashx_len != 32: + raise ValueError() + i = i + 1 + if hashx_len > 0: + hashx = b[i:i + hashx_len].hex() + parsed.hashs.append(hashx) + i = i + hashx_len + if last_element: + break + + # LP(hostname[:port]) + hostname_len = b[i] + i = i + 1 + parsed.hostname = b[i:i + hostname_len].decode("utf-8") + i = i + hostname_len + + # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional) + if i < len(b): + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + bootstrap_ipx_len = b[i] + else: + bootstrap_ipx_len = b[i] ^ 0x80 + i = i + 1 + if bootstrap_ipx_len > 0: + bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8") + parsed.bootstrap_ipi.append(bootstrap_ipx) + i = i + bootstrap_ipx_len + if last_element: + break + + return parsed + + +class ObliviousDoH: + dnssec: bool = False + nolog: bool = False + nofilter: bool = False + hostname: str = None + path: str = None + + @staticmethod + def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x05 + i = 0 + if b[i] != 0x05: + raise ValueError() + i = i + 1 + + parsed = ObliviousDoH() + + # props + props = b[i] + parsed.dnssec = not not ((props >> 0) & 1) + parsed.nolog = not not ((props >> 1) & 1) + parsed.nofilter = not not ((props >> 2) & 1) + i = i + 8 + + # LP(hostname [:port]) + hostname_len = b[i] + i = i + 1 + parsed.hostname = b[i:i + hostname_len].decode("utf-8") + i = i + hostname_len + + # LP(path) + path_len = b[i] + i = i + 1 + parsed.path = b[i:i + path_len].decode("utf-8") + i = i + path_len + + return parsed + + +class DNSCryptRelay: + addr: str = None + + @staticmethod + def parse(stamp): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x81 + i = 0 + if b[i] != 0x81: + raise ValueError() + i = i + 1 + + parsed = DNSCryptRelay() + + # LP(addr) + addr_len = b[i] + i = i + 1 + parsed.addr = b[i:i + addr_len].decode("utf-8") + i = i + addr_len + + return parsed + + +class ObliviousDoHrelay: + dnssec: bool = False + nolog: bool = False + nofilter: bool = False + addr: str = None + hashs: list[str] = [] + hostname: str = None + path: str = None + bootstrap_ipi: list[str] = [] + + @staticmethod + def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x85 + i = 0 + if b[i] != 0x85: + raise ValueError() + i = i + 1 + + parsed = ObliviousDoHrelay() + + # props + props = b[i] + parsed.dnssec = not not ((props >> 0) & 1) + parsed.nolog = not not ((props >> 1) & 1) + parsed.nofilter = not not ((props >> 2) & 1) + i = i + 8 + + # LP(addr) + addr_len = b[i] + i = i + 1 + parsed.addr = b[i:i + addr_len].decode("utf-8") + i = i + addr_len + + # VLP(hash1, hash2, ...hashn) + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + hashx_len = b[i] + else: + hashx_len = b[i] ^ 0x80 + if hashx_len != 0 and hashx_len != 32: + raise ValueError() + i = i + 1 + if hashx_len > 0: + hashx = b[i:i + hashx_len].hex() + parsed.hashs.append(hashx) + i = i + hashx_len + if last_element: + break + + # LP(hostname [:port]) + hostname_len = b[i] + i = i + 1 + parsed.hostname = b[i:i + hostname_len].decode("utf-8") + i = i + hostname_len + + # LP(path) + path_len = b[i] + i = i + 1 + parsed.path = b[i:i + path_len].decode("utf-8") + i = i + path_len + + # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional) + if i < len(b): + last_element = False + while True: + if b[i] & 0x80 == 0: + last_element = True + bootstrap_ipx_len = b[i] + else: + bootstrap_ipx_len = b[i] ^ 0x80 + i = i + 1 + if bootstrap_ipx_len > 0: + bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8") + parsed.bootstrap_ipi.append(bootstrap_ipx) + i = i + bootstrap_ipx_len + if last_element: + break + + return parsed + + +class PlainDNS: + dnssec: bool = False + nolog: bool = False + nofilter: bool = False + addr: str = None + + @staticmethod + def parse(stamp: str): + b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==") + + # 0x00 + i = 0 + if b[i] != 0x00: + raise ValueError() + i = i + 1 + + parsed = PlainDNS() + + # props + props = b[i] + parsed.dnssec = not not ((props >> 0) & 1) + parsed.nolog = not not ((props >> 1) & 1) + parsed.nofilter = not not ((props >> 2) & 1) + i = i + 8 + + # LP(addr) + addr_len = b[i] + i = i + 1 + parsed.addr = b[i:i + addr_len].decode("utf-8") + i = i + addr_len + + return parsed + + +if __name__ == '__main__': + # DNSCrypt + t = parse( + "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ") + print(t) + + # DoH + t = parse("sdns://AgcAAAAAAAAADTIxNy4xNjkuMjAuMjIADWRucy5hYS5uZXQudWsKL2Rucy1xdWVyeQ") + t = parse( + "sdns://AgMAAAAAAAAADjE2My40Ny4xMTcuMTc2oMwQYNOcgym2K2-8fQ1t-TCYabmB5-Y5LVzY-kCPTYDmoPf1ryiAHod9ffOivij-FJ8ydKftKfE2_VA845jLqAsNoLNeBZUM-9gln5N1uhAYcLjDxMDsWlKXV-YxZ-neJqnooEROvWe7g_iAezkh6TiskXi4gr1QqtsRIx8ETPXwjffOoOZEumlj4zX-dly5l2sSsQ61QpS0JHd2TMs6OsyjrLL8ICquP7e_BeTIHEGU3KRFEdT5rzBHhuwa5yGECc9ioINVEGFkbC5hZGZpbHRlci5uZXQKL2Rucy1xdWVyeQ") + print(t) + + # DoT + t = parse("sdns://AwcAAAAAAAAABzEuMS4xLjEAD29uZS5vbmUub25lLm9uZQ") + print(t) + + # DoQ + t = parse("sdns://BAcAAAAAAAAABzEuMS4xLjEAD29uZS5vbmUub25lLm9uZQ") + print(t) + + # oDoH + t = parse("sdns://BQcAAAAAAAAADWpwLnRpYXJhcC5vcmcFL29kb2g") + print(t) + + # DNSCrypt relay + t = parse("sdns://gQ04Ni4xMDYuNzQuMjE5") + print(t) + + # oDoH relay + t = parse("sdns://hQcAAAAAAAAADDg5LjM4LjEzMS4zOAAYb2RvaC1ubC5hbGVrYmVyZy5uZXQ6NDQzBi9wcm94eQ") + print(t) + + # Plain DNS + t = parse("sdns://AAUAAAAAAAAABzEuMS4xLjE") + print(t)