wlvncc icon indicating copy to clipboard operation
wlvncc copied to clipboard

SSH tunnel handling like what tigervnc does

Open je-vv opened this issue 1 year ago • 4 comments

Some remote servers are disallowed to enable remote VNC connections, given not all VNC servers allow encryption (some paid solutions do, but not all). So one must run the server with -localhost to allow only connections from the same machine (no remote connections). So if one wants establish a connection one must use a SSH tunnel. That regardless there's a VPN preventing unencrypted remote access.

In such situations, if not using tiegervnc, one must create the SSH tunnel manually, like:

ssh -NfL 59<num>:127.0.0.1:59<num> <user>@<server>

Or:

ssh -NfL 59<num>:127.0.0.1:59<num> <ssh_host>

Where <ssh_host> is the Host one might define on ~/.ssh/config, specifying for it the full domain name of the server, the user to connect and more, to make SSH related things way easier, by specifying just the defined host.

Notice one needs to find out the SSH process ID for that tunnel, if one wants to kill it with the kill utility. Once the tunnel is up and running, the way to call the client is with:

wlvncc localhost 59<num>

I've alredy tested this BTW.

When creating the VNC session, one can specify the port to be used, which always starts with 59and the reminder of the port number is what one can specify, that's why I'm using 59 as the port number previously.

Creating the tunnel manually can be automated, but killing it requires first finding out the tunnel PID. It's not that complex, but it's not a easy like looking for a SSH process or tunnel, since one might be connected through SSH to remote SSH sessions, or one can actually have different SSH tunnels.

So, tigervnc has made it really easy to create and kill the SSH tunnel for the user, so one doesn't need to deal with SSH manually. All one needs to do with tigervnc is (replacing vincviewer with wlvncc, and also using the wlvncc server and port in different args rather than the common <server>:<port> single arg used by tigervnc client):

wlvncc -via <user>@<server> localhost 59<num>

Or :

wlvncc -via <ssh_defined_remote_host> localhost 59<num>

This way the SSH tunnel is not created neither killed manually, and wlvncc would handle that for the user, just like how tigervnc client does it for the users.

This would be really really useful. And allows using *.desktop files calling xlvncc + wofi for example, very easily.

At this point, actually I prefer to use tigervnc client, which is not wayland native, than wlvncc, just because of the nice SSH tunnel handling provided by tigervnc. This would be really really useful, and would actually allow me to use a wayland native VNC client.

Many thanks !

je-vv avatar Apr 03 '24 05:04 je-vv

It should be possible to create a shell script for this; something like (not tested):

#!/bin/bash

set -e

main()
{
    local host="$1"
    local port="$2"
    shift; shift
    
    ssh -NL 1337:localhost:$port $host &
    local sshpid=$!

    trap "kill $sshpid" INT QUIT TERM EXIT

    wlvncc localhost:1337 $@
}

main $@

Then you'd invoke it like my-vnc-ssh-script.sh remote-host.net 5900

any1 avatar Nov 26 '24 08:11 any1

Just tested the script. I had to add a sleep 3 after the ssh command. Else wlvncc failed with

"Unable to connect to VNC server".

Also it should be "wlvncc localhost 1337" or you get an error:

"ConnectClientToTcpAddr6: getaddrinfo (Name or service not known)"

ckorn avatar Jul 27 '25 01:07 ckorn

It would be more robust to retry the wlvncc command until it succeeds.

It would be nice to have a proper and fully tested script like this in the scripts/ directory. Maybe someone would like to have a go at it? ;)

any1 avatar Aug 17 '25 09:08 any1

This is my current script:

#!/bin/bash

set -e

show_help()
{
    echo "usage: $(basename $BASH_SOURCE) [-p|--ssh-port port] host vnc_port"
    echo
    echo "host: hostname"
    echo "vnc_port: vnc server port at the host"
    echo
    echo "-p|--ssh-port: ssh port at the host. Default 22"
    exit 1
}

free_port()
{
    local start=49152
    local range=5000
    while true; do
        local port=$[$start + ($RANDOM % $range)]
        (echo -n >/dev/tcp/127.0.0.1/${port}) >/dev/null 2>&1
        if [ $? -ne 0 ]; then
            echo $port
            break
        fi
    done
}

main()
{
    POSITIONAL_ARGS=()
    local remote_port="22"
    while [[ $# -gt 0 ]]; do
        case $1 in
          -p|--ssh-port)
            remote_port="$2"
            shift
            shift
            ;;
          -h|--help)
            show_help
            ;;
          -*|--*)
            echo "Unknown option: $1"
            show_help
            exit 1
            ;;
          *)
            POSITIONAL_ARGS+=("$1")
            shift
            ;;
        esac
    done

    set -- "${POSITIONAL_ARGS[@]}"

    local host="$1"
    local port="$2"

    if [ -z "$host" ]; then
        show_help
    fi
    if [ -z "$port" ]; then
        show_help
    fi
    shift; shift

    master_file="$(mktemp -d)/wlvncc"
    port_to_use=$(free_port)
    echo "Using port $port_to_use"
    
    ssh -f -M -p${remote_port} -NL ${port_to_use}:localhost:$port $host -o ControlMaster=yes -o ControlPath=$master_file
    trap "ssh -p $port_to_use -o ControlMaster=no -o ControlPath=$master_file localhost -O exit" INT QUIT TERM EXIT

    wlvncc localhost $port_to_use $@
}

main $@

It uses ssh -f to run in the background. It forks a child process and returns when the connection is established. So no need for sleep here. Problem then is to exit the child process because the $! will only return the parent.

I used the master/slave feature with a control socket to end this process ( https://bugzilla.mindrot.org/show_bug.cgi?id=1473#c5 ).

Also I added the free_port function ( https://unix.stackexchange.com/a/330776 )

//edit Oh, the 35999 is the ssh port I use here. Maybe this also can be a parameter of the script.

//edit2 Added help text and optional argument for the remote ssh port.

ckorn avatar Aug 17 '25 10:08 ckorn