Skip to content

Commit

Permalink
Add support for SSL/TLS communication
Browse files Browse the repository at this point in the history
  • Loading branch information
codepr committed Oct 17, 2018
1 parent e2077b9 commit ba28283
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
(Oct 18, 2018)

- Added basic configuration
- Support for SSL/TLS communication
- Verbose flag

(Oct 14, 2018)
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion aiotunnel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import json
import logging

__version__ = '1.1.0'
__version__ = '1.2.0'

CONFIG = {
'logpath': './',
Expand Down
22 changes: 19 additions & 3 deletions aiotunnel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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']
Expand All @@ -93,13 +106,16 @@ 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
set_config_key('server', {'host': server_host})
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)
14 changes: 8 additions & 6 deletions aiotunnel/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -123,15 +124,16 @@ 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())

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)
Expand All @@ -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)
Expand All @@ -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):
Expand Down
26 changes: 17 additions & 9 deletions aiotunnel/tunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
17 changes: 10 additions & 7 deletions aiotunnel/tunneld.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

from aiohttp import web

from . import CONFIG
from .protocol import TunnelProtocol


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit ba28283

Please sign in to comment.