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>
203 lines
9.7 KiB
PowerShell
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)?'
|
|
}
|
|
}
|