├── LICENSE ├── README.md ├── caddy_config.json ├── create_tunnel_example.sh ├── install.sh ├── run_server.sh └── sirtunnel.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anders Pitman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is it? 2 | 3 | If you have a webserver running on one computer (say your development laptop), 4 | and you want to expose it securely (ie HTTPS) via a public URL, SirTunnel 5 | allows you to easily do that. 6 | 7 | # How do you use it? 8 | 9 | If you have: 10 | 11 | * A SirTunnel [server instance](#running-the-server) listening on port 443 of 12 | `example.com`. 13 | * A copy of the sirtunnel.py script available on the PATH of the server. 14 | * An SSH server running on port 22 of `example.com`. 15 | * A webserver running on port 8080 of your laptop. 16 | 17 | And you run the following command on your laptop: 18 | 19 | ```bash 20 | ssh -tR 9001:localhost:8080 example.com sirtunnel.py sub1.example.com 9001 21 | ``` 22 | 23 | Now any requests to `https://sub1.example.com` will be proxied to your local 24 | webserver. 25 | 26 | 27 | # How does it work? 28 | 29 | The command above does 2 things: 30 | 31 | 1. It starts a standard [remote SSH tunnel][2] from the server port 9001 to 32 | local port 8080. 33 | 2. It runs the command `sirtunnel.py sub1.example.com 9001` on the server. 34 | The python script parses `sub1.example.com 9001` and uses the Caddy API to 35 | reverse proxy `sub1.example.com` to port 9001 on the server. Caddy 36 | automatically retrieves an HTTPS cert for `sub1.example.com`. 37 | 38 | **Note:** The `-t` is necessary so that doing CTRL-C on your laptop stops the 39 | `sirtunnel.py` command on the server, which allows it to clean up the tunnel 40 | on Caddy. Otherwise it would leave `sirtunnel.py` running and just kill your 41 | SSH tunnel locally. 42 | 43 | 44 | # How is it different? 45 | 46 | There are a lot of solutions to this problem. In fact, I've made something of 47 | a hobby of maintaining [a list][0] of the ones I've found so far. 48 | 49 | The main advantages of SirTunnel are: 50 | 51 | * Minimal. It leverages [Caddy][1] and whatever SSH server you already have 52 | running on your server. Other than that, it consists of a 50-line Python 53 | script on the server. That's it. Any time you spend learning to customize 54 | and configure it will be time well spent because you're learning Caddy and 55 | your SSH server. 56 | * 0-configuration. There is no configuration on the server side. Not even CLI 57 | arguments. 58 | * Essentially stateless. The only state is the certs (which is handled entirely 59 | by Caddy) and the tunnel mappings, which are ephemeral and controlled by the 60 | clients. 61 | * Automatic HTTPS certificate management. Some other solutions do this as well, 62 | so it's important but not unique. 63 | * No special client is required. You can use any standard SSH client that 64 | supports remote tunnels. Again, this is not a unique feature. 65 | 66 | 67 | # Running the server 68 | 69 | Assuming you already have an ssh server running, getting the SirTunnel server 70 | going consists of simply downloading a copy of Caddy and running it with the 71 | provided config. Take a look at [`install.sh`](./install.sh) and 72 | [`run_server.sh`](./run_server.sh) for details. 73 | 74 | **Note:** Caddy needs to bind to port 443, either by running as root (not 75 | recommended), setting the `CAP_NET_BIND_SERVICE` capability on the Caddy binary 76 | (what the `install.sh` script does), or changing `caddy_config.json` to bind 77 | to a different port (say 9000) and using something like iptables to forward 78 | to that port. 79 | 80 | # Future Features 81 | 82 | SirTunnel is intended to be a minimal tool. As such, I'm unlikely to add many 83 | features moving forward. However, the simplicity makes it easier to modify 84 | for your needs. For example, see this fork which adds functionality to help 85 | multiple users avoid overwriting each others' tunnels: 86 | 87 | https://github.com/matiboy/SirTunnel 88 | 89 | 90 | [0]: https://github.com/anderspitman/awesome-tunneling 91 | 92 | [1]: https://caddyserver.com/ 93 | 94 | [2]: https://www.ssh.com/ssh/tunneling/example#remote-forwarding 95 | -------------------------------------------------------------------------------- /caddy_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "http": { 4 | "servers": { 5 | "sirtunnel": { 6 | "listen": [":443"], 7 | "routes": [ 8 | ] 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /create_tunnel_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | domain=$1 4 | serverPort=$2 5 | localPort=$3 6 | 7 | ssh -t -R $serverPort:localhost:$localPort $domain sirtunnel $domain $serverPort 8 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | caddyVersion=2.1.1 4 | 5 | echo Download Caddy 6 | caddyGz=caddy_${caddyVersion}_linux_amd64.tar.gz 7 | curl -s -O -L https://github.com/caddyserver/caddy/releases/download/v${caddyVersion}/${caddyGz} 8 | tar xf ${caddyGz} 9 | 10 | echo Clean up extra Caddy files 11 | rm ${caddyGz} 12 | rm LICENSE 13 | rm README.md 14 | 15 | echo Enable Caddy to bind low ports 16 | sudo setcap 'cap_net_bind_service=+ep' caddy 17 | -------------------------------------------------------------------------------- /run_server.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | ./caddy run --config caddy_config.json 4 | -------------------------------------------------------------------------------- /sirtunnel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import json 5 | import time 6 | from urllib import request, error 7 | import argparse 8 | import subprocess 9 | 10 | if __name__ == '__main__': 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('--tls', action='store_true') 13 | parser.add_argument('--insecure', action='store_true') 14 | parser.add_argument('--user', type=str) 15 | parser.add_argument('--password', type=str) 16 | parser.add_argument('host') 17 | parser.add_argument('port') 18 | args = parser.parse_args(sys.argv[1:]) 19 | 20 | host = args.host 21 | port = args.port 22 | tunnel_id = host + '-' + port 23 | 24 | def cleanup(): 25 | delete_url = 'http://127.0.0.1:2019/id/' + tunnel_id 26 | req = request.Request(method='DELETE', url=delete_url) 27 | try: 28 | request.urlopen(req) 29 | return True 30 | except error.HTTPError as e: 31 | if e.code == 404: 32 | return False 33 | else: 34 | raise 35 | 36 | print('Cleaning up any potential stale registrations') 37 | while cleanup(): pass 38 | 39 | caddy_add_route_request = { 40 | "@id": tunnel_id, 41 | "match": [{ 42 | "host": [host], 43 | }], 44 | "handle": [{ 45 | "handler": "reverse_proxy", 46 | "upstreams":[{ 47 | "dial": ':' + port 48 | }] 49 | }] 50 | } 51 | 52 | if args.tls: 53 | tls_options = {} 54 | if args.insecure: 55 | tls_options['insecure_skip_verify'] = True 56 | caddy_add_route_request['handle'][0]['transport'] = {"protocol": "http", "tls": tls_options} 57 | 58 | if args.user: 59 | password = args.password or '' 60 | hashed_password = subprocess.check_output( 61 | ['caddy', 'hash-password', '-plaintext', password], 62 | text=True).strip() 63 | caddy_add_route_request['handle'].insert(0, { 64 | "handler": "authentication", 65 | "providers": { 66 | "http_basic": { 67 | "accounts": [{ "username": args.user, "password": hashed_password }], 68 | "hash_cache": {} 69 | } 70 | } 71 | }) 72 | 73 | body = json.dumps(caddy_add_route_request).encode('utf-8') 74 | headers = { 75 | 'Content-Type': 'application/json' 76 | } 77 | create_url = 'http://127.0.0.1:2019/config/apps/http/servers/default/routes' 78 | req = request.Request(method='POST', url=create_url, headers=headers) 79 | request.urlopen(req, body) 80 | 81 | print(f"Tunnel {host} created successfully") 82 | 83 | while True: 84 | try: 85 | time.sleep(1) 86 | except KeyboardInterrupt: 87 | print("Cleaning up tunnel") 88 | cleanup() 89 | break 90 | --------------------------------------------------------------------------------