Files
d4rk_media/tools/tunnel-einrichten.ps1
T
D4rkst3randClaude Opus 5 15f4c7bc3d fix: der Tunnel scheiterte am Benutzernamen, und die Meldung zeigte woandershin
Der Standard fuer -Benutzer war $env:USERNAME -- der Name am HEIMRECHNER. Auf
dem Server heisst das Konto anders (hier "az319" gegen "Darkster"). Das Skript
lief sauber durch, trug die Aufgabe ein, und der Tunnel kam trotzdem nicht.

SCHLIMMER ALS DER FEHLER WAR DIE DIAGNOSE. Am Ende stand fest verdrahtet
"Haeufigste Ursache: der oeffentliche Schluessel ist noch nicht hinterlegt" --
eine GERATENE Ursache, und sie war falsch. Wer dem folgt, prueft den Schluessel,
findet ihn in Ordnung und sucht weiter an der falschen Stelle.

Jetzt wird gemessen: schlaegt der Tunnel fehl, baut das Skript selbst eine
SSH-Verbindung auf und wertet aus, was der Server antwortet -- "Permission
denied" (dann Name ODER Schluessel, beides benannt), nicht erreichbar,
Weiterleitung verweigert, oder ssh kommt durch und es liegt am Ziel-Port.

Und -Benutzer hat keinen Standard mehr. Fehlt er, wird gefragt: "Wie heisst
dein Konto AUF DEM SERVER? (nicht der Name hier am PC)". Einmal fragen ist
besser als einmal in die Irre fuehren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 01:03:53 +02:00

203 lines
9.7 KiB
PowerShell

# Richtet den SSH-Tunnel zum Server als HINTERGRUNDAUFGABE ein.
#
# LÄUFT AUF DEM HEIMRECHNER, nicht auf dem Server.
#
# pwsh -File tunnel-einrichten.ps1
# pwsh -File tunnel-einrichten.ps1 -Benutzer Florian -LokalerPort 13390
#
# WOZU. Seit dem 13.08.2026 ist RDP am Server gesperrt (Hintergrund:
# docs/vorfall-2026-08-12-gitea.md). Hinein geht es nur noch durch einen
# SSH-Tunnel mit Schlüssel. Von Hand hieße das: vor jeder Sitzung ein
# PowerShell-Fenster öffnen, einen Befehl tippen, das Fenster offen lassen.
#
# Das hält niemand durch, und was niemand durchhält, wird umgangen -- am Ende
# steht dann wieder ein offener Port. Deshalb wird der Tunnel hier zu etwas,
# das man nicht sieht: eine Aufgabe, die beim Anmelden startet, sich bei
# Verbindungsabbruch selbst neu aufbaut und kein Fenster zeigt.
#
# Danach verbindet man sich wie immer -- nur nicht mehr auf die Server-Adresse,
# sondern auf localhost mit dem hier eingestellten Port.
#
# WAS ES BRAUCHT: einen SSH-Schlüssel, dessen öffentlicher Teil auf dem Server
# hinterlegt ist. Fehlt einer, legt dieses Skript ihn an und zeigt, was zu
# schicken ist.
[CmdletBinding()]
param(
[string]$Server = '88.218.224.10',
# KEIN Standard aus $env:USERNAME. Der lokale Windows-Name und das Konto auf
# dem Server sind oft verschieden -- hier hiess der eine "az319" und der
# andere "Darkster". Das Skript lief dann sauber durch und der Tunnel kam
# trotzdem nicht, mit einer Fehlermeldung, die auf den Schluessel zeigte
# statt auf den Namen. Lieber einmal fragen als einmal in die Irre fuehren.
[string]$Benutzer,
# Der Port auf DIESEM Rechner. Nicht 3389 nehmen: Windows lauscht dort
# selbst, und dann landet die Verbindung auf dem eigenen PC statt im Tunnel
# -- mit der irreführenden Meldung "Konsolensitzung bereits aktiv".
[int]$LokalerPort = 13389,
[int]$ZielPort = 3389,
[string]$AufgabenName = 'SSH-Tunnel zum Rootserver'
)
$ErrorActionPreference = 'Stop'
if (-not $Benutzer) {
Write-Host "Wie heisst dein Konto AUF DEM SERVER? (nicht der Name hier am PC)" -ForegroundColor Yellow
$Benutzer = (Read-Host ' Benutzername').Trim()
if (-not $Benutzer) { Write-Host ' Ohne Benutzernamen geht es nicht.' -ForegroundColor Red; exit 1 }
}
function Schritt($t) { Write-Host "==> $t" -ForegroundColor Cyan }
function Gut($t) { Write-Host " $t" -ForegroundColor Green }
function Warnung($t) { Write-Host " $t" -ForegroundColor Yellow }
# -- 1. Ist überhaupt ein SSH-Client da? ---------------------------------------
Schritt 'Voraussetzungen prüfen'
$ssh = (Get-Command ssh.exe -ErrorAction SilentlyContinue).Source
if (-not $ssh) {
Write-Host ' Kein ssh.exe gefunden. Unter Windows 10/11 nachinstallieren mit:' -ForegroundColor Red
Write-Host ' Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0' -ForegroundColor Red
exit 1
}
Gut "ssh.exe: $ssh"
# -- 2. Schlüssel ---------------------------------------------------------------
$schluessel = Join-Path $env:USERPROFILE '.ssh\id_ed25519'
if (-not (Test-Path $schluessel)) {
Schritt 'Es gibt noch keinen Schlüssel -- einen anlegen'
New-Item -ItemType Directory -Force -Path (Split-Path $schluessel) | Out-Null
# Ohne Passphrase, damit die Hintergrundaufgabe ohne Rückfrage startet. Der
# Schutz liegt dann darin, dass die Datei nur diesem Windows-Konto gehört.
# Wer eine Passphrase will, nimmt sie -- muss den Schlüssel dann aber in den
# ssh-agent legen, sonst hängt die Aufgabe an der Abfrage.
& ssh-keygen -t ed25519 -N '""' -f $schluessel -C "$Benutzer-$env:COMPUTERNAME" | Out-Null
Gut 'Schlüssel angelegt.'
Write-Host ''
Write-Host ' ===> DIESE ZEILE muss auf dem Server hinterlegt werden:' -ForegroundColor Yellow
Write-Host ''
Write-Host (" " + (Get-Content "$schluessel.pub")) -ForegroundColor White
Write-Host ''
Write-Host ' Schick sie demjenigen, der den Server betreut. Erst danach' -ForegroundColor Yellow
Write-Host ' funktioniert der Tunnel -- das Skript richtet ihn trotzdem' -ForegroundColor Yellow
Write-Host ' schon ein.' -ForegroundColor Yellow
Write-Host ''
} else {
Gut "Schlüssel vorhanden: $schluessel"
}
# -- 3. Ist der lokale Port frei? ----------------------------------------------
#
# Vorher prüfen und nicht hinterher rätseln: ist er belegt, bindet ssh nicht,
# und die RDP-Verbindung landet bei dem Programm, das dort schon lauscht.
Schritt "Lokalen Port $LokalerPort prüfen"
$belegt = Get-NetTCPConnection -State Listen -LocalPort $LokalerPort -ErrorAction SilentlyContinue
if ($belegt) {
$wer = (Get-Process -Id $belegt[0].OwningProcess -ErrorAction SilentlyContinue).ProcessName
Write-Host " Port $LokalerPort ist belegt (von $wer). Nimm einen anderen:" -ForegroundColor Red
Write-Host " pwsh -File tunnel-einrichten.ps1 -LokalerPort $($LokalerPort + 1)" -ForegroundColor Red
exit 1
}
Gut 'frei'
# -- 4. Das Tunnelskript ablegen ------------------------------------------------
#
# Eine Schleife und kein einzelner Aufruf: bei Zwangstrennung, Standby oder
# kurzem Leitungsaussetzer endet ssh, und ohne Schleife wäre der Tunnel dann
# weg -- ohne dass es jemand merkt, bis die nächste Verbindung scheitert.
$ordner = Join-Path $env:LOCALAPPDATA 'SSH-Tunnel'
New-Item -ItemType Directory -Force -Path $ordner | Out-Null
$skript = Join-Path $ordner 'tunnel.ps1'
@"
# Erzeugt von tunnel-einrichten.ps1 -- von Hand ändern lohnt nicht,
# lieber das Einrichtungsskript neu laufen lassen.
while (`$true) {
# -N: nur weiterleiten, keine Shell.
# ExitOnForwardFailure: lieber sofort scheitern und neu versuchen, als
# scheinbar zu laufen, während die Weiterleitung gar nicht steht.
# ServerAliveInterval: erkennt tote Leitungen, statt still einzufrieren.
& '$ssh' -N ``
-o ExitOnForwardFailure=yes ``
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 ``
-o StrictHostKeyChecking=accept-new ``
-L ${LokalerPort}:localhost:${ZielPort} ``
$Benutzer@$Server
Start-Sleep -Seconds 5
}
"@ | Set-Content -Path $skript -Encoding UTF8
Gut "Tunnelskript: $skript"
# -- 5. Als Aufgabe beim Anmelden ----------------------------------------------
Schritt 'Als Hintergrundaufgabe eintragen'
Unregister-ScheduledTask -TaskName $AufgabenName -Confirm:$false -ErrorAction SilentlyContinue
$pwsh = (Get-Command pwsh.exe -ErrorAction SilentlyContinue).Source
if (-not $pwsh) { $pwsh = (Get-Command powershell.exe).Source }
$aktion = New-ScheduledTaskAction -Execute $pwsh `
-Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$skript`""
$ausloeser = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
# Interactive, nicht S4U: der Schlüssel liegt im Benutzerprofil, und die
# Aufgabe muss ihn lesen können.
$prinzipal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
$optionen = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName $AufgabenName -Action $aktion -Trigger $ausloeser `
-Principal $prinzipal -Settings $optionen `
-Description "Haelt den SSH-Tunnel zu $Server offen. RDP verbindet sich danach auf localhost:$LokalerPort." | Out-Null
Gut 'eingetragen (startet künftig bei jeder Anmeldung)'
# -- 6. Jetzt starten und nachsehen ---------------------------------------------
Schritt 'Starten und nachmessen'
Start-ScheduledTask -TaskName $AufgabenName
$bis = (Get-Date).AddSeconds(25)
do {
Start-Sleep -Seconds 2
$steht = Get-NetTCPConnection -State Listen -LocalPort $LokalerPort -ErrorAction SilentlyContinue
} while (-not $steht -and (Get-Date) -lt $bis)
Write-Host ''
if ($steht) {
Gut "Der Tunnel steht: localhost:$LokalerPort -> ${Server}:$ZielPort"
Write-Host ''
Write-Host " In der Fernwartung ab jetzt eintragen:" -ForegroundColor White
Write-Host " Hostname : localhost" -ForegroundColor White
Write-Host " Port : $LokalerPort" -ForegroundColor White
Write-Host ''
Write-Host " Nicht mehr auf $Server verbinden -- das ist gesperrt." -ForegroundColor DarkGray
} else {
Warnung "Der Tunnel steht noch nicht auf Port $LokalerPort."
Write-Host ''
# NACHSEHEN STATT RATEN. Die erste Fassung behauptete hier "der Schluessel
# fehlt auf dem Server" -- und lag falsch: es war der Benutzername. Eine
# geratene Ursache schickt den Suchenden in die falsche Richtung, und das
# kostet mehr Zeit als gar kein Hinweis.
Schritt 'Nachsehen, woran es liegt'
$antwort = & $ssh -o BatchMode=yes -o ConnectTimeout=10 `
-o StrictHostKeyChecking=accept-new "$Benutzer@$Server" 'exit' 2>&1 | Out-String
if ($antwort -match 'Permission denied') {
Warnung 'Der Server nimmt den Schluessel nicht an. Zwei Ursachen sind haeufig:'
Warnung " - Der Benutzername stimmt nicht. Du hast '$Benutzer' benutzt --"
Warnung ' ist das dein Konto AUF DEM SERVER, nicht das hier am PC?'
Warnung ' - Der oeffentliche Schluessel ist dort noch nicht hinterlegt:'
Write-Host ''
Write-Host (" " + (Get-Content "$schluessel.pub")) -ForegroundColor White
} elseif ($antwort -match 'Could not resolve|timed out|refused|unreachable') {
Warnung "Der Server ist nicht erreichbar: $($antwort.Trim())"
} elseif ($antwort.Trim()) {
Warnung "ssh sagt: $($antwort.Trim())"
} else {
Warnung 'ssh selbst kommt durch -- dann klemmt es an der Weiterleitung.'
Warnung "Steht auf dem SERVER etwas auf Port $ZielPort und laesst er"
Warnung 'Weiterleitungen zu (AllowTcpForwarding)?'
}
}