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,277 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import select
5
+ import socket
6
+ import threading
7
+ import time
8
+ import traceback
9
+
10
+ from dns.message import Message, make_query
11
+ from dns.rcode import SERVFAIL
12
+
13
+ from enumerators.DnsProxyMode import DnsProxyMode
14
+ from exception.DnsException import DnsException
15
+ from modules.dns.DnsModeDeterminator import DnsModeDeterminator
16
+ from modules.dns.DnsResolver import DnsResolver
17
+ from network.DomainResolver import DomainResolver
18
+ from network.NetworkAddress import NetworkAddress
19
+ from network.protocols.Dns import Dns
20
+ from network.tcp.WrappedTcpSocket import WrappedTcpSocket
21
+
22
+
23
+ class DnsProxy:
24
+ """
25
+ Proxy server
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ address: NetworkAddress,
31
+ timeout: int,
32
+ proxy_mode: DnsProxyMode,
33
+ dns_resolver_address: NetworkAddress,
34
+ censored_domain: str,
35
+ compare_ip_ranges: list[str],
36
+ block_page_ips: bool,
37
+ add_sni: bool,
38
+ skip_working_file: bool,
39
+ ):
40
+ # timeout for socket reads and message reception
41
+ self.timeout = timeout
42
+ self.address = address
43
+ self.resolver_address = dns_resolver_address
44
+ self.censored_domain = censored_domain
45
+ self.compare_ip_ranges = compare_ip_ranges
46
+ self.block_page_ips = block_page_ips
47
+ self.proxy_mode = proxy_mode
48
+ self.add_sni = add_sni
49
+ self.skip_working_file = skip_working_file
50
+
51
+ # initialize UDP and TCP server sockets
52
+ self.udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
53
+ self.udp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
54
+ self.udp_server.settimeout(timeout)
55
+ self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
56
+ self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
57
+ self.udp_server.settimeout(timeout)
58
+ self.continue_processing = True
59
+
60
+ # initialized in start()
61
+ self.domain_resolver: DomainResolver | None = None
62
+
63
+ # for time measurement
64
+ self.start_time = time.time()
65
+
66
+ def handle_udp(self, message: bytes, address: NetworkAddress):
67
+ answer = self.resolve_message(message, address)
68
+ self.udp_server.sendto(answer.to_wire(), (address.host, address.port))
69
+ logging.info(f"{address.host}:{address.port}: request resolved")
70
+
71
+ def handle_tcp(self, client_socket: WrappedTcpSocket, address: NetworkAddress):
72
+ try:
73
+ # read 2-byte length field
74
+ _len = int.from_bytes(client_socket.read(2), byteorder="big")
75
+ # read following message
76
+ request = client_socket.read(_len)
77
+ except Exception as e:
78
+ logging.error(f"Could not receive client DNS request with exception: {e}")
79
+ return
80
+ else:
81
+ answer = self.resolve_message(request=request, address=address)
82
+ if answer is not None:
83
+ client_socket.send(answer.to_wire(prepend_length=True))
84
+
85
+ def resolve_message(self, request: bytes, address: NetworkAddress) -> Message | None:
86
+ """
87
+ Resolves the given DNS request bytes at the provider configured on the domain_resolver. Returns an Error DNS
88
+ message on resolution errors and None when the DNS request in unparseable.
89
+ """
90
+ # receive message from client
91
+ try:
92
+ request = Dns.read_dns(request)
93
+ logging.debug(f"{address.host}:{address.port}: parsed dns message:\n{request}")
94
+ except DnsException as e:
95
+ logging.error(f"{address.host}:{address.port}: Could not parse DNS message: {e}")
96
+ return None
97
+
98
+ # save if replaced by DoQ/DoH
99
+ _id = request.id
100
+ try:
101
+ # handle message
102
+ answer = self.domain_resolver.resolve(request)
103
+ except Exception as _:
104
+ logging.error(
105
+ f"{address.host}:{address.port}: Could not query Dns message using mode {self.proxy_mode} "
106
+ f"with error: {traceback.format_exc()}"
107
+ )
108
+ answer = Dns.make_response(request, orig_id=_id)
109
+ answer.set_rcode(SERVFAIL)
110
+ else:
111
+ logging.debug(
112
+ f"{address.host}:{address.port}: Successfully resolved Dns message using mode {self.proxy_mode}. "
113
+ f"Sending answer to client:\n{answer}"
114
+ )
115
+ return answer
116
+
117
+ def generate_domain_resolver(self):
118
+ """
119
+ Generator that yields a new DomainResolver based on the CLI configuration.
120
+ """
121
+ if self.proxy_mode == DnsProxyMode.AUTO:
122
+ _gen = DnsModeDeterminator(
123
+ self.timeout, self.censored_domain, self.compare_ip_ranges, self.block_page_ips
124
+ ).generate_working_resolver()
125
+ yield from _gen
126
+
127
+ elif self.resolver_address.host is None:
128
+ _gen = DnsModeDeterminator(
129
+ self.timeout, self.censored_domain, self.compare_ip_ranges, self.block_page_ips
130
+ ).generate_working_resolver(self.proxy_mode)
131
+ yield from _gen
132
+
133
+ elif self.resolver_address.port is not None:
134
+ logging.info(
135
+ f"mode {self.proxy_mode} and resolver {self.resolver_address.host} specified. Setting standard "
136
+ f"port {self.proxy_mode.default_port()}."
137
+ )
138
+ # mode and resolver specified, set standard port accordingly
139
+ yield DomainResolver(
140
+ dns_mode=self.proxy_mode,
141
+ resolver=NetworkAddress(self.resolver_address.host, self.proxy_mode.default_port()),
142
+ timeout=self.timeout,
143
+ hostname="",
144
+ add_sni=self.add_sni,
145
+ )
146
+ else:
147
+ logging.info(
148
+ f"mode {self.proxy_mode} and resolver {self.resolver_address.host}:{self.resolver_address.port} "
149
+ "specified. Using these values."
150
+ )
151
+ # mode, resolver, and port specified
152
+ yield DomainResolver(
153
+ dns_mode=self.proxy_mode,
154
+ resolver=self.resolver_address,
155
+ timeout=self.timeout,
156
+ hostname="",
157
+ add_sni=self.add_sni,
158
+ )
159
+
160
+ def configure(self):
161
+ """
162
+ Determines a working domain resolver / circumvention method based on the CLI configuration.
163
+ """
164
+ logging.info("Determining working circumvention method / resolver!")
165
+ found_working = False
166
+ domain_resolver_generator = self.generate_domain_resolver()
167
+
168
+ if os.path.exists("working_resolver_config.json") and not self.skip_working_file:
169
+ logging.info("Trying already found working resolver from config")
170
+ with open("working_resolver_config.json", "r") as f:
171
+ data = json.load(f)
172
+ resolver = DomainResolver.from_dict(data)
173
+ self.domain_resolver = resolver
174
+ self.proxy_mode = self.domain_resolver.dns_mode
175
+ found_working = resolver.works(message=make_query(self.censored_domain, "A"))
176
+
177
+ while not found_working:
178
+ # determine next possible resolver
179
+ try:
180
+ dns_resolver: DnsResolver = next(domain_resolver_generator)
181
+ domain_resolver: DomainResolver = DomainResolver(
182
+ dns_mode=dns_resolver.mode,
183
+ resolver=dns_resolver.address,
184
+ timeout=self.timeout,
185
+ hostname=dns_resolver.hostname,
186
+ add_sni=self.add_sni,
187
+ path=dns_resolver.path,
188
+ )
189
+ except StopIteration:
190
+ raise DnsException("No working circumvention method found according to specification in CLI.")
191
+
192
+ # determine if it is working
193
+ logging.info(
194
+ f"Found working circumvention method / resolver {domain_resolver}! Checking if consistently reachable!"
195
+ )
196
+ found_working = domain_resolver.works(message=make_query(self.censored_domain, "A"))
197
+ if not found_working:
198
+ logging.info(f"{domain_resolver} not consistently reachable, attempting to generate new resolver.")
199
+ else:
200
+ logging.info(f"{domain_resolver} consistently reachable, keeping!")
201
+ self.domain_resolver = domain_resolver
202
+ self.proxy_mode = self.domain_resolver.dns_mode
203
+ with open("working_resolver_config.json", "w") as f:
204
+ json.dump(domain_resolver.to_dict(), f, indent=4)
205
+ logging.info(
206
+ f"Finding consistent mode and starting resolvers took {time.time() - self.start_time} seconds "
207
+ "in total."
208
+ )
209
+ if not domain_resolver.add_sni and domain_resolver.dns_mode in [
210
+ DnsProxyMode.DOT,
211
+ DnsProxyMode.DOH,
212
+ DnsProxyMode.DOH3,
213
+ DnsProxyMode.DOQ,
214
+ ]:
215
+ logging.warning(
216
+ "Sending no SNI with an encrypted DNS mode leads to no certificate validation during the "
217
+ "TLS handshake. If you would like to avoid this, set dns_add_sni to True."
218
+ )
219
+ return time.time() - self.start_time
220
+
221
+ def start(self, time_measurement_only: bool = False):
222
+ """
223
+ Starts the proxy. After calling the proxy, listens for connections.
224
+ """
225
+ try:
226
+ # determine circumvention method
227
+ startup_time = self.configure()
228
+ if time_measurement_only:
229
+ return startup_time
230
+ except DnsException as e:
231
+ logging.error(f"{e}")
232
+ else:
233
+ # start tcp and udp DNS server
234
+ threading.Thread(target=self.start_udp_server).start()
235
+ threading.Thread(target=self.start_tcp_server).start()
236
+
237
+ def start_udp_server(self):
238
+ """
239
+ Runs a UDP DNS server.
240
+ """
241
+ # opening server socket
242
+ self.udp_server.bind((self.address.host, self.address.port))
243
+ print(f"### Started UDP DNS server on {self.address.host}:{self.address.port} ###")
244
+
245
+ while self.continue_processing:
246
+ readable, _, _ = select.select([self.udp_server], [], [], 1)
247
+ if not readable:
248
+ continue
249
+ # listen for incoming connections
250
+ message, address = self.udp_server.recvfrom(Dns.DNS_MAX_SIZE * 4)
251
+ address = NetworkAddress(address[0], address[1])
252
+ logging.info(f"{address.host}:{address.port}: request received over UDP")
253
+ # spawn a new thread that runs the function handle()
254
+ threading.Thread(target=self.handle_udp, args=(message, address)).start()
255
+ logging.info("### Stopped UDP DNS server ###")
256
+
257
+ def start_tcp_server(self):
258
+ """
259
+ Runs a TCP DNS server.
260
+ """
261
+ # opening server socket
262
+ self.tcp_server.bind((self.address.host, self.address.port))
263
+ self.tcp_server.listen()
264
+ print(f"### Started TCP DNS server on {self.address.host}:{self.address.port} ###")
265
+
266
+ while self.continue_processing:
267
+ readable, _, _ = select.select([self.tcp_server], [], [], 1)
268
+ if not readable:
269
+ continue
270
+ # listen for incoming connections
271
+ client_socket, address = self.tcp_server.accept()
272
+ address = NetworkAddress(address[0], address[1])
273
+ client_socket = WrappedTcpSocket(self.timeout, client_socket)
274
+ logging.info(f"{address.host}:{address.port}: DNS request received over TCP")
275
+ # spawn a new thread that runs the function handle()
276
+ threading.Thread(target=self.handle_tcp, args=(client_socket, address)).start()
277
+ logging.info("### Stopped TCP DNS server ###")
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass
2
+
3
+ from enumerators.DnsProxyMode import DnsProxyMode
4
+ from network.NetworkAddress import NetworkAddress
5
+
6
+
7
+ @dataclass
8
+ class DnsResolver:
9
+ name: str
10
+ address: NetworkAddress
11
+ mode: DnsProxyMode
12
+ hostname: str
13
+ successes: int
14
+ tries: int
15
+ path: str
16
+
17
+ def __str__(self):
18
+ return f"{self.name}({self.address} - {self.mode}) | ({self.successes}/{self.tries}) Successes"
File without changes
@@ -0,0 +1,69 @@
1
+ import logging
2
+ from argparse import ArgumentParser, Namespace
3
+
4
+ from modules.http.HttpStrategies import HttpStrategies
5
+ from modules.Module import Module
6
+ from modules.tls.TcpProxy import TcpProxy
7
+ from network.NetworkAddress import NetworkAddress
8
+
9
+
10
+ class HttpModule(Module):
11
+ """
12
+ Implements circumvention methods for the HTTP censorship.
13
+ Only one technique allowed at a time. Can not combine direct
14
+ HTTP manipulations and HTTP request smuggling strategies.
15
+ (falls back to direct manipulation in that case)
16
+ """
17
+
18
+ def __init__(self, parser: ArgumentParser):
19
+ super().__init__(parser)
20
+
21
+ @staticmethod
22
+ def register_parameters(parser: ArgumentParser):
23
+
24
+ http_module = parser.add_argument_group("HTTP Module")
25
+
26
+ http_module.add_argument("--http_timeout", type=int, default=10, help="Connection timeout in seconds")
27
+
28
+ http_module.add_argument("--http_host", type=str, default="localhost", help="Address the proxy server runs on")
29
+
30
+ http_module.add_argument("--http_port", type=int, default=8080, help="Port the proxy server runs on")
31
+
32
+ http_module.add_argument(
33
+ "--http_strategy",
34
+ type=int,
35
+ default=None,
36
+ help="Number of which specific http manipulation strategy to apply. "
37
+ "None: no manipulation, [1..70]: basic manipulations, [101, 129]: Smuggling."
38
+ "See HttpStrategies for meaning.",
39
+ )
40
+
41
+ http_module.add_argument(
42
+ "--http_smuggling_uncensored_url",
43
+ type=str,
44
+ default="https://www.gov.cn/",
45
+ help="Uncensored url to use for http smuggling.",
46
+ )
47
+
48
+ def extract_parameters(self, args: Namespace):
49
+ server_address = NetworkAddress(args.http_host, args.http_port)
50
+
51
+ if args.http_strategy is not None and not HttpStrategies.strategy_is_valid(args.http_strategy):
52
+ logging.error(
53
+ f"Invalid http strategy specified. Provided: {args.http_strategy}, Allowed: None: no manipulation,"
54
+ f" [1..70]: basic manipulations, [101, 129]: Smuggling."
55
+ )
56
+
57
+ self.proxy = TcpProxy(
58
+ address=server_address,
59
+ timeout=args.http_timeout,
60
+ http_strategy=args.http_strategy,
61
+ http_smuggling_uncensored_url=args.http_smuggling_uncensored_url,
62
+ )
63
+
64
+ def start(self):
65
+ self.proxy.start()
66
+
67
+ def stop(self):
68
+ self.proxy.continue_processing = False
69
+ logging.info("Waiting for proxy to stop")