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
modules/dns/DnsProxy.py
ADDED
|
@@ -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"
|
modules/dns/__init__.py
ADDED
|
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")
|