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.
Files changed (59) hide show
  1. dpyproxy/__init__.py +1 -0
  2. dpyproxy/__main__.py +4 -0
  3. dpyproxy-2.2.0.dist-info/METADATA +296 -0
  4. dpyproxy-2.2.0.dist-info/RECORD +59 -0
  5. dpyproxy-2.2.0.dist-info/WHEEL +4 -0
  6. dpyproxy-2.2.0.dist-info/entry_points.txt +2 -0
  7. dpyproxy-2.2.0.dist-info/licenses/LICENSE +201 -0
  8. enumerators/DnsProxyMode.py +39 -0
  9. enumerators/DnsResolvers.py +141 -0
  10. enumerators/HttpMethod.py +17 -0
  11. enumerators/Modules.py +38 -0
  12. enumerators/Port.py +11 -0
  13. enumerators/TcpProxyMode.py +17 -0
  14. enumerators/TlsVersion.py +21 -0
  15. enumerators/__init__.py +0 -0
  16. exception/DnsException.py +7 -0
  17. exception/ParserException.py +7 -0
  18. exception/__init__.py +0 -0
  19. main.py +94 -0
  20. modules/Module.py +45 -0
  21. modules/__init__.py +0 -0
  22. modules/dns/DnsModeDeterminator.py +358 -0
  23. modules/dns/DnsModule.py +113 -0
  24. modules/dns/DnsProxy.py +277 -0
  25. modules/dns/DnsResolver.py +18 -0
  26. modules/dns/__init__.py +0 -0
  27. modules/http/HttpModule.py +69 -0
  28. modules/http/HttpStrategies.py +849 -0
  29. modules/http/HttpUtils.py +94 -0
  30. modules/http/__init__.py +0 -0
  31. modules/tls/TcpProxy.py +106 -0
  32. modules/tls/TlsModule.py +173 -0
  33. modules/tls/__init__.py +0 -0
  34. network/DomainResolver.py +472 -0
  35. network/NetworkAddress.py +10 -0
  36. network/WrappedSocket.py +97 -0
  37. network/__init__.py +0 -0
  38. network/protocols/Dns.py +62 -0
  39. network/protocols/Http.py +109 -0
  40. network/protocols/Socksv4.py +70 -0
  41. network/protocols/Socksv5.py +106 -0
  42. network/protocols/Tls.py +113 -0
  43. network/protocols/__init__.py +0 -0
  44. network/tcp/Forwarder.py +203 -0
  45. network/tcp/TcpConnectionHandler.py +264 -0
  46. network/tcp/WrappedTcpSocket.py +30 -0
  47. network/tcp/__init__.py +0 -0
  48. network/udp/__init__.py +0 -0
  49. test/Sink.py +23 -0
  50. test/__init__.py +0 -0
  51. test/test_dns.py +98 -0
  52. test/test_http.py +57 -0
  53. test/test_tls.py +63 -0
  54. util/DnsAutoModeRuntimeMeasurement.py +62 -0
  55. util/DnsReachabilityCollector.py +160 -0
  56. util/DnsResolversDomainResolver.py +36 -0
  57. util/Util.py +62 -0
  58. util/__init__.py +0 -0
  59. 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}"
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class NetworkAddress:
6
+ host: str
7
+ port: int
8
+
9
+ def __str__(self):
10
+ return f"{self.host}:{self.port}"
@@ -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
@@ -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