├── .editorconfig
├── .gitignore
├── .idea
├── DoHVerifier.iml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .travis.yml
├── .vscode
├── launch.json
└── settings.json
├── README.md
├── doh_verifier.py
├── libs
├── __init__.py
└── log
│ └── __init__.py
├── logging.conf
└── requirements.txt
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | end_of_line = lf
11 | charset = utf-8
12 |
13 | # Docstrings and comments use max_line_length = 79
14 | [*.py]
15 | max_line_length = 119
16 |
17 | # Use 2 spaces for the HTML files
18 | [*.html]
19 | indent_size = 2
20 |
21 | # The JSON files contain newlines inconsistently
22 | [*.json]
23 | indent_size = 2
24 | insert_final_newline = ignore
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | *~
3 | .fuse_hidden*
4 | .directory
5 | .Trash-*
6 | .nfs*
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 | *.so
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | pip-wheel-metadata/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 | *.manifest
31 | *.spec
32 | pip-log.txt
33 | pip-delete-this-directory.txt
34 | htmlcov/
35 | .tox/
36 | .nox/
37 | .coverage
38 | .coverage.*
39 | .cache
40 | nosetests.xml
41 | coverage.xml
42 | *.cover
43 | .hypothesis/
44 | .pytest_cache/
45 | *.mo
46 | *.pot
47 | *.log
48 | local_settings.py
49 | db.sqlite3
50 | instance/
51 | .webassets-cache
52 | .scrapy
53 | docs/_build/
54 | target/
55 | .ipynb_checkpoints
56 | profile_default/
57 | ipython_config.py
58 | .python-version
59 | celerybeat-schedule
60 | *.sage.py
61 | .env
62 | .venv
63 | env/
64 | venv/
65 | ENV/
66 | env.bak/
67 | venv.bak/
68 | .spyderproject
69 | .spyproject
70 | .ropeproject
71 | /site
72 | .mypy_cache/
73 | .dmypy.json
74 | dmypy.json
75 | .pyre/
76 | .idea/**/workspace.xml
77 | .idea/**/tasks.xml
78 | .idea/**/usage.statistics.xml
79 | .idea/**/dictionaries
80 | .idea/**/shelf
81 | .idea/**/contentModel.xml
82 | .idea/**/dataSources/
83 | .idea/**/dataSources.ids
84 | .idea/**/dataSources.local.xml
85 | .idea/**/sqlDataSources.xml
86 | .idea/**/dynamic.xml
87 | .idea/**/uiDesigner.xml
88 | .idea/**/dbnavigator.xml
89 | .idea/**/gradle.xml
90 | .idea/**/libraries
91 | cmake-build-*/
92 | .idea/**/mongoSettings.xml
93 | *.iws
94 | out/
95 | .idea_modules/
96 | atlassian-ide-plugin.xml
97 | .idea/replstate.xml
98 | com_crashlytics_export_strings.xml
99 | crashlytics.properties
100 | crashlytics-build.properties
101 | fabric.properties
102 | .idea/httpRequests
103 | .idea/caches/build_file_checksums.ser
104 | .DS_Store
105 | .AppleDouble
106 | .LSOverride
107 | Icon
108 | ._*
109 | .DocumentRevisions-V100
110 | .fseventsd
111 | .Spotlight-V100
112 | .TemporaryItems
113 | .Trashes
114 | .VolumeIcon.icns
115 | .com.apple.timemachine.donotpresent
116 | .AppleDB
117 | .AppleDesktop
118 | Network Trash Folder
119 | Temporary Items
120 | .apdisk
121 | Thumbs.db
122 | Thumbs.db:encryptable
123 | ehthumbs.db
124 | ehthumbs_vista.db
125 | *.stackdump
126 | [Dd]esktop.ini
127 | $RECYCLE.BIN/
128 | *.cab
129 | *.msi
130 | *.msix
131 | *.msm
132 | *.msp
133 | *.lnk
134 |
135 | GeoLite2-Country.mmdb
136 | public-resolvers.md
137 |
138 | # https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
139 | .vscode/*
140 | !.vscode/settings.json
141 | !.vscode/tasks.json
142 | !.vscode/launch.json
143 | !.vscode/extensions.json
144 |
145 |
146 | COPYRIGHT.txt
147 | LICENSE.txt
148 |
--------------------------------------------------------------------------------
/.idea/DoHVerifier.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial # required for Python >= 3.7
2 | language: python
3 | python:
4 | - "3.7"
5 | install:
6 | - pip install -r requirements.txt
7 | - curl https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md -o public-resolvers.md
8 | - curl https://raw.githubusercontent.com/akeeba/geoip-plugin/8bf26d5abae7024622e6fca21648f2af1fb8876f/plugins/system/akgeoip/db/GeoLite2-Country.mmdb -o GeoLite2-Country.mmdb
9 | script:
10 | - python3 doh_verifier.py
11 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [{
7 | "name": "Python: Current File",
8 | "type": "python",
9 | "request": "launch",
10 | "program": "${file}",
11 | "console": "integratedTerminal"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "venv/bin/python"
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DoHVerifier
2 |
3 | [](https://travis-ci.org/TheCjw/DoHVerifier)
4 |
5 | Find best DoH resolver and test if ECS(edns client subnet) supported.
--------------------------------------------------------------------------------
/doh_verifier.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import concurrent.futures
5 | import base64
6 | import io
7 | from struct import unpack
8 | import re
9 |
10 | import requests
11 | from requests import ConnectTimeout
12 | from requests import ReadTimeout
13 | from tabulate import tabulate
14 | import maxminddb
15 |
16 | from libs.log import logger
17 |
18 | reader = maxminddb.open_database("GeoLite2-Country.mmdb")
19 |
20 |
21 | def parse_resolvers(content):
22 | result = re.findall(r"^##.+?(?P.+$)(?P(\n|.)+?)(?P^sdns.+)",
23 | content, re.M)
24 | if result is None:
25 | return None
26 |
27 | resolvers = []
28 | for r in result:
29 | # Skip sdns://
30 | stamp = r[3][7:]
31 |
32 | # FIX Padding.
33 | stamp += "=" * ((4 - len(stamp) % 4) % 4)
34 | decoded_stamp = base64.urlsafe_b64decode(stamp)
35 |
36 | stream = io.BytesIO(decoded_stamp)
37 | # https://github.com/jedisct1/dnscrypt-proxy/wiki/stamps
38 |
39 | flag = unpack("B", stream.read(1))[0]
40 |
41 | # Parse DNS-over-HTTPS only.
42 | if flag != 0x02:
43 | continue
44 |
45 | resolver = {}
46 |
47 | resolver["name"] = r[0]
48 | resolver["ip_address"] = ""
49 |
50 | props = unpack("Q", stream.read(8))[0]
51 |
52 | _len = unpack("B", stream.read(1))[0]
53 | if _len != 0:
54 | # can be empty.
55 | ip_address = stream.read(_len)
56 | resolver["ip_address"] = ip_address.decode()
57 |
58 | # https://github.com/jedisct1/dnscrypt-proxy/blob/master/vendor/github.com/jedisct1/go-dnsstamps/dnsstamps.go#L159
59 | while True:
60 | vlen = unpack("B", stream.read(1))[0]
61 | _len = vlen & (~0x80)
62 | if _len > 0:
63 | hashes = stream.read(_len)
64 |
65 | if (vlen & 0x80) != 0x80:
66 | break
67 |
68 | _len = unpack("B", stream.read(1))[0]
69 | host = None
70 | if _len != 0:
71 | host = stream.read(_len)
72 |
73 | _len = unpack("B", stream.read(1))[0]
74 | path = None
75 | if _len != 0:
76 | path = stream.read(_len)
77 |
78 | resolver["url"] = f"https://{host.decode()}{path.decode()}"
79 | resolvers.append(resolver)
80 |
81 | return resolvers
82 |
83 |
84 | def test_resolver(resolver):
85 | logger.debug(f"Querying {resolver['name']}")
86 | try:
87 | params = {
88 | "name": "dl.google.com"
89 | }
90 | r = requests.get(resolver["url"], params=params, timeout=2)
91 | resolver["latency(ms)"] = int(r.elapsed.total_seconds() * 1000)
92 |
93 | for answer in r.json()["Answer"]:
94 | if answer["type"] == 1:
95 | ip = answer["data"]
96 | country = reader.get(ip)
97 | resolver["google"] = f"{ip}({country['country']['iso_code']})"
98 | break
99 | except (ConnectTimeout, ReadTimeout):
100 | resolver["latency(ms)"] = "timeout"
101 | return resolver
102 |
103 |
104 | def main():
105 | content = open("public-resolvers.md", encoding="utf-8").read()
106 | resolvers = parse_resolvers(content)
107 |
108 | ipv4_resolvers = []
109 | for resolver in resolvers:
110 | ip_address = resolver["ip_address"]
111 | if len(ip_address):
112 | # Ignore ipv6
113 | if ip_address[0] == "[":
114 | continue
115 | ipv4_resolvers.append(resolver)
116 |
117 | result = []
118 | with concurrent.futures.ThreadPoolExecutor() as executor:
119 | future_list = {executor.submit(
120 | test_resolver, resolver): resolver for resolver in ipv4_resolvers}
121 | for future in concurrent.futures.as_completed(future_list):
122 | try:
123 | if future.result():
124 | result.append(future.result())
125 | except:
126 | pass
127 |
128 | print(tabulate(result, headers="keys", tablefmt="github"))
129 |
130 |
131 | if __name__ == "__main__":
132 | main()
133 |
--------------------------------------------------------------------------------
/libs/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
--------------------------------------------------------------------------------
/libs/log/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import logging.config
5 |
6 | logging.config.fileConfig("logging.conf", disable_existing_loggers=True)
7 |
8 | logger = logging.getLogger("DoHVerifier")
9 |
--------------------------------------------------------------------------------
/logging.conf:
--------------------------------------------------------------------------------
1 | [loggers]
2 | keys=root,DoHVerifier
3 |
4 | [handlers]
5 | keys=consoleHandler
6 |
7 | [formatters]
8 | keys=colorlog
9 |
10 | [logger_root]
11 | level=ERROR
12 | handlers=consoleHandler
13 |
14 | [logger_DoHVerifier]
15 | level = DEBUG
16 | handlers = consoleHandler
17 | qualname = DoHVerifier
18 | propagate = 0
19 |
20 | [handler_consoleHandler]
21 | class = StreamHandler
22 | level = DEBUG
23 | formatter = colorlog
24 | args = (sys.stdout,)
25 |
26 | [formatter_colorlog]
27 | class = colorlog.ColoredFormatter
28 | format = %(log_color)s%(asctime)s %(levelname)s%(reset)s %(message)s
29 | datefmt = %Y-%m-%d %H:%M:%S
30 | reset = true
31 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | colorlog
3 | maxminddb
4 | tabulate
--------------------------------------------------------------------------------