1. August 2021

In diesem Artikel zeigen wir eine mögliche Vorgehensweise, um von einem Linux-Rechner einen anderen fernzusteuern, ohne dabei einen zentralen Server zu verwenden. Dafür verwenden wir einen Wireguard-Tunnel und xpra. Um die Benutzung einfach zu halten, stellen wir hierfür zwei Skripte zur Verfügung.
Xpra agiert hierbei als die Fernsteuerungswerkzeug. Es hat allerdings die Einschränkung, dass der Hilfegebende die Verbindung zum Hilfenehmenden aufbauen muss. Um diese Verbindungsaufbaurichtung umzukehren, verwenden wir die Netzwerktunnellösung Wireguard. Damit verbindet sich der Hilfenehmer zunächst mit dem Hilfegeber, damit der Hilfegeber sich dann über diesen Tunnel mit xpra beim Hilfenehmer verbinden kann. Diese Vorgehensweise würde auch mit anderen Fernsteuerungstechnologien wie SSH oder einem VNC-Server funktionieren, aber wir zeigen es mit xpra.
Einschränkungen
Um falsche Erwartungen zu vermeiden gehen wir zuerst darauf ein, welche Probleme wir hier nicht lösen.
X-Server
xpra kann nur grafische Oberflächen auf X-Server-Basis weiterleiten. Wer keine grafische Oberfläche verwendet wird das sicherlich genau wissen. Beim X-Server und Wayland ist nicht immer klar, was man verwendet - insbesondere, da einige Desktop Environments beides unterstützten und in beiden Fällen genau gleich aussehen.
Als einfachen und universellen Test gibt es das Programm xeyes
(bei Debian im Paket x11-apps
).
Dieses Programm zeigt zwei Augen, die zum Mauszeiger gucken. Das funktioniert (also reagiert auf Mausbewegungen)
beim X-Server jederzeit (d.h. unabhängig von der Anwendung über der der Mauszeiger ist und auch bei Bedienelementen vom Desktop Environment).
Wenn xeyes
gar nicht gestartet werden kann, ist das ein Indikator dafür, dass man gar keinen X-Server hat. Das ist allerdings noch unüblich.
Die dritte Möglichkeit ist, dass man Wayland mit X-Kompatibilitätsmodus (XWayland) verwendet. Dann bewegen sich die Augen nur, wenn der Mauszeiger
über einer X-Anwendung ist (also z.B. xeyes
selbst), aber sonst nicht. In diesem Fall könnte man xpra zwar starten, aber dann würde nur ein schwarzes
Bild übertragen werden.
Teilweise kann man im Displaymanager auswählen, ob man eine X-Server oder Wayland-Sitzung starten möchte. Das ist dann natürlich nur ein Workaround, da das Problem an einer anderen Stelle liegt.
Netzwerk
Die netzwerkabhängigen Einschränkungen ergeben sich dadurch, dass man keinen zentralen Zwischenserver hat.
Der Hilfenehmende (B) muss eine Verbindung zum Hilfegebenden (A) aufbauen können. Wenn man das über das Internet und nicht nur in seinem Heimnetz - was dann wahrscheinlich keine Fernhilfe mehr ist - macht, dann muss eine Verbindung möglich sein. Dabei kann es durch die zwei verschiedenen, momentan eingesetzten Protokollversionen von IP - IPv4 und IPv6 - zu Problemen kommen.
Am Besten ist es, wenn A ein Provider hat, der einen sog. Dual-Stack-Betrieb durchführt. Dann kann A uneingeschränkt IPv4 und Pv6 gleichzeitig verwenden. Ansonsten hat A nur eine öffentliche IPv4 oder IPv6-Adresse. Dann können an der Seite von B Tunnelprotokolle verwendet werden, falls der Provider von B nur die andere Version unterstützt. Einfacher ist es natürlich, wenn man nicht auf Tunnelprotokolle zurückgreifen muss.
Beim Einsatz von virtuellen Maschinen kann es vorkommen, dass der eigene Rechner selbst auch als Router agiert. Wenn dabei eine Paketfilterung oder eine Network Address Translation stattfindet, dann muss man auch das entsprechend konfigurieren, um als A eingehene Verbindungen annehmen zu können.
Konzept
Wir bauen einen Wireguard-Tunnel vom Hilfenehmenden (B) zum Hilfegebenden (A) auf. Dafür muss B die IP-Adresse von A kennen. Sofern A nicht direkt am Internet angebunden ist, muss A den konfigurierten UDP-Port vom Router an den entsprechenden Rechner weiterleiten lassen.
Dann startet B einen xpra-Server, der über den Tunnel erreichbar ist. A verbindet sich mit diesem und dann haben wir unser Ziel erreicht.
Damit es bequem benutzbar wird, stellen wir hierfür zwei Skripte zur Verfügung.
Um die Benutzung für B besonders einfach zu halten, wird B nur nach der IP-Adresse von A gefragt. Der Public-Key und Port von A wird bei B fest konfiguriert. A wird stattdessen nach dem Public Key von B gefragt, damit diese Verbindung zugelassen wird. A muss die Public-Keys selbst “verwalten”, also z.B. eine Liste mit Namen und den zugehörigen Public Keys erstellen.
Portweiterleitung
Wie schon im Konzept angedeutet, muss der Helfende seinen Router passend konfigurieren, damit die Verbindung von außen an seinen Computer möglich wird. Die Portnummer muss mit der im Skript hinterlegten übereinstimmen. Sofern der Router eine Wahl anbietet, reicht eine UDP-Weiterleitung. Eine Schritt-für-Schritt-Anleitung haben wir hier nicht, aber die wäre auch nur begrenzt hilfreich, da die Oberfläche bei jedem Router anders aufgebaut ist.
Durch die Portweiterleitung entsteht kein Sicherheitsproblem. Solange kein Programm auf eurem Rechner den Port bindet, kann man über diesen Port Nichts von außen erreichen. Wenn wiederum Wireguard läuft, dann reagiert es nur auf Pakete, die eine Kenntnis des eigenen Public-Keys zeigen.
Bei zu großen Bedenken kann die Portweiterleitung auch bei Bedarf aktiviert werden und danach wieder deaktiviert werden.
Skripte
Voraussetzungen
benötige Anwendungen:
- xpra
- wireguard (d.h. wireguard-tools und Kernel-Modul, bei Debian im Paket wireguard)
- ping (z.B. aus dem Paket iputils-ping)
- ip (aus dem Paket iproute2)
- sudo (ohne sudo muss das Skript bearbeitet werden, um anders vom root zurück zum Aufruferbenutzer zu wechseln)
optionale Anwendungen:
- miniupnpc (ermöglicht das Anzeigen der externen IPv4-Adresse)
- curl (ermöglicht die Angabe der IP-Adresse per Datei auf einem Webserver)
Konfiguration
Die Konfiguration erfolgt durch das Setzen von Variablen am Anfang der beiden Skripte. Wie man neue Werte für die Schlüssel und Ports generiert wird in Kommentaren im Skript erläutert, sodass es an dieser Stelle nicht wiederholt wird.
Da private Schlüssel im Skript eingebettet werden, ist es empfehlenswert, die Dateiberechtigungen so zu setzten, dass nur der entsprechende Benutzer diese Datei lesen kann.
Das Skript für den Hilfenehmenden ist so gebaut, dass es wenig Fragen stellt. Was das Skript nicht fragt, muss es schon wissen. Das bedeutet mehr Konfiguration, die im Voraus erfolgen muss. Es ist also dafür gedacht, dass der Hilfegebende es einmalig beim Hilfenehmenden (z.B. vor Ort) einrichtet.
Sicherheit
Sämtliche Daten werden durch einen Wireguard-Tunnel übertragen, um die Vertrautlichkeit und Integrität sicherzustellen. xpra wird so konfiguriert, dass die Verbindung in einem Bestätigungsdialog zugelassen werden muss. Zusätzlich wird xpra so konfiguriert, dass es nur über den Tunnel erreichbar ist. Die Skripte benötigen root-Rechte, um Wireguard zu konfigurieren, aber für den xpra-Aufruf wird per sudo zurück zum nicht-root-Benutzer gewechselt.
connect-to-xpra-session.sh
Dieses Skript wird vom Hilfegebenden (also A) ausgeführt.
#!/usr/bin/env bash
set -euo pipefail
# use "wg genkey" to generate this one
# mit "wg genkey" kann dieser Wert generiert werden
MY_PRIVKEY="oMOhNurb+WKDlhymjaMNYQgLNCERg3DsaFcP3J7jOXg="
# use "echo $((50000 + $RANDOM % 1000))" to generate this one
# when configuring your firewall/router, you only need to allow the UDP port with
# this number, TCP is not needed/used
#
# mit "echo $((50000 + $RANDOM % 1000))" kann ein Wert hierfür generiert werden
# Bei der Konfiguration der Firewall/des Routers muss nur dieser Port und auch nur
# für UDP freigegeben werden; TCP wird nicht verwendet
HELPER_PORT=50831
# end of important configuration/Ende der wichtigen Konfiguration
INTERFACE_NAME="helpnet0"
# end of configuration/Ende der Konfiguration
NEEDS_WG_SHUTDOWN=0
wg_pubkey_to_ip() {
# Use this function to deterministically calculate an IP address from a wiregurd
# public key. This uses a address range which we chose randomly and which is intended
# for such purposes.
#
# Diese Funktion berechnet eine IP-Adresse mit einem Wireguard-Public-Key. Der dafür
# verwendete Adressbereich wurde zufällig gewählt ist für solche Einsatzzwecke vorgesehen.
#
# parameter 1: base64 encoded public key
#
# fd = private and generated ipv6 network
# 70cc7ee212 = global id
# 19b8 = subnet id
#
# the remaining four blocks are the start of the public key converted to hex
# die verbleibenden vier Blöcke entsprechen dem Anfang des Public-Keys in Hexadezimalkodierung
printf "%s" 'fd70:cc7e:e212:19b8:'; echo "$1" | base64 --decode | od -An -N 8 -t x1 | tr -cd [0-9a-f] | cut --output-delimiter=: -c1-4,5-8,9-12,13-16
}
setup_wg() {
ip link add dev "$INTERFACE_NAME" type wireguard
NEEDS_WG_SHUTDOWN=1
echo "$MY_PRIVKEY" | wg set "$INTERFACE_NAME" listen-port "$HELPER_PORT" private-key /dev/stdin
ip address add "$(wg_pubkey_to_ip "$(echo "$MY_PRIVKEY" | wg pubkey)")/64" dev "$INTERFACE_NAME"
wg set "$INTERFACE_NAME" peer "$HELPED_PUBKEY" allowed-ips "$(wg_pubkey_to_ip "$HELPED_PUBKEY")"
ip link set "$INTERFACE_NAME" up
}
shutdown_wg() {
if [ $NEEDS_WG_SHUTDOWN -eq 1 ]; then
ip link delete dev "$INTERFACE_NAME"
fi
NEEDS_WG_SHUTDOWN=0
}
cleanup() {
shutdown_wg
}
trap cleanup EXIT
if [[ ! -v SUDO_USER ]]; then
echo 'Please start this script with sudo, it will drop the permissions where possible'
exit 1
fi
read -rep 'Public key of the other side: ' HELPED_PUBKEY
setup_wg
echo 'Output of "ip address":'
ip address | grep global | awk '{ v = $2; gsub(/\/.*$/, "", v); print v }'
if which external-ip > /dev/null; then
echo 'Output of "external-ip":'
sudo -u nobody external-ip # don't run this as root
else
echo 'If you install miniupnpc, then your public IPv4 address could be shown here too'
fi
echo 'Now tell the other side your IP and ask it to connect'
read -sp 'Press ENTER when the other side seems to be ready'; echo ''
echo 'Now we send pings until they work before we try to start the session'
while true; do
OK=1
ping -c1 -w1 "$(wg_pubkey_to_ip "$HELPED_PUBKEY")" || OK=0
[ $OK -eq 1 ] && break
echo 'No connection yet ...'
sleep 1
done
echo 'Connected!' && sleep 1
sudo -u "$SUDO_USER" xpra attach "tcp/$(wg_pubkey_to_ip "$HELPED_PUBKEY")" --encoding=vp8
echo 'Shutting down ...'
cleanup
start-xpra-session.sh
Dieses Skript wird vom Hilfenehmenden (also B) ausgeführt.
#!/usr/bin/env bash
set -euo pipefail
# use "wg genkey" to generate this one
# mit "wg genkey" kann dieser Wert generiert werden
MY_PRIVKEY="IEO9hOluqqfon8oG/BW6yQ4//miO4PsEOeWwHR0L4kE="
# use "echo [keydata] | wg pubkey" to get the public key; this is the public
# key of the other side, otherwise we could just use the MY_PRIVKEY variable
# to calculate it
#
# Mit "echo [keydata] | wg pubkey" kann der öffentliche Schlüssel berechnet werden;
# Hier muss der Schlüssel der ANDEREN Seite verwendet werden, sonst könnte es ja
# auch direkt vom Skript berechnet werden
HELPER_PUBKEY="028Y/kcgJyR5wOiL1XWwipzHomSy98Wxgrz0cZkWgBY="
# The URL which serves a text file with the IP address of the helper.
# If this is empty or the URL does not work, then the user is asked for the value.
#
# Eine URL, unter der die IP-Adresse des Helfers ausgeliefert wird.
# Wenn das leer ist oder die URL nicht funktioniert, dann wird der Benutzer nach dem
# Wert gefragt.
HELPER_IP_URL=""
# use the value which the helper uses to make it work
# Hier muss der gleiche Port wie beim Helfenden eingestellt werden, sonst funktioniert es nicht
HELPER_WG_PORT=50831
# end of important configuration/Ende der wichtigen Konfiguration
SELF_WG_PORT="$((50000 + $RANDOM % 1000))"
INTERFACE_NAME="helpnet0"
# end of configuration/Ende der Konfiguration
NEEDS_WG_SHUTDOWN=0
NEEDS_XPRA_SHUTDOWN=0
XPRA_PID=0
wg_pubkey_to_ip() {
# Use this function to deterministically calculate an IP address from a wiregurd
# public key. This uses a address range which we chose randomly and which is intended
# for such purposes.
#
# Diese Funktion berechnet eine IP-Adresse mit einem Wireguard-Public-Key. Der dafür
# verwendete Adressbereich wurde zufällig gewählt ist für solche Einsatzzwecke vorgesehen.
#
# parameter 1: base64 encoded public key
#
# fd = private and generated ipv6 network
# 70cc7ee212 = global id
# 19b8 = subnet id
#
# the remaining four blocks are the start of the public key converted to hex
# die verbleibenden vier Blöcke entsprechen dem Anfang des Public-Keys in Hexadezimalkodierung
printf "%s" 'fd70:cc7e:e212:19b8:'; echo "$1" | base64 --decode | od -An -N 8 -t x1 | tr -cd [0-9a-f] | cut --output-delimiter=: -c1-4,5-8,9-12,13-16
}
setup_wg() {
ip link add dev "$INTERFACE_NAME" type wireguard
NEEDS_WG_SHUTDOWN=1
echo "$MY_PRIVKEY" | wg set "$INTERFACE_NAME" listen-port "$SELF_WG_PORT" private-key /dev/stdin
ip address add "$(wg_pubkey_to_ip "$(echo "$MY_PRIVKEY" | wg pubkey)")/64" dev "$INTERFACE_NAME"
wg set "$INTERFACE_NAME" peer "$HELPER_PUBKEY" allowed-ips "$(wg_pubkey_to_ip "$HELPER_PUBKEY")" endpoint "${HELPER_IP}:${HELPER_WG_PORT}" persistent-keepalive 5
ip link set "$INTERFACE_NAME" up
}
shutdown_wg() {
if [ $NEEDS_WG_SHUTDOWN -eq 1 ]; then
ip link delete dev "$INTERFACE_NAME"
fi
NEEDS_WG_SHUTDOWN=0
}
setup_xpra() {
# the pid file must be created by the user that runs xpra
XPRA_PID_FILE="$(sudo -u "$SUDO_USER" mktemp)"
sudo -u "$SUDO_USER" xpra shadow --daemon=no --mdns=no --bind=none "--bind-tcp=[$(wg_pubkey_to_ip "$(echo "$MY_PRIVKEY" | wg pubkey)")]:14500" --tcp-auth=exec "--pidfile=$XPRA_PID_FILE" &
for i in {1..15}; do
XPRA_PID="$(sudo -u "$SUDO_USER" cat "$XPRA_PID_FILE")"
[ -z "$XPRA_PID" ] || break
sleep 1
done
# the pid file is not needed anymore
rm -f "$XPRA_PID_FILE"
if [ -z "$XPRA_PID" ]; then
echo "Could not start xpra"
return 1
fi
NEEDS_XPRA_SHUTDOWN=1
}
shutdown_xpra() {
if [ $NEEDS_XPRA_SHUTDOWN -eq 1 ]; then
kill "$XPRA_PID"
fi
NEEDS_XPRA_SHUTDOWN=0
}
cleanup() {
shutdown_xpra
shutdown_wg
}
trap cleanup EXIT
if [[ ! -v SUDO_USER ]]; then
echo 'Please start this script with sudo, it will drop the permissions where possible'
exit 1
fi
HELPER_IP=""
if ! [ -z "$HELPER_IP_URL" ]; then
echo "Try to automatically obtain the helper IP ..."
HELPER_IP_ERR=0
HELPER_IP="$(curl --silent --show-error --fail "$HELPER_IP_URL" || HELPER_IP_ERR=1)"
if [ "$HELPER_IP_ERR" -eq 1 ]; then
HELPER_IP=""
fi
fi
if [ -z "$HELPER_IP" ]; then
# ask at runtime
read -rep 'IP address of the helper: ' HELPER_IP
fi
setup_wg
setup_xpra
read -sp 'Press ENTER to shutdown xpra'; echo ''
Desktop-Verknüpfung
Wie man eine Desktop-Verknüpfung erstellt wird in unserem Wiki beschrieben. Das ist besonders beim Hilfenehmenden sinnvoll.
Dabei sollte man nicht vergessen, dass die Skripte mit sudo
gestartet werden müssen. Wenn man das Skript
so speichert, dass der Benutzer dafür keine Schreibrechte hat, dann kann man in der sudoers
-Datei eintragen,
dass der Benutzer dieses konkrete Skript ohne Passwortabfrage starten darf.