This repository has been archived by the owner on Jan 25, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
doh.py
149 lines (126 loc) · 5.26 KB
/
doh.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""
Used to send queries over DoH
Has support for both GET and POST as specified by RFC
Test method for using non-standard json methods
"""
__author__ = "Jacob Davis as part of research at imaal.byu.edu"
import requests
import base64
import dns.message
import dns.rdatatype
import argparse
import multiprocessing as mp
from tqdm import tqdm
import json
def create_query(url, record_type="A", b64=False):
"""
Creates a DNS query in wire format. Can be encoded in base64 for use in GET method
:param url: the url to create a query for e.g. example.com
:param record_type: the desired record type in string format e.g. AAAA
:param b64: If true will base64url encode the query
:return: the dns message in wire format or a b64 string
"""
message = dns.message.make_query(url, dns.rdatatype.from_text(record_type)).to_wire()
if not b64:
return message
else:
return base64.urlsafe_b64encode(message).decode('utf-8').strip("=")
def decode_b64_answer(data):
"""
Decodes a base64 response into wire format
:param data: the base64 response
:return: a dns wire message
"""
message = dns.message.from_wire(data)
return message
def get_wire(resolver_url, query_name):
"""
Official RFC method. Send a get request to resolver/dns-query with param dns={base64 encoded dns wire query}
:param resolver_url: The resolver to query e.g. 1.1.1.1
:param query_name: The query url e.g. example.com
:return: a dns.message object received from the resolver
"""
headers = {"accept": "application/dns-message"}
payload = {"dns": create_query(query_name, b64=True)}
url = "https://{}/dns-query".format( resolver_url)
try:
res = requests.get(url, params=payload, headers=headers, stream=True, timeout=10)
return [a.to_text() for a in decode_b64_answer(res.content).answer]
except Exception as e:
return None
def post_wire(resolver_url, query_name):
"""
Official RFC method. Send a post request with the body being a raw dns query in wire format
:param resolver_url: The resolver to query e.g. 1.1.1.1
:param query_name: The query url e.g. example.com
:return: a dns.message object received from the resolver
"""
query = create_query(query_name)
headers = {"accept": "application/dns-message", "content-type": "application/dns-message",
"content-length": str(len(query))}
url = "https://{}/dns-query".format(resolver_url)
try:
res = requests.post(url, data=query, headers=headers, stream=True, timeout=10)
return [a.to_text() for a in decode_b64_answer(res.content).answer]
except Exception as e:
return None
def get_json(resolver_url, query_name):
"""
Not in RFC, but appears to be a common method. Send get with a param name={url}. Response in json
:param resolver_url: The resolver to query e.g. 1.1.1.1
:param query_name: The query url e.g. example.com
:return: a json response from the resolver
"""
headers = {"accept": "application/dns-json"}
payload = {"name": query_name}
if resolver_url in ["8.8.8.8", "8.8.4.4", "dns.google.com"]: # Google requires dns.google.com and /resolve
print("Google is special")
url = "https://dns.google.com/resolve"
else:
url = "https://{}/dns-query".format(resolver_url)
try:
res = requests.get(url, params=payload, headers=headers, stream=True, timeout=10)
return res.json()
except Exception as e:
return None
def test_resolver(resolver):
"""
Tests the given resolver for both GET and POST
:param resolver: the resolver to query e.g. 8.8.8.8
:return: a dictionary of {resolver, got response from either, post response, get response}
"""
resolver = resolver.strip('\n')
query = "example.com"
data = {"resolver": resolver, "got_res": False, "post_res": "", "get_res": ""}
post = post_wire(resolver, query)
get = get_wire(resolver, query)
if post is not None:
data["post_res"] = post
data["got_res"] = True
if get is not None:
data["get_res"] = get
data["got_res"] = True
return data
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Running a series of doh tests")
parser.add_argument('input', help="Input file containing a list of IPs")
parser.add_argument('output', help="Output file to write results to")
parser.add_argument('-n', '--num-threads', help="Number of threads to execute queries", default=64, type=int)
args = parser.parse_args()
in_file = open(args.input)
targets = in_file.readlines()
in_file.close()
if "." not in targets[0]:
targets = targets[1:]
threads = min(args.num_threads, len(targets))
results = []
print("Beginning the {} queries using {} threads.".format(len(targets), threads))
with open(args.output, 'w') as output_file:
with mp.Pool(processes=threads) as p:
try:
for result in tqdm(p.imap_unordered(test_resolver, targets), total=len(targets)):
output_file.write(json.dumps(result) + '\n')
except KeyboardInterrupt:
p.terminate()
p.join()
print("Exiting early from queries. Current results will still be written")