Seamless Remote Access to LAN Apps With SSH, Termux, and Tasker

Posted on November 8, 2022, updated on 2022-12-13 tags: linux, self-hosted

I’ve been using SSH port-forwarding, Termux, and Tasker to remotely access apps I self-host on my LAN like HomeAssistant, Navidrome, and Jellyfin without publicly exposing them. The tunnels run automatically in the background when I’m not on-LAN and stay seamlessly connected.

Update: I don’t use this anymore, I just use WireGuard now and it works much better. At one point in time, using a VPN wasn’t ideal for my situation. I think that my old phone was on a version of Android that didn’t support split tunneling and I didn’t want to tunnel all of my traffic back through my home connection. I set this up and it worked well enough that I never checked to see if split tunneling support was added. I’ll leave this post up because although it’s no longer a great technique, I still think it’s clever (particularly the nginx failover.)

I’ll use HomeAssistant as an example in this post, but any app could work.

Goals

HomeAssistant Location Updates

My primary goal with this project is for HomeAssistant to update my location so that automations can be configured based on my home/away status. The HomeAssistant app integrates with phone location and wifi connectivity to determine whether you’re home or not, but of course it can’t update if your instance is behind your LAN and you’re away.

No Public-Facing Apps

I’m not a security specialist, and I don’t mind moderate inconvenience for peace of mind. Exposing apps publicly is more effort than I’d like to take on (well, maybe not more effort so much as different effort) and I would rather rely on SSH’s security.

Prerequisites

  • Android phone with the Home Assistant app, Tasker, Termux, and the Termux:Tasker plugin 1
  • Server (raspberry pi, etc) with publicly accessible SSH that can access internal apps
  • SSH configured in Termux with SSH keys

Termux Script

Termux will manage the actual SSH connections and this script will open and close the SSH tunnel for each app. We need to make sure only one SSH tunnel is attempted at a time. There are many ways to enforce one-and-only-one instance behavior in Linux, but SSH supports it out of the box with multiplexing and control sockets.

We can open a control-socket controlled SSH instance with ssh -f -N -M -S ctl_path -L local_port:host:host_port ssh_server

  • -f will fork ssh to the background
  • -N opts out of executing a remote command because we’re just using it for the tunnel
  • -M enables connection multiplexing, using a single TCP connection for multiple SSH connections 2
  • -S <ctl_path> enables control sockets and specifies a file to use as a control socket
  • -L local_port:host:host_port sets up a tunnel from the local port to the host server’s host_port. Keep in mind that the host is specified relative to the server being SSH’d into.

The whole script will look something like this:

#!/bin/bash
ctl_socket="$HOME/.ha-tunnel.socket"
my_server="my-ssh-server"
ha_instance="192.168.1.123" # remember, this is relative to your SSH server

echo "Attempting to close previous tunnel..."
ssh -S "$ctl_socket" -O exit $my_server
if [[ "$1" != "down" ]]; then
    echo "Tunneling Home Assistant..."
    ssh -f -N -M -S "$ctl_socket" -L 8123:$ha_instance:8123 $my_server
fi

Save it as ~/.termux/tasker/ha-tunnel.sh (where ~ is your home folder in termux) so that it can be used in Tasker.

Tasker

Tasks

Next we’ll set up a Tasker task named something like “Home Assistant Tunnel Up” with the Termux:Tasker plugin. Add a Termux action by navigating to Plugin > Termux and then typing in ha-tunnel.sh for the script path. 3 Uncheck the option “Execute in Terminal Session” so that it runs in the background. For HomeAssistant, I have an additional action to open the HomeAssistant app in the background so that location gets updated.

Clone the task, rename it “Home Assistant Tunnel Down” then update the Termux Tasker action and add “down” as an argument.

Profiles

On WiFi Disconnect

Add a new Tasker profile named something like “Auto HA Tunnel” with a trigger of State > Wifi Connected and then enter your home wifi. Tick the “Invert” option so that it runs when your phone disconnects. Add the “Home Assistant Tunnel Up” task to the profile, and the “Home Assistant Tunnel Down” as the exit task.

On HomeAssistant App Launch

Tasker can also open the tunnel when the HomeAssistant app is opened when not on wifi. Clone the “On WiFi Disconnect” profile, then long press the “WiFi Connect” profile trigger and select Add > Application and select HomeAssistant.

Note that Opening the tunnel is not instantaneous and the HomeAssistant app will complain about not being able to make a connection for a few seconds.

Shortcut

The “Auto HA Tunnel” profile will automatically bring the tunnel up and down, but you may also want to create a Tasker shortcut that runs the “Home Assistant Tunnel Up” and add it to your home screen in case the automatically started tunnel is closed for some reason. I use NovaLauncher and have it configured to open both the tunnel and open HomeAssistant when I swipe up on the HomeAssistant app icon.

Home Assistant app

In the Home Assistant app, open the Companion App settings by opening the dock on the lift > Configuration > Companion app. Set the “Home Network WiFi SSID” option to your home network wifi SSID. Then set “HomeAssistant URL” (for the external URL) to http://localhost:8123.

nginx Failover

The HomeAssistant app handles internal/external failover natively (and in fact it assumes you will be moving in and out of your LAN) but other apps support only a single server location and have no failover. We can add this capability add-hoc using nginx: nginx has loadbalancing and failover proxy features that will enable seamlessly switch between on-LAN and off-LAN connections.

First, install nginx in Termux with.

Next create a new nginx conf file in its conf.d folder: this is probably at /data/data/com.termux/files/usr/etc/nginx/conf.d, however, I’m not certain that all Termux “root” folders have the same path on every Termux installation.

upstream backend {
    server 192.168.1.123:1234; # Replace with your application's LAN IP and port
    server 127.0.0.1:1234 backup; # Replace with your ssh forwarded port

server {
    listen       45332; # The port that nginx will listen to; must be different than the ssh forwarded port
    server_name  127.0.0.1;

    location / {
        proxy_pass http://backend; # this is not a fake value, it will use the `upstream backend` defined above

        proxy_connect_timeout 1; # timeout in seconds to wait on the primary before failing over to the secondary

        # Usual nginx reverse-proxy options
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # proxy websocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
    }
}

This isn’t a particularly sophisticated failover strategy and relies on the connection timing out to fail over. A proper load balancer with heartbeat would probably be more reactive.

Caveats

Termux runs as a normal app on an unrooted Android phone and is subject to process harvesting just like any app. As a result, the process that’s holding the tunnel open may be killed. For my purposes of periodically updating HomeAssistant this isn’t an issue, but for apps that require a stable connection this may be a deal-breaker.

Additionally, SSH is notoriously fragile to connection changes. It’s possble that the tunnel will be lost when moving between networks.

BONUS: Almost-Automatic SOCKS Proxy

If you use FireFox on Android, we can extend this technique to open and close SOCKS proxies as well. With the FoxyProxy extension, we can use the SSH SOCKS proxy on specific routes.

Could maybe run srelay in termux and have FoxyProxy always enabled. Start srelay when on LAN, kill it and start SSH SOCKS when off LAN


  1. Available on FDroid as well (please throw the dev a buck or two if you’re able).↩︎

  2. Hat tip to oooops.dev for a great primer and resource on SSH multiplexing and socket-control: https://oooops.dev/2021/01/31/ssh-multiplexing-and-master-mode/↩︎

  3. Note that Termux:Tasker only has access to scripts in the ~/.termux/tasker folder.↩︎