@@ -9,20 +9,30 @@ class HTTPSFragProxy:
9
9
10
10
buff_sz = 1500
11
11
12
- def __init__ (self , host , port , sni_list = None , af = 0 ):
13
- self .bind , self .log = ( host , port , af ), logging . getLogger ( 'nhp.proxy' )
12
+ def __init__ (self , sni_list = None , idle_timeout = None , ** bind ):
13
+ self .idle_timeout , self .bind = idle_timeout , bind
14
14
if sni_list is not None :
15
15
if not sni_list : sni_list = [b'\0 ' ] # match nothing
16
16
else : sni_list = list (s .strip ().encode () for s in sni_list )
17
- self .snis = sni_list
17
+ self .snis , self . log = sni_list , logging . getLogger ( 'nhp.proxy' )
18
18
19
19
async def run (self ):
20
- ( host , port , af ), loop = self . bind , asyncio .get_running_loop ()
20
+ loop , self . activity = asyncio . get_running_loop () , asyncio .Event ()
21
21
for sig in signal .SIGINT , signal .SIGTERM : # asyncio.start_server hangs
22
22
loop .add_signal_handler (sig , lambda : os ._exit (0 ))
23
- server = await asyncio .start_server (self .conn_handle , host , port , family = af )
23
+ if self .idle_timeout :
24
+ asyncio .create_task (self .idle_stop (self .idle_timeout ))
25
+ server = await asyncio .start_server (self .conn_handle , ** self .bind )
24
26
await server .serve_forever ()
25
27
28
+ async def idle_stop (self , timeout ):
29
+ while True :
30
+ try : await asyncio .wait_for (self .activity .wait (), timeout )
31
+ except TimeoutError :
32
+ self .log .debug ('Exiting on timeout (%.1fs)' , timeout )
33
+ return os .kill (os .getpid (), signal .SIGTERM )
34
+ self .activity .clear ()
35
+
26
36
async def conn_wrap (self , func , reader , writer , close_writer = False ):
27
37
try : await func (reader , writer )
28
38
except Exception as err :
@@ -34,6 +44,7 @@ class HTTPSFragProxy:
34
44
def conn_handle (self , reader , writer ):
35
45
return self .conn_wrap (self ._conn_handle , reader , writer )
36
46
async def _conn_handle (self , reader , writer ):
47
+ self .activity .set ()
37
48
cid = ':' .join (map (str , writer .get_extra_info ('peername' )))
38
49
http_data = await reader .read (self .buff_sz )
39
50
if not http_data : writer .close (); return
@@ -53,6 +64,7 @@ class HTTPSFragProxy:
53
64
try : xreader , xwriter = await asyncio .open_connection (host , port )
54
65
except OSError as err : return self .log .info ( 'Connection [%s]'
55
66
' to %s:%s failed (tls=%s): %s' , cid , host , port , int (https ), err_fmt (err ) )
67
+ self .activity .set ()
56
68
if not https : xwriter .write (http_data ); await xwriter .drain ()
57
69
else :
58
70
writer .write (b'HTTP/1.1 200 Connection Established\r \n \r \n ' )
@@ -83,29 +95,32 @@ class HTTPSFragProxy:
83
95
84
96
async def pipe (self , reader , writer ):
85
97
while not reader .at_eof () and not writer .is_closing ():
98
+ self .activity .set ()
86
99
data = await reader .read (self .buff_sz ); writer .write (data ); await writer .drain ()
87
100
88
101
89
102
def main (args = None ):
90
103
import argparse , textwrap , re
91
104
dd = lambda text : re .sub ( r' \t+' , ' ' ,
92
105
textwrap .dedent (text ).strip ('\n ' ) + '\n ' ).replace ('\t ' , ' ' )
93
- parser = argparse .ArgumentParser (
94
- formatter_class = argparse .RawTextHelpFormatter , usage = '%(prog)s [opts] [lines]' ,
95
- description = dd ('''
106
+ parser = argparse .ArgumentParser (usage = '%(prog)s [options]' ,
107
+ formatter_class = argparse .RawTextHelpFormatter , description = dd ('''
96
108
HTTP-proxy for TLS connections that detects specific hosts in SNI fields
97
109
and fragments those to avoid/confuse deep packet inspection (DPI) systems.
98
110
Despite the name, it should be set as "HTTP Proxy" in e.g. browsers, NOT HTTPS one.
99
111
Based on https://github.com/GVCoder09/NoDPI distributed under GPL v3 license.''' ))
100
112
parser .add_argument ('-i' , '--bind' ,
101
113
metavar = 'host[:port]' , default = '127.0.0.1:8101' , help = dd ('''
102
114
Address/host and port to bind proxy server socket to (default: %(default)s).
103
- It should be used and accessible with e.g. http_proxy= env-var.''' ))
115
+ It should be used and accessible with e.g. http_proxy= env-var.
116
+ Ignored if process is started by systemd from a socket unit.''' ))
104
117
parser .add_argument ('-d' , '--frag-domains' , metavar = 'file' , help = dd ('''
105
118
File with a list of space/newline-separated domains
106
119
to detect in TLS handshakes and fragment connection data for.
107
120
"-" can be used for stdin, or %%-prefixed number for any file descriptor (e.g. %%3).
108
121
Default is to apply such fragmentation to all processed connections.''' ))
122
+ parser .add_argument ('-t' , '--idle-timeout' , type = float , metavar = 'seconds' , help = dd ('''
123
+ Stop after number of seconds being idle. Useful if started from systemd socket.''' ))
109
124
parser .add_argument ('--debug' , action = 'store_true' , help = 'Verbose logging to stderr.' )
110
125
opts = parser .parse_args (sys .argv [1 :] if args is None else args )
111
126
@@ -123,8 +138,17 @@ def main(args=None):
123
138
if opts .frag_domains :
124
139
with in_file (opts .frag_domains ) as src : sni_list = src .read ().split ()
125
140
126
- host , _ , port = opts .bind .partition (':' ); port = int (port or 8101 )
127
- proxy = HTTPSFragProxy (host , port , sni_list )
141
+ sd_pid , sd_fds = (int (os .environ .get (f'LISTEN_{ k } ' , 0 )) for k in ['PID' , 'FDS' ])
142
+ if sd_pid == os .getpid () and sd_fds :
143
+ if sd_fds > 1 : return log .error ( 'More than one socket passed'
144
+ ' from systemd, exiting (pid=%s fds=%s)' , sd_pid , sd_fds ) or 1
145
+ log .debug ('Listening on socket fd=3 received from systemd' )
146
+ proxy = dict (sock = socket .socket (fileno = 3 ))
147
+ else :
148
+ host , _ , port = opts .bind .partition (':' ); port = int (port or 8101 )
149
+ proxy = dict (host = host , port = port )
150
+ proxy = HTTPSFragProxy (sni_list = sni_list , idle_timeout = opts .idle_timeout , ** proxy )
151
+
128
152
log .debug ( 'Starting proxy (%s)...' ,
129
153
f'{ len (sni_list ):,d} SNI domains' if sni_list is not None else 'fragmenting any SNI' )
130
154
return asyncio .run (proxy .run ())
0 commit comments