Skip to content

Commit ca12f70

Browse files
committed
nodpi-https-proxy: add --block-http option, better parsing for destination, quieter conn errors
1 parent 03df333 commit ca12f70

File tree

2 files changed

+41
-39
lines changed

2 files changed

+41
-39
lines changed

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2588,14 +2588,15 @@ Not a replacement for wireshark or tcpdump firehose-filters.
25882588
##### [nodpi-https-proxy](nodpi-https-proxy)
25892589

25902590
Simpler ~100-line version of [GVCoder09/NoDPI] http-proxy script,
2591-
which fragments https requests where it detects SNI that won't be
2592-
allowed through by some censorshit DPI otherwise.
2591+
which fragments https requests where it detects SNI domains that won't
2592+
be allowed through by some state-censorshit DPI devices otherwise.
25932593

2594-
Rewritten to not have as much needless stats, boilerplate, verbosity
2595-
and cross-platform cruft, to make easier adjustments, e.g. to start/stop
2596-
as-needed in systemd user session:
2594+
Rewritten to not have as much needless stats, boilerplate, verbosity and
2595+
cross-platform cruft, to make easier adjustments, e.g. to block plain http
2596+
and start/stop as-needed in systemd user session:
25972597

25982598
``` ini
2599+
# nodpi.socket
25992600
[Socket]
26002601
ListenStream=127.0.0.1:8101
26012602
@@ -2604,9 +2605,10 @@ WantedBy=sockets.target
26042605
```
26052606

26062607
``` ini
2608+
# nodpi.service
26072609
[Service]
26082610
Type=exec
2609-
ExecStart=nodpi-https-proxy -t 600
2611+
ExecStart=nodpi-https-proxy --block-http --idle-timeout 600
26102612
```
26112613

26122614
[GVCoder09/NoDPI]: https://github.com/GVCoder09/NoDPI

nodpi-https-proxy

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22

3-
import os, sys, socket, logging, inspect, random, signal, asyncio, contextlib as cl
3+
import os, sys, socket, logging, inspect, random, signal, asyncio
44

55

66
err_fmt = lambda err: f'[{err.__class__.__name__}] {err}'
@@ -9,8 +9,8 @@ class HTTPSFragProxy:
99

1010
buff_sz = 1500
1111

12-
def __init__(self, sni_list=None, idle_timeout=None, **bind):
13-
self.idle_timeout, self.bind = idle_timeout, bind
12+
def __init__(self, sni_list=None, idle_timeout=None, block_http=False, **bind):
13+
self.idle_timeout, self.block_http, self.bind = idle_timeout, block_http, bind
1414
if sni_list is not None:
1515
if not sni_list: sni_list = [b'\0'] # match nothing
1616
else: sni_list = list(s.strip().encode() for s in sni_list)
@@ -33,47 +33,48 @@ class HTTPSFragProxy:
3333
return os.kill(os.getpid(), signal.SIGTERM)
3434
self.activity.clear()
3535

36-
async def conn_wrap(self, func, reader, writer, close_writer=False):
36+
async def conn_wrap(self, func, cid, reader, writer, close_writer=True):
3737
try: await func(reader, writer)
38-
except Exception as err:
38+
except Exception as err: # handles client closing connection too
3939
writer.close()
40-
self.log.exception('Failed handing connection: %s', err_fmt(err))
40+
self.log.info('Connection error [%s]: %s', ':'.join(cid), err_fmt(err))
4141
finally:
4242
if close_writer: writer.close()
4343

4444
def conn_handle(self, reader, writer):
45-
return self.conn_wrap(self._conn_handle, reader, writer)
46-
async def _conn_handle(self, reader, writer):
45+
cid = list(map(str, writer.get_extra_info('peername')))
46+
return self.conn_wrap(lambda *a: self._conn_handle(cid, *a), cid, reader, writer)
47+
48+
async def _conn_handle(self, cid, reader, writer):
4749
self.activity.set()
48-
cid = ':'.join(map(str, writer.get_extra_info('peername')))
4950
http_data = await reader.read(self.buff_sz)
5051
if not http_data: writer.close(); return
51-
method, url = (headers := http_data.split(b'\r\n'))[0].decode().split()[:2]
52+
method, url = dst = http_data.split(b'\r\n')[0].decode().split()[:2]
5253
self.log.debug( 'Connection [%s] ::'
53-
' %s %s [buff=%d]', cid, method, url, len(http_data) )
54+
' %s %s [buff=%d]', ':'.join(cid), method, url, len(http_data) )
55+
cid.extend(dst)
5456

55-
if https := (method == 'CONNECT'): # https
56-
host, _, port = url.partition(':')
57-
port = int(port) if port else 443
58-
else: # paintext http
59-
host_header = next((h for h in headers if h.startswith(b'Host: ')), None)
60-
if not host_header: raise ValueError('Missing Host header')
61-
host, _, port = host_header[6:].partition(b':')
62-
host, port = host.decode(), int(port) if port else 80
57+
if https := (method == 'CONNECT'): port_default = 443
58+
elif not url.startswith('http://') or self.block_http: raise ValueError(url)
59+
else: url, port_default = url[7:].split('/', 1)[0], 80
60+
host, _, port = url.rpartition(':')
61+
if host and port and port.isdigit(): port = int(port)
62+
else: host, port = url, port_default
63+
if host[0] == '[' and host[-1] == ']': host = host[1:-1] # raw ipv6
6364

6465
try: xreader, xwriter = await asyncio.open_connection(host, port)
65-
except OSError as err: return self.log.info( 'Connection [%s]'
66-
' to %s:%s failed (tls=%s): %s', cid, host, port, int(https), err_fmt(err) )
66+
except OSError as err: return self.log.info( 'Connection [%s] to %s:%s'
67+
' failed (tls=%s): %s', ':'.join(cid), host, port, int(https), err_fmt(err) )
6768
self.activity.set()
6869
if not https: xwriter.write(http_data); await xwriter.drain()
6970
else:
7071
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
7172
await writer.drain()
72-
await self.conn_wrap(self.fragment_data, reader, xwriter)
73+
await self.conn_wrap(self.fragment_data, cid, reader, xwriter, False)
7374

7475
for task in asyncio.as_completed([
75-
self.conn_wrap(self.pipe, reader, xwriter, True),
76-
self.conn_wrap(self.pipe, xreader, writer, True) ]):
76+
self.conn_wrap(self.pipe, cid, reader, xwriter),
77+
self.conn_wrap(self.pipe, cid, xreader, writer) ]):
7778
await task
7879

7980
async def fragment_data(self, reader, writer):
@@ -121,22 +122,20 @@ def main(args=None):
121122
Default is to apply such fragmentation to all processed connections.'''))
122123
parser.add_argument('-t', '--idle-timeout', type=float, metavar='seconds', help=dd('''
123124
Stop after number of seconds being idle. Useful if started from systemd socket.'''))
125+
parser.add_argument('--block-http', action='store_true', help=dd('''
126+
Reject/close insecure plaintext http connections made through the proxy.'''))
124127
parser.add_argument('--debug', action='store_true', help='Verbose logging to stderr.')
125128
opts = parser.parse_args(sys.argv[1:] if args is None else args)
126129

127-
@cl.contextmanager
128-
def in_file(path):
129-
if not path or path == '-': return (yield sys.stdin)
130-
if path[0] == '%': path = int(path[1:])
131-
with open(path) as src: yield src
132-
133130
logging.basicConfig( format='%(levelname)s :: %(message)s',
134131
level=logging.WARNING if not opts.debug else logging.DEBUG )
135132
log = logging.getLogger('nhp.main')
136133

137134
sni_list = None
138-
if opts.frag_domains:
139-
with in_file(opts.frag_domains) as src: sni_list = src.read().split()
135+
if p := opts.frag_domains:
136+
if not p or p == '-': src = sys.stdin
137+
else: src = open(p if p[0] != '%' else int(p[1:]))
138+
sni_list = src.read().split()
140139

141140
sd_pid, sd_fds = (int(os.environ.get(f'LISTEN_{k}', 0)) for k in ['PID', 'FDS'])
142141
if sd_pid == os.getpid() and sd_fds:
@@ -147,7 +146,8 @@ def main(args=None):
147146
else:
148147
host, _, port = opts.bind.partition(':'); port = int(port or 8101)
149148
proxy = dict(host=host, port=port)
150-
proxy = HTTPSFragProxy(sni_list=sni_list, idle_timeout=opts.idle_timeout, **proxy)
149+
proxy = HTTPSFragProxy( sni_list=sni_list,
150+
idle_timeout=opts.idle_timeout, block_http=opts.block_http, **proxy )
151151

152152
log.debug( 'Starting proxy (%s)...',
153153
f'{len(sni_list):,d} SNI domains' if sni_list is not None else 'fragmenting any SNI' )

0 commit comments

Comments
 (0)