diff --git a/tools/tunnel-einrichten.ps1 b/tools/tunnel-einrichten.ps1 new file mode 100644 index 0000000..9996545 --- /dev/null +++ b/tools/tunnel-einrichten.ps1 @@ -0,0 +1,170 @@ +# 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', + [string]$Benutzer = $env:USERNAME, + # 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' +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." + Warnung 'Häufigste Ursache: der öffentliche Schlüssel ist auf dem Server noch' + Warnung 'nicht hinterlegt. Zum Nachsehen einmal von Hand, dann sagt ssh warum:' + Write-Host "" + Write-Host " ssh $Benutzer@$Server" -ForegroundColor White +}