From cc3b33a16933f354490c82ee945da35694e97527 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Tue, 14 Jun 2022 02:39:58 -0700 Subject: [PATCH] add tests to verify quic functionality. document quic... This also points to my own repo of quic as it contains a bug fix for drain... --- README.md | 61 ++++++++++++++++++++++++-- ntunnel/quic.py | 111 +++++++++++++++++++++++++++++++++++++++++++++--- setup.py | 2 +- 3 files changed, 163 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cb3c437..ef1309c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,26 @@ be secure and simple to use and setup. Due to the flexibility, it can forward any standard stream socket to another stream socket, including TCP sockets. +ntunnel also supports using QUIC instead of Noise. The advantage of +QUIC is that you it operates over UDP and allows setting congestion +control parameters. The disadvantage is that it using TLS 1.3 like +handshake (instead of the stronger Noise), and channel binding is not +available. + +Installing +---------- + +``` +python3 -m venv p +. ./p/bin/activate +pip install git+https://www.funkthat.com/gitea/jmg/ntunnel.git +``` + +and if you want to install the QUIC variant: +``` +pip install 'ntunnel [quic] @ git+https://www.funkthat.com/gitea/jmg/ntunnel.git' +``` + Example ------- @@ -15,8 +35,8 @@ Note: If you have installed the package, there is also the program Generate the keys: ``` -python -m ntunnel genkey serverkey -python -m ntunnel genkey clientkey +ntunnel genkey serverkey +ntunnel genkey clientkey ``` Create the target for the pass through: @@ -26,8 +46,8 @@ nc -lU finalsock Start the server and client: ``` -python -m ntunnel server serverkey --clientkey clientkey.pub unix:$(pwd)/servsock unix:$(pwd)/finalsock -python -m ntunnel client clientkey serverkey.pub unix:$(pwd)/clientsock unix:$(pwd)/servsock +ntunnel server serverkey --clientkey clientkey.pub unix:$(pwd)/servsock unix:$(pwd)/finalsock +ntunnel client clientkey serverkey.pub unix:$(pwd)/clientsock unix:$(pwd)/servsock ``` Attach to the client: @@ -38,6 +58,39 @@ nc -U clientsock Now when you type text into either of the nc windows, you should see the same text come out the other side. +Example for QUIC +---------------- + +Generate a self-signed server key: +``` +tmp=$(mktemp) +cat > "$tmp" << EOF +[req] +distinguished_name=req +[san] +subjectAltName=DNS:localhost,server.example.com +EOF +openssl req -x509 -newkey rsa:4096 -sha256 -days 3560 -nodes \ + -keyout example.key -out example.crt \ + -subj '/CN=ntunnel example cert' -config "$tmp" +rm "$tmp" +``` + +Note: as QUIC uses standard TLS certificates, instead of a self-signed +certificate as generated above, a certificate signed by a CA may be used +instead. This allows the client to not need the server certificate and uses +the normal CA root store. + +Run the server: +``` +ntunnel quic_serv -k example.key -c example.crt udp:192.0.2.5:12322 tcp:127.0.0.1:22 +``` + +Run client: +``` +ntunnel quic_client --ca-certs funkthat.crt tcp:127.0.0.1:42720 udp:192.0.2.5:12322 +``` + Running Tests ------------- diff --git a/ntunnel/quic.py b/ntunnel/quic.py index 15bb04a..9aef80e 100644 --- a/ntunnel/quic.py +++ b/ntunnel/quic.py @@ -23,9 +23,15 @@ # import asyncio +import os +import random +import shutil +import subprocess +import tempfile import unittest from . import parsesockstr, connectsockstr, listensockstr +from . import async_test, _awaitfile from aioquic.asyncio import QuicConnectionProtocol, serve from aioquic.asyncio.client import connect @@ -37,16 +43,14 @@ async def fwd_data(reader, writer): data = await reader.read(16384) if data == b'': #_debprint('fwd_data eof', repr(reader), repr(writer)) - # XXX - aioquic doesn't implement close - #writer.close() - #await writer.wait_closed() + writer.close() + await writer.wait_closed() #_debprint('fwd_data done', repr(reader), repr(writer)) return #_debprint('fwd_data data', repr(reader), repr(writer), len(data)) writer.write(data) - # XXX - aioquic doesn't implement is_closing - #await writer.drain() + await writer.drain() async def run_connect(dst, rdr, wrr): connrdr, connwrr = await connectsockstr(dst) @@ -147,4 +151,99 @@ def quic_parsers(subparsers): parser_quic_client.set_defaults(func=cmd_quic_client) class Tests(unittest.IsolatedAsyncioTestCase): - pass + def setUp(self): + # setup temporary directory + d = os.path.realpath(tempfile.mkdtemp()) + self.basetempdir = d + self.tempdir = os.path.join(d, 'subdir') + os.mkdir(self.tempdir) + + # Generate key + self.privkey = os.path.join(self.tempdir, 'example.key') + self.cert = os.path.join(self.tempdir, 'example.crt') + conf = ''' +[req] +distinguished_name=req +[san] +subjectAltName=DNS:localhost,server.example.com +'''.encode('utf-8') + k = subprocess.run(['openssl', 'req', '-x509', + '-newkey', 'rsa:4096', '-sha256', '-days', '3560', + '-nodes', '-keyout', self.privkey, '-out', self.cert, + '-subj', '/CN=ntunnel example cert', + '-config', '/dev/stdin'], input=conf, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + self.assertEqual(k.returncode, 0) + + self.assertTrue(os.path.exists(self.privkey)) + self.assertTrue(os.path.exists(self.cert)) + + def tearDown(self): + shutil.rmtree(self.basetempdir) + self.tempdir = None + + @async_test + async def test_e2e(self): + unixservsock = os.path.join(self.tempdir, 'unix.serv.sock') + unixclientsock = os.path.join(self.tempdir, 'unix.client.sock') + + port = random.randint(2000, 65000) + + async def echofun(rdr, wrr): + while True: + d = await rdr.read(16384) + if d: + wrr.write(d) + await wrr.drain() + else: + wrr.close() + await wrr.wait_closed() + return + + # start the destination server + servsock = await asyncio.start_unix_server(echofun, path=unixservsock) + + # start up ntunnel quic processes + serv = await asyncio.create_subprocess_exec('ntunnel', 'quic_serv', '-k', self.privkey, '-c', self.cert, 'udp:127.0.0.1:%d' % port, 'unix:' + unixservsock) + + client = await asyncio.create_subprocess_exec('ntunnel', 'quic_client', '--ca-certs', self.cert, 'unix:' + unixclientsock, 'udp:127.0.0.1:%d' % port) + + # make sure everything has started + await _awaitfile(unixservsock) + await _awaitfile(unixclientsock) + + # run tests + rdr, wrr = await asyncio.open_unix_connection(unixclientsock) + + data = [ b'asldkfj', b'asldkjfasdklj', b'asdlfkjadsf' ] + + for i in data: + wrr.write(i) + await wrr.drain() + + d = await rdr.read(16384) + + self.assertEqual(d, i) + + # make sure close hasn't happened yet + self.assertFalse(rdr.at_eof()) + + # close the writer + wrr.write_eof() + wrr.close() + await wrr.wait_closed() + + # make sure the reader sees that the client closed + self.assertFalse(await rdr.read()) + + # Done terminate daemons + serv.terminate() + client.terminate() + + await serv.wait() + await client.wait() + + # termiante unix server + servsock.close() + await servsock.wait_closed() diff --git a/setup.py b/setup.py index 71b7afd..2fb1be7 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ setup(name='ntunnel', ], extras_require = { 'dev': [ 'coverage' ], - 'quic': [ 'aioquic' ], + 'quic': [ 'aioquic @ git+https://github.com/jmgurney/aioquic.git' ], }, entry_points={ 'console_scripts': [