Information wants to be free...

Reverse SSH Tunnel Launcher

Here is a script which helps with starting a reverse SSH tunnel. It gets the instructions from a web server, in the form of a JSON file, which determines at what time to open the tunnel(s). The JSON file also determines when to check for a JSON file next time. It operates on a daily schedule and uses the HH:MM format.

The idea is to leave this script running on a box behind a firewall. It is hardcoded to only keep the tunnel open for 5 minutes before closing it again. So to get a connection that lasts longer, a new tunnel probably has to be made manually after connecting.

The Python code:

#!/usr/bin/python

import urllib2
import json
import subprocess
import signal
import datetime
import time
import logging
import sys

config_url = "http://192.168.0.1/config.json"
ssh_host = "192.168.0.1"
ssh_port = 22
local_port = 1337
tunnel_open_time = 300 # In seconds.

tunnel = None

def alarm_handler(signum, frame):
    global tunnel
    logger.info("Terminating current tunnel.")
    tunnel.kill();
    tunnel = None

def minutes_until(minute_ts):
    if minute_ts == None:
        return (24 * 60)

    now = datetime.datetime.now().timetuple()
    now_ts = (now.tm_hour * 60) + now.tm_min

    if now_ts > minute_ts:
        # Already passed, will be next day.
        return minute_ts - now_ts + (24 * 60)
    else:
        return minute_ts - now_ts

def update_config():
    try:
        config = json.load(urllib2.urlopen(config_url))

        # Pick closest time to update next time:
        closest_config_ts = None
        for uc in config["ConfigUpdate"]:
            config_ts = (int(uc.split(":")[0]) * 60) + int(uc.split(":")[1])
            if minutes_until(config_ts) < minutes_until(closest_config_ts):
                closest_config_ts = config_ts

        tunnel_ts = list()
        for to in config["TunnelOpen"]:
            tunnel_ts.append((int(to.split(":")[0]) * 60) + int(to.split(":")[1]))

        logger.debug("Config timestamp: %d" % (closest_config_ts))
        logger.debug("Tunnel timestamps: %s" % (str(tunnel_ts)))

        return closest_config_ts, tunnel_ts

    except Exception, e:
        logger.error("Exception during config update: %s" % (e))
        return 0, []

if __name__ == "__main__":
    logger = logging.getLogger('reverse_tunnel')
    logger.setLevel(logging.DEBUG)
    logger.addHandler(logging.StreamHandler())

    # Get initial configuration:
    config_ts, tunnel_ts = update_config()
    if len(tunnel_ts) == 0:
        sys.exit(1) # Exit right away if initial configuration fails.

    signal.signal(signal.SIGALRM, alarm_handler)

    while True:
        now = datetime.datetime.now().timetuple()
        now_ts = (now.tm_hour * 60) + now.tm_min

        # Is it time for a config update?
        if now_ts == config_ts:
            logger.info("Timed config update.")
            config_ts, tunnel_ts = update_config()
            if config_ts == now_ts:
                config_ts = config_ts - 2 # Prevent immediate update.

        # Is it time to open a new tunnel?
        for ts in tunnel_ts:
            if now_ts == ts:
                if tunnel == None:
                    logger.info("Opening new tunnel.")
                    tunnel = subprocess.Popen(["ssh", "-R", str(local_port) + ":localhost:22", "-N", "-p", str(ssh_port), ssh_host])
                    signal.alarm(tunnel_open_time)
                else:
                    logger.warning("Tunnel already running.")

        time.sleep(10)
          


Example JSON file:

{
  "ConfigUpdate" : ["17:00"],
  "TunnelOpen" : ["18:00","19:00","20:00"]
}
          


Topic: Scripts and Code, by Kjetil @ 29/09-2018, Article Link