diff --git a/CHANGES.md b/CHANGES.md index 9a25977..a0dd1fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ (Oct 18, 2018) - Added basic configuration +- Support for SSL/TLS communication - Verbose flag (Oct 14, 2018) diff --git a/README.md b/README.md index 07289dc..2738fa2 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,27 @@ doe@10.5.0.240:~$ docker build -t aiotunnel /path/to/aiotunnel doe@10.5.0.240:~$ docker run --rm --network host aiotunnel python aiotunnel.py client --server-addr 10.5.0.10 --server-port 8080 -A localhost -p 22 -r ``` +### Security + +`SSL/TLS` is supported, just set certificates cain and ca in the configuration or by the CLI process +to encrypt the communication and use HTTPS (defaulting on port 8443 instead of 8080) + +```sh +doe@10.5.0.10:~$ python aiotunnel.py server -r --ca /path/to/ca.crt --cert /path/to/cert.crt --key +/path/to/keyfile.key +======== Running on https://0.0.0.0:8443 ======== +``` + +And client side + +```sh +doe@10.5.0.240:~$ python aiotunnel.py client -A 127.0.0.1 -P 22 --ca /path/to/ca.crt --cert +/path/to/cert.crt --key /path/to/keyfile.key +[2018-10-18 22:20:45,806] Opening a connection with 127.0.0.1:22 and 0.0.0.0:8888 over HTTPS +[2018-10-18 22:20:45,831] 0.0.0.0:8888 over HTTPS to https://10.5.0.10:8443/aiotunnel +[2018-10-18 22:20:45,832] Obtained a client id: aeb7dfc4-3da3-4wc1-b769-n81621db96eb +``` + ## Installation Clone the repository and install it locally or play with it using `python -i` or `ipython`. diff --git a/aiotunnel/__init__.py b/aiotunnel/__init__.py index c82ab9b..9f2c949 100644 --- a/aiotunnel/__init__.py +++ b/aiotunnel/__init__.py @@ -32,7 +32,7 @@ import json import logging -__version__ = '1.1.0' +__version__ = '1.2.0' CONFIG = { 'logpath': './', diff --git a/aiotunnel/cli.py b/aiotunnel/cli.py index 91c677f..c14d4c3 100644 --- a/aiotunnel/cli.py +++ b/aiotunnel/cli.py @@ -53,6 +53,9 @@ def get_parser(): parser.add_argument('--target-port', '-P', action='store', help='Set the port for target-addr') parser.add_argument('--server-addr', '-sa', action='store', help='Set the target address') parser.add_argument('--server-port', '-sp', action='store', help='Set the target port') + parser.add_argument('--ca', action='store', help='Set the cert. authority file') + parser.add_argument('--cert', action='store', help='Set the crt file for SSL/TLS encryption') + parser.add_argument('--key', action='store', help='Set the key file for SSL/TLS encryption') return parser @@ -70,6 +73,16 @@ def main(): setup_logging() + # SSL/TLS certificates + cafile = args.ca + certfile = args.cert + keyfile = args.key + + if cafile or (certfile and keyfile): + set_config_key('client', {'server_port': 8443}) + set_config_key('server', {'port': 8443}) + + # Connection directives, addresses and targets client_host, client_port = CONFIG['client']['host'], CONFIG['client']['port'] server_host, server_port = CONFIG['server']['host'], CONFIG['server']['port'] reverse = args.reverse or CONFIG['reverse'] @@ -93,8 +106,10 @@ def main(): if args.server_port: server_port = args.server_port set_config_key('server', {'port': server_port}) - url = f'http://{server_host}:{server_port}/aiotunnel' - start_tunnel(url, (client_host, client_port), (target_addr, target_port), reverse) + scheme = 'https' if cafile else 'http' + url = f'{scheme}://{server_host}:{server_port}/aiotunnel' + start_tunnel(url, (client_host, client_port), (target_addr, target_port), + reverse, cafile=cafile, certfile=certfile, keyfile=keyfile) else: if args.addr: server_host = args.addr @@ -102,4 +117,5 @@ def main(): if args.port: server_port = args.port set_config_key('server', {'port': server_port}) - start_tunneld(server_host, server_port, reverse) + start_tunneld(server_host, server_port, reverse, + cafile=cafile, certfile=certfile, keyfile=keyfile) diff --git a/aiotunnel/protocol.py b/aiotunnel/protocol.py index 8ca71f8..2b3a0fd 100644 --- a/aiotunnel/protocol.py +++ b/aiotunnel/protocol.py @@ -87,12 +87,13 @@ async def async_consume_request(self): class LocalTunnelProtocol(BaseTunnelProtocol): - def __init__(self, loop, remote_host, url, on_conn_lost=None): + def __init__(self, loop, remote_host, url, on_conn_lost=None, ssl_context=None): self.cid = None self.url = url self.remote_host = remote_host self.write_queue = asyncio.Queue() self.on_conn_lost = on_conn_lost + self.ssl_context = ssl_context self.logger = logging.getLogger('aiotunnel.protocol.LocalTunnelProtocol') super().__init__(loop) @@ -113,7 +114,7 @@ async def async_open_remote_connection(self): remote = self.remote_host.encode() try: async with aiohttp.ClientSession() as session: - async with session.post(self.url, data=remote) as resp: + async with session.post(self.url, data=remote, ssl_context=self.ssl_context) as resp: cid = await resp.text() except (aiohttp.ClientError, asyncio.TimeoutError): self.logger.debug("Cannot communicate with %s", self.url) @@ -123,7 +124,8 @@ async def async_open_remote_connection(self): await asyncio.sleep(5) else: self.cid = cid - self.logger.info("%s over HTTP to %s", self.remote_host, self.url) + scheme = 'HTTPS' if self.ssl_context else 'HTTP' + self.logger.info("%s over %s to %s", self.remote_host, scheme, self.url) self.logger.info("Obtained a client id: %s", cid) self.loop.create_task(self.async_write_data()) self.loop.create_task(self.async_read_data()) @@ -131,7 +133,7 @@ async def async_open_remote_connection(self): async def async_close_remote_connection(self): try: async with aiohttp.ClientSession() as session: - await session.delete(f'{self.url}/{self.cid}') + await session.delete(f'{self.url}/{self.cid}', ssl_context=self.ssl_context) except (aiohttp.ClientError, asyncio.TimeoutError): self.logger.debug("Cannot communicate with %s", self.url) await asyncio.sleep(5) @@ -144,7 +146,7 @@ async def async_write_data(self): data = await self.write_queue.get() try: async with aiohttp.ClientSession() as session: - await session.put(f'{self.url}/{self.cid}', data=data) + await session.put(f'{self.url}/{self.cid}', data=data, ssl_context=self.ssl_context) except (aiohttp.ClientError, asyncio.TimeoutError): self.logger.debug("Cannot communicate with %s", self.url) await asyncio.sleep(5) @@ -156,7 +158,7 @@ async def async_read_data(self): while not self._shutdown.is_set(): try: async with aiohttp.ClientSession() as session: - async with session.get(f'{self.url}/{self.cid}') as resp: + async with session.get(f'{self.url}/{self.cid}', ssl_context=self.ssl_context) as resp: data = await resp.read() self.transport.write(data) except (aiohttp.ClientError, asyncio.TimeoutError): diff --git a/aiotunnel/tunnel.py b/aiotunnel/tunnel.py index 02d3ee0..2ea4e2b 100644 --- a/aiotunnel/tunnel.py +++ b/aiotunnel/tunnel.py @@ -28,6 +28,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import ssl import asyncio import logging @@ -39,7 +40,7 @@ logger = logging.getLogger(__name__) -async def create_endpoint(url, client_addr, target_addr): +async def create_endpoint(url, client_addr, target_addr, ssl_context=None): """Create a server endpoint TCP. Args: @@ -59,18 +60,19 @@ async def create_endpoint(url, client_addr, target_addr): host, port = client_addr target_host, target_port = target_addr remote_host = target_host + ':' + str(target_port) - logger.info("Opening local port %s and %s:%s over HTTP", port, target_host, target_port) + scheme = 'HTTPS' if ssl_context else 'HTTP' + logger.info("Opening local port %s and %s:%s over %s", port, target_host, target_port, scheme) loop = asyncio.get_running_loop() # Start the server and serve forever server = await loop.create_server( - lambda: LocalTunnelProtocol(loop, remote_host, url), + lambda: LocalTunnelProtocol(loop, remote_host, url, ssl_context), host, port ) async with server: await server.serve_forever() -async def open_connection(url, client_addr, target_addr): +async def open_connection(url, client_addr, target_addr, ssl_context=None): """Open a TCP connection Args: @@ -88,11 +90,12 @@ async def open_connection(url, client_addr, target_addr): remote = f'{client_addr[0]}:{client_addr[1]}' host, port = target_addr - logger.info("Opening a connection with %s:%s and %s over HTTP", host, port, remote) + scheme = 'HTTPS' if ssl_context else 'HTTP' + logger.info("Opening a connection with %s:%s and %s over %s", host, port, remote, scheme) loop = asyncio.get_running_loop() on_con_lost = loop.create_future() transport, _ = await loop.create_connection( - lambda: LocalTunnelProtocol(loop, remote, url, on_con_lost), + lambda: LocalTunnelProtocol(loop, remote, url, on_con_lost, ssl_context), host, port ) try: @@ -101,11 +104,16 @@ async def open_connection(url, client_addr, target_addr): transport.close() -def start_tunnel(url, client_addr, target_addr, reverse=False): +def start_tunnel(url, client_addr, target_addr, + reverse=False, cafile=None, certfile=None, keyfile=None): + ssl_context = None + if cafile: + ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile=cafile) + ssl_context.load_cert_chain(certfile, keyfile) try: if not reverse: - asyncio.run(create_endpoint(url, client_addr, target_addr)) + asyncio.run(create_endpoint(url, client_addr, target_addr, ssl_context)) else: - asyncio.run(open_connection(url, client_addr, target_addr)) + asyncio.run(open_connection(url, client_addr, target_addr, ssl_context)) except: pass diff --git a/aiotunnel/tunneld.py b/aiotunnel/tunneld.py index 35e4a11..8aafd22 100644 --- a/aiotunnel/tunneld.py +++ b/aiotunnel/tunneld.py @@ -37,6 +37,7 @@ from aiohttp import web +from . import CONFIG from .protocol import TunnelProtocol @@ -130,7 +131,6 @@ async def post_aiotunnel(self, request): cid = uuid.uuid4() service = await request.text() channel = Channel() - self.logger.debug("POST /aiotunnel/%s HTTP/1.1 200", cid) host, port = service.split(':') if self.reverse: self.logger.info("Opening local port %s", port) @@ -167,8 +167,8 @@ async def delete_aiotunnel(self, request): return web.Response() -def create_ssl_context(certfile, keyfile): - ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) +def create_ssl_context(cafile, certfile, keyfile): + ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=cafile) ssl_context.load_cert_chain(certfile, keyfile) return ssl_context @@ -178,17 +178,20 @@ async def on_shutdown_coro(app, handler): await app.shutdown() -def start_tunneld(host, port, reverse=False, certfile=None, keyfile=None): +def start_tunneld(host, port, reverse=False, cafile=None, certfile=None, keyfile=None): app = web.Application() handler = Handler(app, reverse) on_shutdown = partial(on_shutdown_coro, handler=handler) app.on_shutdown.append(on_shutdown) try: - if certfile or keyfile: - ssl_context = create_ssl_context(certfile, keyfile) + if cafile: + ssl_context = create_ssl_context(cafile, certfile, keyfile) web.run_app(app, host=host, port=port, ssl_context=ssl_context, access_log=logger) else: web.run_app(app, host=host, port=port, access_log=logger, access_log_format='"%r" %s %b %Tf %a - "%{User-agent}i"') except: - logger.info("Shutdown") + if CONFIG['verbose']: + logger.critical('Shutdown') + else: + logger.info("Shutdown") diff --git a/setup.py b/setup.py index dc04e8f..ac33cad 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='aiotunnel', - version='1.1.0', + version='1.2.0', description='HTTP tunnel on top of aiohttp and asyncio', long_description=readme, author='Andrea Giacomo Baldan',