dpyproxy 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dpyproxy/__init__.py +1 -0
- dpyproxy/__main__.py +4 -0
- dpyproxy-2.2.0.dist-info/METADATA +296 -0
- dpyproxy-2.2.0.dist-info/RECORD +59 -0
- dpyproxy-2.2.0.dist-info/WHEEL +4 -0
- dpyproxy-2.2.0.dist-info/entry_points.txt +2 -0
- dpyproxy-2.2.0.dist-info/licenses/LICENSE +201 -0
- enumerators/DnsProxyMode.py +39 -0
- enumerators/DnsResolvers.py +141 -0
- enumerators/HttpMethod.py +17 -0
- enumerators/Modules.py +38 -0
- enumerators/Port.py +11 -0
- enumerators/TcpProxyMode.py +17 -0
- enumerators/TlsVersion.py +21 -0
- enumerators/__init__.py +0 -0
- exception/DnsException.py +7 -0
- exception/ParserException.py +7 -0
- exception/__init__.py +0 -0
- main.py +94 -0
- modules/Module.py +45 -0
- modules/__init__.py +0 -0
- modules/dns/DnsModeDeterminator.py +358 -0
- modules/dns/DnsModule.py +113 -0
- modules/dns/DnsProxy.py +277 -0
- modules/dns/DnsResolver.py +18 -0
- modules/dns/__init__.py +0 -0
- modules/http/HttpModule.py +69 -0
- modules/http/HttpStrategies.py +849 -0
- modules/http/HttpUtils.py +94 -0
- modules/http/__init__.py +0 -0
- modules/tls/TcpProxy.py +106 -0
- modules/tls/TlsModule.py +173 -0
- modules/tls/__init__.py +0 -0
- network/DomainResolver.py +472 -0
- network/NetworkAddress.py +10 -0
- network/WrappedSocket.py +97 -0
- network/__init__.py +0 -0
- network/protocols/Dns.py +62 -0
- network/protocols/Http.py +109 -0
- network/protocols/Socksv4.py +70 -0
- network/protocols/Socksv5.py +106 -0
- network/protocols/Tls.py +113 -0
- network/protocols/__init__.py +0 -0
- network/tcp/Forwarder.py +203 -0
- network/tcp/TcpConnectionHandler.py +264 -0
- network/tcp/WrappedTcpSocket.py +30 -0
- network/tcp/__init__.py +0 -0
- network/udp/__init__.py +0 -0
- test/Sink.py +23 -0
- test/__init__.py +0 -0
- test/test_dns.py +98 -0
- test/test_http.py +57 -0
- test/test_tls.py +63 -0
- util/DnsAutoModeRuntimeMeasurement.py +62 -0
- util/DnsReachabilityCollector.py +160 -0
- util/DnsResolversDomainResolver.py +36 -0
- util/Util.py +62 -0
- util/__init__.py +0 -0
- util/constants.py +8 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import socket
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
|
|
6
|
+
import dns
|
|
7
|
+
import httpx
|
|
8
|
+
from dns.exception import Timeout
|
|
9
|
+
from dns.message import Message
|
|
10
|
+
from dns.query import (
|
|
11
|
+
BadResponse,
|
|
12
|
+
_check_status,
|
|
13
|
+
_compute_times,
|
|
14
|
+
_destination_and_source,
|
|
15
|
+
_HTTPTransport,
|
|
16
|
+
_remaining,
|
|
17
|
+
quic,
|
|
18
|
+
receive_udp,
|
|
19
|
+
send_udp,
|
|
20
|
+
tcp,
|
|
21
|
+
tls,
|
|
22
|
+
udp,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from enumerators.DnsProxyMode import DnsProxyMode
|
|
26
|
+
from exception.DnsException import DnsException
|
|
27
|
+
from network.NetworkAddress import NetworkAddress
|
|
28
|
+
from network.tcp.WrappedTcpSocket import WrappedTcpSocket
|
|
29
|
+
from util.Util import parse_all_ips
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fix_transaction_id(f):
|
|
33
|
+
"""
|
|
34
|
+
Fixes the transaction id for upgraded DNS requests. DoQ specifies and DoH recommends setting the transaction id to 0
|
|
35
|
+
https://www.rfc-editor.org/rfc/rfc9250.html#section-4.2.1
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def _inner(message, *args, **kwargs):
|
|
39
|
+
_id = message.id
|
|
40
|
+
# dnspython replaces message.id with 0 in this call
|
|
41
|
+
answer = f(message, *args, **kwargs)
|
|
42
|
+
answer.id = _id
|
|
43
|
+
return answer
|
|
44
|
+
|
|
45
|
+
return _inner
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DomainResolver:
|
|
49
|
+
"""
|
|
50
|
+
Resolves domains to ip addresses. Can use DNS over TLS, DNS over DoQ, DNS over UDP, DNS over TCP, DNS over TCP with
|
|
51
|
+
TCP fragmentation, and a China-specific mode that circumvents.
|
|
52
|
+
Offers static methods and non-static methods for specifiable and non-specifiable DNS resolvers and timeouts
|
|
53
|
+
respectively.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
DNS_TCP_FRAG_SIZE = 20
|
|
57
|
+
THRESHOLD_CONFIRM_WORKING = 3
|
|
58
|
+
TRIES_CONFIRM_WORKING = 5
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
dns_mode: DnsProxyMode,
|
|
63
|
+
resolver: NetworkAddress,
|
|
64
|
+
timeout: int,
|
|
65
|
+
hostname: str,
|
|
66
|
+
tcp_frag_size: int = DNS_TCP_FRAG_SIZE,
|
|
67
|
+
add_sni: bool = True,
|
|
68
|
+
path: str = "/dns-query",
|
|
69
|
+
):
|
|
70
|
+
self.dns_mode = dns_mode
|
|
71
|
+
self.resolver = resolver
|
|
72
|
+
self.timeout = timeout
|
|
73
|
+
self.hostname = hostname
|
|
74
|
+
self.tcp_frag_size = tcp_frag_size
|
|
75
|
+
self.add_sni = add_sni
|
|
76
|
+
self.path = path
|
|
77
|
+
|
|
78
|
+
def to_dict(self):
|
|
79
|
+
return {
|
|
80
|
+
"dns_mode": self.dns_mode.value,
|
|
81
|
+
"resolver": asdict(self.resolver),
|
|
82
|
+
"timeout": self.timeout,
|
|
83
|
+
"hostname": self.hostname,
|
|
84
|
+
"tcp_frag_size": self.tcp_frag_size,
|
|
85
|
+
"add_sni": self.add_sni,
|
|
86
|
+
"path": self.path,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def from_dict(data):
|
|
91
|
+
return DomainResolver(
|
|
92
|
+
dns_mode=DnsProxyMode(data["dns_mode"]),
|
|
93
|
+
resolver=NetworkAddress(**data["resolver"]),
|
|
94
|
+
timeout=data["timeout"],
|
|
95
|
+
hostname=data["hostname"],
|
|
96
|
+
tcp_frag_size=data["tcp_frag_size"],
|
|
97
|
+
add_sni=data["add_sni"],
|
|
98
|
+
path=data["path"],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def resolve_local(domain: str) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Resolves the given domain to an ip address using the system's DNS resolver.
|
|
105
|
+
"""
|
|
106
|
+
return socket.gethostbyname(domain)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
@fix_transaction_id
|
|
110
|
+
def resolve_dot_static(
|
|
111
|
+
message: Message, resolver: NetworkAddress, timeout: int, hostname: str, add_sni: bool = True
|
|
112
|
+
) -> Message:
|
|
113
|
+
"""
|
|
114
|
+
Resolves the given domain to an ip address using DNS over TLS on the given DNS resolver.
|
|
115
|
+
:param message: the DNS message to resolve
|
|
116
|
+
:param resolver: the DNS resolver to use
|
|
117
|
+
:param timeout: the DNS request timeout
|
|
118
|
+
:param hostname: the hostname of the DoT server, used in SNI
|
|
119
|
+
:param add_sni: whether to add SNI
|
|
120
|
+
:return: The Dns response by the resolver
|
|
121
|
+
"""
|
|
122
|
+
if add_sni:
|
|
123
|
+
return tls(
|
|
124
|
+
message, where=resolver.host, port=resolver.port, timeout=timeout, server_hostname=hostname, verify=True
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
return tls(message, where=resolver.host, port=resolver.port, timeout=timeout, verify=False)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
@fix_transaction_id
|
|
131
|
+
def resolve_doh_static(
|
|
132
|
+
message: Message,
|
|
133
|
+
resolver: NetworkAddress,
|
|
134
|
+
timeout: int,
|
|
135
|
+
hostname: str,
|
|
136
|
+
add_sni: bool = True,
|
|
137
|
+
path: str = "/dns-query",
|
|
138
|
+
) -> Message:
|
|
139
|
+
"""
|
|
140
|
+
Resolves the given domain to an ip address using DNS over HTTPS on the given DNS resolver.
|
|
141
|
+
:param message: the DNS message to resolve
|
|
142
|
+
:param resolver: the DNS resolver to use
|
|
143
|
+
:param timeout: the DNS request timeout
|
|
144
|
+
:param hostname: the hostname that TLS should use in SNI
|
|
145
|
+
:param add_sni: whether to add SNI
|
|
146
|
+
:param path: the path used in the HTTP URL
|
|
147
|
+
:return: The Dns response by the resolver
|
|
148
|
+
"""
|
|
149
|
+
# Reused code from dnypython's https function below
|
|
150
|
+
|
|
151
|
+
url = f"https://{resolver.host}:{resolver.port}{path}"
|
|
152
|
+
|
|
153
|
+
(_, _, the_source) = _destination_and_source(resolver.host, resolver.port, None, 0, False)
|
|
154
|
+
|
|
155
|
+
extensions = {}
|
|
156
|
+
bootstrap_address = resolver.host
|
|
157
|
+
verify = add_sni
|
|
158
|
+
if add_sni:
|
|
159
|
+
extensions["sni_hostname"] = hostname
|
|
160
|
+
q = message
|
|
161
|
+
|
|
162
|
+
wire = q.to_wire()
|
|
163
|
+
headers = {"accept": "application/dns-message"}
|
|
164
|
+
|
|
165
|
+
if the_source is None:
|
|
166
|
+
local_address = None
|
|
167
|
+
local_port = 0
|
|
168
|
+
else:
|
|
169
|
+
local_address = the_source[0]
|
|
170
|
+
local_port = the_source[1]
|
|
171
|
+
|
|
172
|
+
transport = _HTTPTransport(
|
|
173
|
+
local_address=local_address,
|
|
174
|
+
http1=False,
|
|
175
|
+
http2=True,
|
|
176
|
+
verify=verify,
|
|
177
|
+
local_port=local_port,
|
|
178
|
+
bootstrap_address=bootstrap_address,
|
|
179
|
+
resolver=resolver,
|
|
180
|
+
family=socket.AF_UNSPEC,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
cm = httpx.Client(http1=False, http2=True, verify=verify, transport=transport)
|
|
184
|
+
|
|
185
|
+
with cm as session:
|
|
186
|
+
headers.update(
|
|
187
|
+
{
|
|
188
|
+
"content-type": "application/dns-message",
|
|
189
|
+
"content-length": str(len(wire)),
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
response = session.post(
|
|
193
|
+
url,
|
|
194
|
+
headers=headers,
|
|
195
|
+
content=wire,
|
|
196
|
+
timeout=timeout,
|
|
197
|
+
extensions=extensions,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# status code exception
|
|
201
|
+
if response.status_code < 200 or response.status_code > 299:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"{resolver.host} responded with status code {response.status_code}"
|
|
204
|
+
f"\nResponse headers: {response.headers}"
|
|
205
|
+
f"\nResponse body: {response.content}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
r = dns.message.from_wire(
|
|
209
|
+
response.content,
|
|
210
|
+
keyring=q.keyring,
|
|
211
|
+
request_mac=q.request_mac,
|
|
212
|
+
one_rr_per_rrset=False,
|
|
213
|
+
ignore_trailing=False,
|
|
214
|
+
)
|
|
215
|
+
r.time = response.elapsed.total_seconds()
|
|
216
|
+
if not q.is_response(r):
|
|
217
|
+
raise BadResponse
|
|
218
|
+
return r
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
@fix_transaction_id
|
|
222
|
+
def resolve_doh3_static(
|
|
223
|
+
message: Message,
|
|
224
|
+
resolver: NetworkAddress,
|
|
225
|
+
timeout: int,
|
|
226
|
+
hostname: str,
|
|
227
|
+
add_sni: bool = True,
|
|
228
|
+
path: str = "/dns-query",
|
|
229
|
+
) -> Message:
|
|
230
|
+
"""
|
|
231
|
+
Resolves the given domain to an ip address using DNS over HTTP3 on the given DNS resolver.
|
|
232
|
+
:param message: the DNS message to resolve
|
|
233
|
+
:param resolver: the DNS resolver to use
|
|
234
|
+
:param timeout: the DNS request timeout
|
|
235
|
+
:param hostname: the hostname of the DNS resolver to use in SNI
|
|
236
|
+
:param add_sni: whether to add SNI
|
|
237
|
+
:param path: the path used in the HTTP URL
|
|
238
|
+
:return: The Dns response by the resolver
|
|
239
|
+
"""
|
|
240
|
+
# Reused code from dnypython's _http3 function below
|
|
241
|
+
|
|
242
|
+
q = message
|
|
243
|
+
where = resolver.host
|
|
244
|
+
url = f"https://{resolver.host}:{resolver.port}{path}"
|
|
245
|
+
|
|
246
|
+
q.id = 0
|
|
247
|
+
wire = q.to_wire()
|
|
248
|
+
|
|
249
|
+
if add_sni:
|
|
250
|
+
manager = dns.quic.SyncQuicManager(verify_mode=True, server_name=hostname, h3=True)
|
|
251
|
+
else:
|
|
252
|
+
manager = dns.quic.SyncQuicManager(verify_mode=False, server_name=None, h3=True)
|
|
253
|
+
|
|
254
|
+
with manager:
|
|
255
|
+
connection = manager.connect(where, resolver.port, None, 0)
|
|
256
|
+
(start, expiration) = _compute_times(timeout)
|
|
257
|
+
with connection.make_stream(timeout) as stream:
|
|
258
|
+
stream.send_h3(url, wire, True)
|
|
259
|
+
wire = stream.receive(_remaining(expiration))
|
|
260
|
+
_check_status(stream.headers(), where, wire)
|
|
261
|
+
finish = time.time()
|
|
262
|
+
|
|
263
|
+
r = dns.message.from_wire(
|
|
264
|
+
wire,
|
|
265
|
+
keyring=q.keyring,
|
|
266
|
+
request_mac=q.request_mac,
|
|
267
|
+
one_rr_per_rrset=False,
|
|
268
|
+
ignore_trailing=False,
|
|
269
|
+
)
|
|
270
|
+
r.time = max(finish - start, 0.0)
|
|
271
|
+
if not q.is_response(r):
|
|
272
|
+
raise BadResponse
|
|
273
|
+
return r
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
@fix_transaction_id
|
|
277
|
+
def resolve_doq_static(
|
|
278
|
+
message: Message, resolver: NetworkAddress, timeout: int, hostname: str, add_sni: bool = True
|
|
279
|
+
) -> Message:
|
|
280
|
+
"""
|
|
281
|
+
Resolves the given domain to an ip address using DNS over QUIC on the given DNS resolver.
|
|
282
|
+
:param message: the DNS message to resolve
|
|
283
|
+
:param resolver: the DNS resolver to use
|
|
284
|
+
:param timeout: the DNS request timeout
|
|
285
|
+
:param hostname: hostname of the DoQ server, used in SNI
|
|
286
|
+
:param add_sni: whether to add SNI
|
|
287
|
+
:return: The Dns response by the resolver
|
|
288
|
+
"""
|
|
289
|
+
if add_sni:
|
|
290
|
+
return quic(
|
|
291
|
+
message,
|
|
292
|
+
where=resolver.host,
|
|
293
|
+
port=resolver.port,
|
|
294
|
+
timeout=timeout,
|
|
295
|
+
server_hostname=hostname,
|
|
296
|
+
hostname=hostname,
|
|
297
|
+
verify=True,
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
return quic(message, where=resolver.host, port=resolver.port, timeout=timeout, verify=False)
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def resolve_udp_static_to_ip(domain: str, resolver: NetworkAddress, timeout: int) -> str:
|
|
304
|
+
"""
|
|
305
|
+
Resolves the given domain to an ip address using DNS over UDP on the given DNS resolver.
|
|
306
|
+
:param domain: the domain to resolve
|
|
307
|
+
:param resolver: the DNS resolver to use
|
|
308
|
+
:param timeout: the DNS request timeout
|
|
309
|
+
:return: The resolved ip address
|
|
310
|
+
"""
|
|
311
|
+
message = dns.message.make_query(domain, "A")
|
|
312
|
+
response = DomainResolver.resolve_udp_static(message, resolver, timeout)
|
|
313
|
+
ips = parse_all_ips(response)
|
|
314
|
+
if not ips or len(ips) == 0:
|
|
315
|
+
raise DnsException(f"No IPs found for domain {domain} using resolver {resolver}")
|
|
316
|
+
return ips[0]
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def resolve_udp_static(message: Message, resolver: NetworkAddress, timeout: int) -> Message:
|
|
320
|
+
"""
|
|
321
|
+
Resolves the given domain to an ip address using DNS over UDP on the given DNS resolver.
|
|
322
|
+
:param message: the DNS message to resolve
|
|
323
|
+
:param resolver: the DNS resolver
|
|
324
|
+
:param timeout: the DNS request timeout
|
|
325
|
+
:return: The Dns response by the resolver
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
return udp(message, where=resolver.host, port=resolver.port, timeout=timeout)
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def resolve_tcp_static(message: Message, resolver: NetworkAddress, timeout: int) -> Message:
|
|
332
|
+
"""
|
|
333
|
+
Resolves the given domain to an ip address using DNS over TCP on the given DNS resolver.
|
|
334
|
+
:param message: the DNS message to resolve
|
|
335
|
+
:param resolver: the DNS resolver
|
|
336
|
+
:param timeout: the DNS timeout
|
|
337
|
+
:return: The Dns response by the resolver
|
|
338
|
+
"""
|
|
339
|
+
# call fragmentation method without using fragmentation
|
|
340
|
+
return DomainResolver.resolve_tcp_frag_static(message, resolver, timeout, 0)
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def resolve_tcp_frag_static(message: Message, resolver: NetworkAddress, timeout: int, frag_size: int) -> Message:
|
|
344
|
+
"""
|
|
345
|
+
Resolves the given domain to an ip address using DNS over TCP with fragmentation on the given DNS resolver.
|
|
346
|
+
:param message: the DNS message to resolve
|
|
347
|
+
:param resolver: the DNS resolver to use
|
|
348
|
+
:param timeout: the DNS timeout
|
|
349
|
+
:param frag_size: size of the TCP segments used to fragment the DNS message. If 0 or negative, the message will
|
|
350
|
+
not be fragmented
|
|
351
|
+
:return: The Dns response by the resolver
|
|
352
|
+
"""
|
|
353
|
+
# create fragmenting tcp socket and pass to dnspython
|
|
354
|
+
try:
|
|
355
|
+
_socket = socket.create_connection((resolver.host, resolver.port), timeout=timeout)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
raise DnsException(e)
|
|
358
|
+
|
|
359
|
+
frag_socket = WrappedTcpSocket(timeout=timeout, _socket=_socket, tcp_frag_size=frag_size)
|
|
360
|
+
return tcp(message, where=resolver.host, sock=frag_socket, timeout=timeout)
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def resolve_last_response_static(message: Message, resolver: NetworkAddress, timeout: int) -> Message:
|
|
364
|
+
"""
|
|
365
|
+
Resolves the given domain to an ip address over UDP but waiting a certain timeout and forwarding the last answer
|
|
366
|
+
received. This circumvents China-specific censorship.
|
|
367
|
+
:param message: the DNS message to resolve
|
|
368
|
+
:param resolver: the DNS resolver to use
|
|
369
|
+
:param timeout: timeout in seconds, the last message received in this timeout is returned
|
|
370
|
+
:return: the last DNS response message received from the server in the given timeout
|
|
371
|
+
"""
|
|
372
|
+
# create udp socket
|
|
373
|
+
_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
374
|
+
_socket.setblocking(False)
|
|
375
|
+
_address = (resolver.host, resolver.port)
|
|
376
|
+
# send message to server
|
|
377
|
+
send_udp(sock=_socket, what=message, destination=_address, expiration=time.time() + timeout)
|
|
378
|
+
last_received = None
|
|
379
|
+
stop_time = time.time() + timeout
|
|
380
|
+
while True:
|
|
381
|
+
try:
|
|
382
|
+
last_received, _ = receive_udp(sock=_socket, destination=_address, expiration=stop_time)
|
|
383
|
+
except Timeout:
|
|
384
|
+
break
|
|
385
|
+
return last_received
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def resolve_static(
|
|
389
|
+
mode: DnsProxyMode,
|
|
390
|
+
message: Message,
|
|
391
|
+
resolver: NetworkAddress,
|
|
392
|
+
timeout: int,
|
|
393
|
+
frag_size: int = DNS_TCP_FRAG_SIZE,
|
|
394
|
+
hostname: str = "",
|
|
395
|
+
add_sni: bool = True,
|
|
396
|
+
path: str = "/dns-query",
|
|
397
|
+
) -> Message:
|
|
398
|
+
"""
|
|
399
|
+
Resolves the requested message based on the selected mode.
|
|
400
|
+
:param mode: the DNS proxy mode to use. Must not be AUTO, will raise a DnsException
|
|
401
|
+
:param message: the DNS message to resolve
|
|
402
|
+
:param resolver: the DNS resolver
|
|
403
|
+
:param timeout: the DNS request timeout
|
|
404
|
+
:param frag_size: size of the TCP segments used to fragment the DNS message if the mode is TCP_FRAG
|
|
405
|
+
:param hostname: the hostname of the DNS resolver
|
|
406
|
+
:param add_sni: whether to add SNI
|
|
407
|
+
:param path: the path used in the HTTP URL
|
|
408
|
+
"""
|
|
409
|
+
if mode == DnsProxyMode.AUTO:
|
|
410
|
+
raise DnsException("No resolution function for mode AUTO")
|
|
411
|
+
elif mode == DnsProxyMode.DOT:
|
|
412
|
+
return DomainResolver.resolve_dot_static(
|
|
413
|
+
message, resolver=resolver, timeout=timeout, hostname=hostname, add_sni=add_sni
|
|
414
|
+
)
|
|
415
|
+
elif mode == DnsProxyMode.DOH:
|
|
416
|
+
return DomainResolver.resolve_doh_static(
|
|
417
|
+
message, resolver=resolver, timeout=timeout, hostname=hostname, add_sni=add_sni, path=path
|
|
418
|
+
)
|
|
419
|
+
elif mode == DnsProxyMode.DOH3:
|
|
420
|
+
return DomainResolver.resolve_doh3_static(
|
|
421
|
+
message, resolver=resolver, timeout=timeout, hostname=hostname, add_sni=add_sni, path=path
|
|
422
|
+
)
|
|
423
|
+
elif mode == DnsProxyMode.DOQ:
|
|
424
|
+
return DomainResolver.resolve_doq_static(
|
|
425
|
+
message, resolver=resolver, timeout=timeout, hostname=hostname, add_sni=add_sni
|
|
426
|
+
)
|
|
427
|
+
elif mode == DnsProxyMode.UDP:
|
|
428
|
+
return DomainResolver.resolve_udp_static(message, resolver=resolver, timeout=timeout)
|
|
429
|
+
elif mode == DnsProxyMode.TCP:
|
|
430
|
+
return DomainResolver.resolve_tcp_static(message, resolver=resolver, timeout=timeout)
|
|
431
|
+
elif mode == DnsProxyMode.TCP_FRAG:
|
|
432
|
+
return DomainResolver.resolve_tcp_frag_static(
|
|
433
|
+
message, resolver=resolver, timeout=timeout, frag_size=frag_size
|
|
434
|
+
)
|
|
435
|
+
elif mode == DnsProxyMode.LAST_RESPONSE:
|
|
436
|
+
return DomainResolver.resolve_last_response_static(message, resolver=resolver, timeout=timeout)
|
|
437
|
+
else:
|
|
438
|
+
raise DnsException(f"Unknown mode {mode}")
|
|
439
|
+
|
|
440
|
+
def resolve(self, message: Message) -> Message:
|
|
441
|
+
"""
|
|
442
|
+
Resolves the given DNS message using the configured mode on the configured provider using the configured
|
|
443
|
+
timeout and frag size.
|
|
444
|
+
"""
|
|
445
|
+
return DomainResolver.resolve_static(
|
|
446
|
+
mode=self.dns_mode,
|
|
447
|
+
message=message,
|
|
448
|
+
resolver=self.resolver,
|
|
449
|
+
timeout=self.timeout,
|
|
450
|
+
frag_size=self.tcp_frag_size,
|
|
451
|
+
hostname=self.hostname,
|
|
452
|
+
add_sni=self.add_sni,
|
|
453
|
+
path=self.path,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def works(self, message: Message) -> bool:
|
|
457
|
+
"""
|
|
458
|
+
Determines if the configures resolver is consistently reachable, returns true if at least 3 out of 5 connection
|
|
459
|
+
attempts worked.
|
|
460
|
+
"""
|
|
461
|
+
working = 0
|
|
462
|
+
for _ in range(DomainResolver.TRIES_CONFIRM_WORKING):
|
|
463
|
+
try:
|
|
464
|
+
self.resolve(message=message)
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logging.debug(f"Could not resolve to {self.resolver} for mode {self.dns_mode} with exception {e}")
|
|
467
|
+
else:
|
|
468
|
+
working += 1
|
|
469
|
+
return working >= DomainResolver.THRESHOLD_CONFIRM_WORKING
|
|
470
|
+
|
|
471
|
+
def __str__(self):
|
|
472
|
+
return f"{self.dns_mode} - {self.resolver}"
|
network/WrappedSocket.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from time import time
|
|
3
|
+
|
|
4
|
+
from exception.ParserException import ParserException
|
|
5
|
+
from util.constants import STANDARD_SOCKET_RECEIVE_SIZE
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WrappedSocket:
|
|
9
|
+
"""
|
|
10
|
+
Wraps a socket with useful utility functions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, timeout: int, _socket: socket.socket):
|
|
14
|
+
self.timeout = timeout
|
|
15
|
+
self.buffer = b""
|
|
16
|
+
self.socket = _socket
|
|
17
|
+
self.socket.settimeout(timeout)
|
|
18
|
+
|
|
19
|
+
def read(self, size: int) -> bytes:
|
|
20
|
+
"""
|
|
21
|
+
Reads specified amount of data from socket. Blocks until amount of data received or timeout.
|
|
22
|
+
:param size: Data to read.
|
|
23
|
+
:return: Read data
|
|
24
|
+
"""
|
|
25
|
+
while len(self.buffer) < size:
|
|
26
|
+
self.buffer += self.socket.recv(STANDARD_SOCKET_RECEIVE_SIZE)
|
|
27
|
+
_res = self.buffer[:size]
|
|
28
|
+
self.buffer = self.buffer[size:]
|
|
29
|
+
return _res
|
|
30
|
+
|
|
31
|
+
def read_until(self, until: list[bytes], max_len: int = 100, peek: bool = False) -> bytes:
|
|
32
|
+
"""
|
|
33
|
+
Returns all bytes from the socket until and including the given bytes. Also cancels after timeout
|
|
34
|
+
:param until: Bytes until which to receive
|
|
35
|
+
:param max_len: Max length until which to search
|
|
36
|
+
:param peek: Whether to keep the bytes in the buffer
|
|
37
|
+
:return: Read data
|
|
38
|
+
"""
|
|
39
|
+
start_time = time()
|
|
40
|
+
while len(list(filter(lambda x: x in self.buffer, until))) == 0:
|
|
41
|
+
if len(self.buffer) > max_len:
|
|
42
|
+
raise ParserException(f"Exceeded max length of {max_len} bytes")
|
|
43
|
+
if time() - start_time > self.timeout:
|
|
44
|
+
raise ParserException(f"Exceeded timeout of {self.timeout}s")
|
|
45
|
+
self.buffer += self.socket.recv(STANDARD_SOCKET_RECEIVE_SIZE)
|
|
46
|
+
until = list(filter(lambda x: x in self.buffer, until))[0]
|
|
47
|
+
index = self.buffer.index(until) + len(until)
|
|
48
|
+
_res = self.buffer[:index]
|
|
49
|
+
if not peek:
|
|
50
|
+
self.buffer = self.buffer[index:]
|
|
51
|
+
return _res
|
|
52
|
+
|
|
53
|
+
def peek(self, size: int) -> bytes:
|
|
54
|
+
"""
|
|
55
|
+
Similar to read, but keeps data in buffer.
|
|
56
|
+
"""
|
|
57
|
+
while len(self.buffer) < size:
|
|
58
|
+
self.buffer += self.socket.recv(STANDARD_SOCKET_RECEIVE_SIZE)
|
|
59
|
+
return self.buffer[:size]
|
|
60
|
+
|
|
61
|
+
def recv(self, size: int, *args, **kwargs) -> bytes:
|
|
62
|
+
"""
|
|
63
|
+
Works similar to recv of the wrapped socket. Prepends any bytes still buffered.
|
|
64
|
+
:param size: Size of the buffer to read into.
|
|
65
|
+
:return: Bytes read from the socket
|
|
66
|
+
"""
|
|
67
|
+
if len(self.buffer) > 0:
|
|
68
|
+
_res = self.buffer[:size]
|
|
69
|
+
self.buffer = self.buffer[size:]
|
|
70
|
+
else:
|
|
71
|
+
_res = self.socket.recv(size, *args, **kwargs)
|
|
72
|
+
return _res
|
|
73
|
+
|
|
74
|
+
def close(self):
|
|
75
|
+
"""
|
|
76
|
+
Closes the underlying socket.
|
|
77
|
+
"""
|
|
78
|
+
self.socket.shutdown(socket.SHUT_RDWR)
|
|
79
|
+
self.socket.close()
|
|
80
|
+
|
|
81
|
+
def try_close(self):
|
|
82
|
+
"""
|
|
83
|
+
Tries to close the underlying socket. If that fails, we ignore the error.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
self.socket.shutdown(socket.SHUT_RDWR)
|
|
87
|
+
self.socket.close()
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
def inject(self, content: bytes):
|
|
92
|
+
"""
|
|
93
|
+
Injects bytes to the front of the buffer. Can be used to write back read data.
|
|
94
|
+
:param content: the bytes to prepend
|
|
95
|
+
:return: None
|
|
96
|
+
"""
|
|
97
|
+
self.buffer = content + self.buffer
|
network/__init__.py
ADDED
|
File without changes
|
network/protocols/Dns.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import dns
|
|
2
|
+
from dns.message import Message, make_response
|
|
3
|
+
|
|
4
|
+
from exception.DnsException import DnsException
|
|
5
|
+
from network.DomainResolver import DomainResolver
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Dns:
|
|
9
|
+
"""
|
|
10
|
+
Implements methods to parse DNS messages.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
DNS_MAX_SIZE = 512
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def read_dns(message: bytes) -> dns.message.Message:
|
|
17
|
+
try:
|
|
18
|
+
return dns.message.from_wire(message)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
raise DnsException(f"Could not parse DNS message: {e}")
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def query_from_domain(domain: str) -> dns.message.Message:
|
|
24
|
+
domain = dns.name.from_text(domain)
|
|
25
|
+
if not domain.is_absolute():
|
|
26
|
+
domain = domain.concatenate(dns.name.root)
|
|
27
|
+
|
|
28
|
+
query = dns.message.make_query(domain, dns.rdatatype.A)
|
|
29
|
+
query.flags |= dns.flags.AD
|
|
30
|
+
query.find_rrset(query.additional, dns.name.root, 65535, dns.rdatatype.OPT, create=True, force_unique=True)
|
|
31
|
+
return query
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def ip_from_response(response: dns.message.Message, domain_resolver: DomainResolver) -> str:
|
|
35
|
+
if response.rcode() != dns.rcode.NOERROR:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
# filter ipv4 answer
|
|
39
|
+
ips = []
|
|
40
|
+
for record in response.answer:
|
|
41
|
+
if record.rdtype == dns.rdatatype.A:
|
|
42
|
+
for item in record.items:
|
|
43
|
+
ips.append(str(item.address))
|
|
44
|
+
if len(ips) > 0:
|
|
45
|
+
return ips[0]
|
|
46
|
+
else:
|
|
47
|
+
# read CNAME hostnames from answer
|
|
48
|
+
for record in response.answer:
|
|
49
|
+
if record.rdtype == dns.rdatatype.CNAME:
|
|
50
|
+
for item in record.items:
|
|
51
|
+
return domain_resolver.resolve(str(item.target))
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def make_response(message: Message, orig_id: int) -> Message:
|
|
56
|
+
"""
|
|
57
|
+
Creates a DNS response message to the given query. Replaces the answers id with the original id. Necessary for
|
|
58
|
+
DoQ and maybe DoH as they set the id to 0.
|
|
59
|
+
"""
|
|
60
|
+
answer = make_response(message)
|
|
61
|
+
answer.id = orig_id
|
|
62
|
+
return answer
|