Skip to content

Instantly share code, notes, and snippets.

@seahorsepip
Last active March 22, 2024 15:30
Show Gist options
  • Save seahorsepip/5bc644a850d2ac3c62c9da521eabbecd to your computer and use it in GitHub Desktop.
Save seahorsepip/5bc644a850d2ac3c62c9da521eabbecd to your computer and use it in GitHub Desktop.
Reverse SSH tunnel NGINX config script

To be used with /etc/ssh/sshd_config ForceCommand.

Requirements:

Example usage:

#!/bin/bash
clear
declare -A parsed=()
function rand () {
head /dev/urandom | tr -dc a-z | head -c 3 ; echo ''
}
function get_port () {
tail /var/log/auth.log --lines 200 \
| grep 'Local forwarding listening on 127.0.0.1 port' \
| tail -n1 \
| ([[ $(cat) =~ .*[[:space:]]([[:digit:]]*).* ]]; echo ${BASH_REMATCH[1]})
}
function get_name () {
tail /var/log/auth.log --lines 200 \
| grep 'server_input_global_request: tcpip-forward listen' \
| tail -n1 \
| ([[ $(cat) =~ .*[[:space:]]listen[[:space:]](.*)[[:space:]]port.* ]]; echo ${BASH_REMATCH[1]}) \
| tr -s '/'
}
function parse () {
IFS='#' read -ra servers <<< "${config//\}\}/#}"
parsed[.length]=${#servers[@]}
for i in "${!servers[@]}"
do
IFS='#' read -ra locations <<< "${servers[i]//location /#}"
local server_name=${locations:20:-12}
unset "locations[0]"
locations=("${locations[@]}")
parsed[$server_name]=$i
parsed[$i]=$server_name
parsed[$i.length]=${#locations[@]}
for j in "${!locations[@]}"
do
[[ ${locations[$j]} =~ (.*)[[:space:]]\{proxy_pass[[:space:]].*:(.*)\; ]]
local location=${BASH_REMATCH[1]}
local proxy_pass=${BASH_REMATCH[2]}
if [ ${#location} -gt 2 ]
then
location=${location:1:-6}
proxy_pass=${proxy_pass:0:-2}
fi
parsed[$i.$location]=${j}
parsed[$i.$j.location]=$location
parsed[$i.$j.port]=$proxy_pass
done
done
}
function sort_locations () {\
for ((i=0; i <= $((${#locations[@]} - 2)); ++i))
do
for ((j=((i + 1)); j <= ((${#locations[@]} - 1)); ++j))
do
a=${locations[i]}
if [ ${#a} -eq 1 ]
then
a=''
fi
a=${a//[^\/]}
b=${locations[j]}
if [ ${#b} -eq 1 ]
then
b=''
fi
b=${b//[^\/]}
if [ ${#b} -gt ${#a} ]
then
tmp=${locations[i]}
locations[i]=${locations[j]}
locations[j]=$tmp
fi
done
done
}
function stringify () {
config=''
for i in $(seq 0 $((${parsed[.length]} - 1)))
do
if [ -n "${parsed[$i]}" ]
then
config+="server {server_name ${parsed[$i]};listen 443;"
locations=()
declare -A ports=()
for j in $(seq 0 $((${parsed[$i.length]} - 1)))
do
if [ -n "${parsed[$i.$j.location]}" ]
then
locations[$j]=${parsed[$i.$j.location]}
ports[${parsed[$i.$j.location]}]=${parsed[$i.$j.port]}
fi
done
sort_locations
local location
for location in "${locations[@]}"
do
if [ -n "$location" ]
then
local proxy_pass="http://127.0.0.1:${ports[$location]}"
if [ ${#location} -gt 1 ]
then
location="~$location(/.*)\$"
proxy_pass+="\$1\$is_args\$args"
fi
config+="location $location {proxy_pass $proxy_pass;}"
fi
done
config+="}"
fi
done
}
function add () {
local i=${parsed[$server_name]}
if [ -z "$i" ]
then
i=${parsed[.length]}
parsed[.length]=$(($i + 1))
parsed[$server_name]=$i
parsed[$i]=$server_name
fi
if [ -n "${parsed[$i.$location]}" ]
then
echo "The web address $url is already in use."
exit 1
fi
local j=${parsed[$i.length]}
if [ -z "$j" ]
then
j=0
fi
parsed[$i.length]=$(($j + 1))
parsed[$i.$location]=$j
parsed[$i.$j.location]=$location
parsed[$i.$j.port]=$port
}
function remove () {
local i=${parsed[$server_name]}
local j=${parsed[$i.$location]}
unset "parsed[$i.$j.location]"
if [ ${parsed[$i.length]} -eq 1 ]
then
unset "parsed[$i]"
fi
}
function load () {
config=$(cat $file \
| tr -d '\n\r' \
| tr -s ' ')
}
function store () {
echo -e "$config" > $file
}
file='/etc/nginx/sites-available/tunnel.seapip.com'
name=$(get_name)
[[ "$name" =~ \/?([^\/]*)\/?(.*)\/? ]]
server_name=${BASH_REMATCH[1]}
location="/${BASH_REMATCH[2]}"
if [ "$server_name" == 'localhost' ]
then
server_name=$(rand)
fi
server_name+=".seapip.com"
port=$(get_port)
url="https://$server_name"
if [ ${#location} -gt 1 ]
then
url+="$location/"
fi
load && parse && add && stringify && store
trap "load && parse && remove && stringify && store" EXIT
echo "$url"
read -r -d '' _ </dev/tty
@ekawahyu
Copy link

This setup seems to require username for reverse tunneling, like so:

ssh -R 0:127.0.0.1:6000 -l tunnel example.com

In this case, the username is tunnel. How did you setup SSH server so that you don't need to pass -l <user> in your reverse tunnel examples? Or, maybe, you configured SSH client to use default username?

Thank you for this awesome script!

@seahorsepip
Copy link
Author

This setup seems to require username for reverse tunneling, like so:

ssh -R 0:127.0.0.1:6000 -l tunnel example.com

In this case, the username is tunnel. How did you setup SSH server so that you don't need to pass -l <user> in your reverse tunnel examples? Or, maybe, you configured SSH client to use default username?

Thank you for this awesome script!

I didn't include details regarding this, I tend to use the myusername@myserver.com syntax over the -l param.

@ekawahyu
Copy link

Thank you for confirming. I thought you had figured out a way to create the tunnel with no authentication whatsoever, just like Serveo does. I am still wondering how it can be done that way.

@seahorsepip
Copy link
Author

seahorsepip commented Jul 14, 2023

I would highly recommend to not remove authentication 😅

Instead you can setup authentication with an ssh key instead of password so no input is required and if you don't want to pass a username either, you can try to use a user called "root" since most ssh clients will use that by default. But please make sure to use strong ssh key authentication!

(Serveo probably is a custom ssh server, it could be done with this script and a root user with no password or key and all permissions restricted but I wouldn't recommend making your server publicly available)

@ekawahyu
Copy link

Well, user tunnel in my case, is set as passwordless and no PAM. Correct me if I am wrong, but with the ForceCommand set for user tunnel, there is no chance it can get into the shell, right? Unless there is other means of getting into the shell that I don't know about.

@seahorsepip
Copy link
Author

Well, user tunnel in my case, is set as passwordless and no PAM. Correct me if I am wrong, but with the ForceCommand set for user tunnel, there is no chance it can get into the shell, right? Unless there is other means of getting into the shell that I don't know about.

There have been some ways to get around it and I think you might still be able connect with sftp, so you'll need to limit file access correctly etc. I would not rely on force command to make things secure.

@virtueer
Copy link

Thanks for this pretty script. I was wondering is there any other way to get ssh args without reading auth.log file?

@seahorsepip
Copy link
Author

Thanks for this pretty script. I was wondering is there any other way to get ssh args without reading auth.log file?

No idea, I'm far from knowledgeable with ssh scripting, this implementation was done based on throwing together ideas from multiple sources 😅

If you do find a way, let me know, the current log file approach is rather ugly and requires giving acces to the file :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment