Der S4U-Weg aus dem letzten Commit war richtig gedacht und praktisch unbrauchbar: das Eintragen einer S4U-Aufgabe braucht erhoehte Rechte, und in einem gewoehnlichen PowerShell-Fenster scheitert es mit "Zugriff verweigert". Ein Werkzeug, das Administratorrechte verlangt, benutzt am Ende niemand -- und genau darum ging es ja: was umstaendlich ist, wird umgangen. Jetzt startet die Aufgabe wscript.exe mit einem winzigen VBS, und dessen Run-Aufruf hat den Fenstermodus 0. Das heisst wirklich unsichtbar, im Unterschied zu "-WindowStyle Hidden", das bei Konsolenprogrammen in einer interaktiven Sitzung nur halb wirkt. Besondere Rechte braucht es nicht, weil nichts Besonderes passiert. Der Anmeldetyp bleibt damit Interactive; das Fenster verhindert der Starter und nicht der Anmeldetyp. Das Protokoll aus dem letzten Commit bleibt und wird jetzt erst recht gebraucht: sichtbar ist nichts mehr, also muss nachlesbar sein, was ssh sagt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
236 lines
12 KiB
PowerShell
236 lines
12 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.
|
|
#
|
|
# PROTOKOLL, weil es kein Fenster mehr gibt: die Aufgabe laeuft ausserhalb der
|
|
# Sitzung, also sieht niemand mehr, was ssh sagt. Ohne diese Datei waere ein
|
|
# abgelehnter Schluessel oder ein besetzter Port unsichtbar -- der Tunnel
|
|
# stuende einfach nicht, und niemand wuesste warum.
|
|
`$log = Join-Path `$PSScriptRoot 'tunnel.log'
|
|
if ((Test-Path `$log) -and (Get-Item `$log).Length -gt 1MB) { Remove-Item `$log -Force }
|
|
|
|
while (`$true) {
|
|
("[" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "] verbinde ...") | Add-Content `$log
|
|
# -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 2>&1 | Add-Content `$log
|
|
("[" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "] beendet, neuer Versuch in 5 s") | Add-Content `$log
|
|
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 }
|
|
|
|
# EIN VBS-STARTER ALS ZWISCHENSCHICHT, und der Umweg hat zwei Gruende.
|
|
#
|
|
# 1. "-WindowStyle Hidden" versteckt ein Konsolenprogramm nur halb. Die Aufgabe
|
|
# lief interaktiv in der Anmeldesitzung, bekam dort ein Fenster -- und wer
|
|
# es zumacht, kappt den Tunnel, ohne zu wissen was er tut.
|
|
# 2. Der naheliegende Ausweg, die Aufgabe per S4U ausserhalb der Sitzung laufen
|
|
# zu lassen, braucht erhoehte Rechte. In einem gewoehnlichen Fenster
|
|
# scheitert das Eintragen mit "Zugriff verweigert" -- und ein Werkzeug, das
|
|
# Administratorrechte verlangt, benutzt am Ende niemand.
|
|
#
|
|
# wscript.exe startet mit Fenstermodus 0, und das heisst wirklich unsichtbar.
|
|
# Es braucht keine besonderen Rechte, weil nichts Besonderes passiert.
|
|
$starter = Join-Path $ordner 'tunnel.vbs'
|
|
@"
|
|
' Erzeugt von tunnel-einrichten.ps1. Startet den Tunnel OHNE Fenster.
|
|
' Die 0 im Run-Aufruf ist der ganze Trick: Fenstermodus "unsichtbar".
|
|
Set sh = CreateObject("WScript.Shell")
|
|
sh.Run """$pwsh"" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ""$skript""", 0, False
|
|
"@ | Set-Content -Path $starter -Encoding ASCII
|
|
|
|
$aktion = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\wscript.exe" -Argument "`"$starter`""
|
|
$ausloeser = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
|
|
# Interactive und NICHT S4U: S4U braucht Administratorrechte zum Eintragen.
|
|
# Das Fenster verhindert der VBS-Starter, nicht der Anmeldetyp.
|
|
$prinzipal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
|
|
$optionen = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
|
-ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
|
|
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
|
|
Write-Host ''
|
|
Write-Host ' Es gibt jetzt KEIN Fenster mehr. Nachsehen kann man so:' -ForegroundColor DarkGray
|
|
Write-Host " Get-NetTCPConnection -LocalPort $LokalerPort -State Listen" -ForegroundColor DarkGray
|
|
Write-Host " Get-Content '$ordner unnel.log' -Tail 20" -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)?'
|
|
}
|
|
}
|