SSH für.NET-Applikationen mit SSH.NET
Best of NuGet, Teil 1

Die Secure Shell hat sich in vielen Bereichen als unersetzbares Werkzeug etabliert. Unabhängig davon, ob ein im Headless-Betrieb arbeitender Prozessrechner oder eine „unixoide Workstation“ zum Einsatz kommt: Immer dann, wenn der Zugriff unter Verbrauch von wenig Bandbreite zu erfolgen hat, führt an SSH kein Weg vorbei.
Anders als beim absoluten Klassiker Telnet gilt, dass die über die Leitung gesendeten Informationen unter Verwendung vergleichsweise starker Kryptografie abgesichert werden.
In diesem Kurz-Tutorial wollen wir das Paket SSH.NET verwenden. Als Gegenstelle dient ein mit einer aktuellen Version von Raspberry Pi OS ausgestatteter Raspberry Pi. Zu beachten ist, dass die Prozessrechner mit den Standardeinstellungen aus Sicherheitsgründen kein SSH-Interface mehr exponieren. Anleitungen zur Aktivierung findet man als interessierter Experimentator unter dem Stichwort Remote Access in der Raspberry-Pi-Dokumentation.
Einrichtung von Projektstruktur und Bibliothek
Einer der wichtigsten Erfolgsfaktoren von NuGet ist die Einfachheit der Installation selbst bei komplexesten Komponenten. Was einst manuelles Herumklicken mit Assembly und Co. erforderte, können .NET-Entwicklerinnen und -Entwickler nur mit wenigen Klicks innerhalb von Visual Studio erledigen. In den folgenden Schritten wollen wir mit einem auf der Vorlage Konsolen-App basierenden Projekt beginnen, als Name sei SSHTest1 angenommen.
Im Bereich der von den verschiedenen Bibliotheken unterstützten .NET-Versionen gibt es keinen wirklich einheitlichen Standard. Neben der Konsultation der Dokumentation kann es auch empfehlenswert sein, die offizielle NuGet-Galerie zu besuchen. Im Fall von SSH.NET informiert sie wie in Bild 1 gezeigt über die unterstützten Versionen.

Nun ist klar: Unser Konsolenprojekt muss auf .NET 8.0 basieren (Bild 1)
NuGet.org
Zum Abschluss der Vorbereitungshandlungen bietet es sich an, einen ersten Testlauf gegen den Raspberry Pi durchzuführen. Hierzu ist folgender Code erforderlich:
using Renci.SshNet;
using (var client = new SshClient("192.168.1.103", "pi", "raspberry")) {
client.Connect();
using SshCommand cmd = client.RunCommand("echo 'Greets from RPi!'");
Console.WriteLine(cmd.Result);
}
Die Verwendung der Bibliothek erfolgt im Allgemeinen durch Nutzung eines SshClient-Objekts, das die Beziehung zwischen der .NET-Applikation und der unixoiden Gegenstelle repräsentiert. Wir authentifizieren uns hier unter Nutzung von Benutzernamen und Passwort – wer sich stattdessen mit einem Dateipaar ausweisen möchte, muss eine andere Variante des Objekt-Konstruktors verwenden.
Der Rest des Codes nutzt dann den Befehl RunCommand, um einen Befehl auf dem unixoiden Prozessrechner zur Ausführung zu bringen. Zu guter Letzt wird die Antwort in die Kommandozeile der Windows-Workstations ausgeworfen – die Ausführung des Programms führt zum Erscheinen der Begrüßungsmeldung.
Interaktive Experimente mit SSH.NET
In der Praxis sind „direkt und uninteraktiv“ ablaufende Kommandos höchst selten – das geringste Ärgernis, das man sich einhandelt, sind erhebliche Latenzen. Im nächsten Schritt wollen wir den für Prozessrechnerjobs hilfreichen Scanner nmap in Betrieb nehmen. Im Interesse der didaktischen Komplexität wird der Autor davon ausgehen, dass das Image des Raspberry Pi das Produkt noch nicht enthält – wäre das Paket bereits vorhanden, würden die folgenden Experimente weniger lehrreich ausfallen.
Als ersten Versuch bietet es sich jedenfalls an, nach folgendem Schema die Ausführung der Engine zu befehligen. Beachten Sie außerdem, dass die Auswertung nun sowohl das Feld Error als auch das Feld Result tangiert:
using SshCommand cmd = client.RunCommand("nmap -sN 192.168.1.0/24"); Console.WriteLine(cmd.Result + " / " + cmd.Error);
Als Ergebnis gibt die Ausführung den folgenden String zurück:
bash: line 1: nmap: command not found.
Würde unser Code das Error-Feld nicht auswerten, so erschiene ein leeres Kommandozeilenfenster am Bildschirm.
Aus der Logik folgt, dass der nächste Schritt das probeweise Nachinstallieren des Pakets ist. Unter unixoiden Betriebssystemen ist die Paketverwaltung einfach, weshalb eine naive Modifikation folgendermaßen aussehen würde:
using SshCommand cmd = client.RunCommand("sudo apt-get install nmap");
Als Resultat erscheint ein „leeres“ Kommandozeilenfenster, das auch nach einiger Zeit nicht vom Feld am Bildschirm verschwindet. Ursache dieses unbefriedigenden Verhaltens ist, dass der Befehl apt-get install vom Benutzer eine Eingabe verlangt. Spezifischerweise präsentiert sich der Flow wie in Bild 2 gezeigt.
![Das Programm hängt beim Statement Do you want to continue? [Y/n] (Bild 2) Das Programm hängt beim Statement Do you want to continue? [Y/n] (Bild 2)](https://eu-images.contentstack.com/v3/assets/blt22276bff283176bd/blt094ee7a8ccec61e7/68d2600fb20d4d0e19ca8a16/2.png?width=640&format=webply)
Das Programm hängt beim Statement Do you want to continue? [Y/n] (Bild 2)
AutorDie nächste Version des Programms präsentiert sich folgendermaßen:
using (var client = new SshClient("192.168.1.103", "pi", "raspberry")) { client.Connect(); using (ShellStream shellStream = client.CreateShellStream("ShellName", 80, 24, 800, 600, 1024)) { string prompt = shellStream.Expect(new Regex(@"[$>]"));
Anstatt wie bisher unter Nutzung von RunCommand direkt Befehle gegen den Prozessrechner zur Ausführung zu bringen, setzen wir nun auf einen ShellStream. Dabei handelt es sich um eine Art des IO-Streams, der allerdings mit der angeschlossenen SSH-Gegenstelle zu interagieren sucht.
Shells sind für die Abarbeitung von per Batch ablaufender Aufgaben vorgesehen. Da der Prozessrechner für die Bereitstellung der Shell-Session mitunter etwas Zeit in Anspruch nimmt, sorgt die Zeile mit dem Regex @"[$>]" für eine variable Totzeit. Spezifischerweise gilt, dass der ShellStream so lange anhält, bis eine den Regex „befriedigende“ Antwort eintrifft. Da wir hier auf den Systemprompt matchen, deutet dies auf das Verfügbarsein des Prozessrechners hin.
Die eigentliche Installationsanweisung nutzt dann einen weiteren Matcher-Block, um auf das Erscheinen der Abfrage zu warten. Die Wartezeit ist erforderlich, weil die Berechnung der notwendigen Paket-Transaktionen insbesondere auf langsameren Prozessrechnern einige Zeit in Anspruch nehmen kann:
shellStream.WriteLine("sudo apt-get install nmap"); prompt = shellStream.Expect("Do you want to"); Thread.Sleep(100); shellStream.WriteLine("y"); shellStream.Expect(new Regex(@"[$>]")); }
Die Zeile shellStream.WriteLine("y"); kümmert sich dann um das Senden der Quittierung; ein weiterer Regex wartet die erfolgreiche Abarbeitung der (lange dauernden) Paket-Operationen des Betriebssystems ab.
Nach der erfolgreichen Abarbeitung des Installationsprozesses zerstören wir das ShellStream-Objekt, um danach nach dem bekannten Schema einen Scanbefehl loszutreten:
using SshCommand cmd = client.RunCommand("nmap -sN 192.168.1.0/24"); Console.WriteLine(cmd.Result + " / " + cmd.Error); }
Ergebnis der Programmausführung ist nur das Erscheinen der in Bild 3 gezeigten Fehlermeldung: Ursache dafür ist, dass manche fortgeschrittene Scanarten Superuserrechte voraussetzen, unser Programm allerdings mit den Rechten des Pi-Users arbeitet.

Der Gutteil der Ausführungszeit – bis zum Erscheinen der Meldung kann etwas Zeit ins Land gehen – wird für die Operationen des Paketmanagers verbraucht. Dies lässt sich dadurch überprüfen, dass man beispielsweise unter Nutzung von Putty eine zweite Verbindung zum Prozessrechner aufbaut und dort eine beliebige Paketmanager-Operation ausführt. Dies scheitert – wie in Bild 4 gezeigt – mit einem Verweis darauf, dass das Paketmanager-Singleton gesetzt ist und deshalb keine weiteren Paketoperationen durchgeführt werden können.

Die Fehlermeldung vermeldet paradoxerweise Erfolg (Bild 4)
AutorFazit und Ausblick
Obwohl wir hier nur einen winzigen Teil der Möglichkeiten der SSH.NET-Bibliothek tangiert haben, ist offensichtlich, dass sie die verschiedensten Arten der Kommunikation zwischen .NET-Applikation und Prozessrechnersystem ermöglicht.
Angemerkt sei, dass es sich hierbei nur um eine von vielen im NuGet-Ökosystem lebenden Bibliotheken handelt. In Folgeartikeln stellen wir weitere nützliche Module vor – bleiben Sie dran, denn das Selbstimplementieren von kostenlos Vorhandenem ist immer die ineffizienteste Vorgehensweise.