From: Max Vilimpoc Date: Thu, 27 Aug 2026 23:56:49 +0000 (+0200) Subject: dotfiles: install the rsync release's ssh.exe beside rsync.exe X-Git-Url: https://vilimpoc.org/repos/dotfiles/commitdiff_plain/b8144088df5abe5fb042890ef5bc8fd72b279b79?ds=inline dotfiles: install the rsync release's ssh.exe beside rsync.exe The rsync-windows release is now one zip per architecture -- rsync.exe, the ssh.exe it runs, and the licence texts, under exactly those names -- rather than a bare rsync.exe, so the pinned .../download/v3.5.0-g521ad8ad/rsync.exe this script fetched no longer exists. Fetch rsync-windows-x64.zip or rsync-windows-x86.zip by OS bitness, verify it against the .sha256 published beside it, unpack to a scratch directory and move out the four files we asked for -- so a future release adding a fifth cannot quietly drop it onto the machine PATH. The two exes go in together on purpose: rsync.exe prefers an ssh.exe in its own directory, and the release builds one because the client Windows ships reads its stdin 3KB at a time, which holds a transfer *from* the box at ~17MB/s however fast the link is. Nothing else about it differs -- same ~/.ssh, same ssh-agent, same known_hosts -- and a bare `ssh` still resolves to the in-box client, which sits ahead of C:\Tools\rsync on the PATH. That ssh.exe links against the libcrypto.dll the OpenSSH Client capability puts in System32, and ships no copy of its own, so that capability is now installed here rather than assumed: it was already the thing ssh-agent, the git core.sshCommand in the non-elevated half, and rsync's own transport all depend on. Where it is absent, or its LibreSSL is older than the 3.8.2 the release is built against, the script says so and installs rsync alone. The URL follows the releases/latest/download/ redirect rather than the API, whose unauthenticated 60/hour per-IP limit a provisioning run behind a shared NAT can genuinely exhaust; pin the tag in $RsyncUrl to hold a box on a build. Verified end to end against the live release under Windows PowerShell 5.1 -- which is what the .bat launches, and which refuses to Expand-Archive anything not named .zip, hence "download-$RsyncAsset" for the scratch file. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D4ozcQcqMSJBi2Ez4Pfxyc --- diff --git a/README.md b/README.md index e10d872..063056a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ throwaway VM reachable from a Linux host. | --- | --- | | `setup-windows.bat` | Entry point. Runs the winget installs, then the non-elevated script, then launches the elevated half and prints its log. | | `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge and BinSkim on the user `PATH`, and the global git config (identity, plus `core.sshCommand`). Can also be run directly from an ordinary prompt. | -| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs and starts OpenSSH Server, installs `rsync.exe` to the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit. Can also be run directly from an Administrator prompt. | +| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit. Can also be run directly from an Administrator prompt. | ## Usage @@ -83,6 +83,26 @@ throwaway VM reachable from a Linux host. a profile. Key auth needs `~/.ssh/authorized_keys` ACL'd to just you and `SYSTEM`; accounts in the Administrators group use `C:\ProgramData\ssh\administrators_authorized_keys` instead. +- **rsync brings its own `ssh.exe`.** That release ships as one zip per + architecture — `rsync-windows-x64.zip` / `rsync-windows-x86.zip`, each holding + `rsync.exe`, an `ssh.exe`, `COPYING.txt` and `NOTICE-ssh.txt` — and the + elevated half picks the zip for the OS bitness, verifies it against the + published `.sha256`, and unpacks the pair together. Together is the point: + `rsync.exe` prefers an `ssh.exe` sitting in its own directory, and the release + builds one because the client Windows ships reads its stdin 3 KB at a time, + which holds a transfer *from* the box at ~17 MB/s however fast the link is. + Nothing else about it differs — same `~/.ssh`, same `ssh-agent`, same + `known_hosts` — and a bare `ssh` still resolves to the in-box client, which + sits ahead of `C:\Tools\rsync` on the `PATH`. It links against the + `libcrypto.dll` the **OpenSSH Client** capability puts in `System32` (Windows' + own LibreSSL, which uses AES-NI) and ships no copy of its own, so the elevated + half installs that capability first and falls back to `rsync.exe` alone, + warning, on an image that will not offer it. +- The `rsync` download follows the `releases/latest/download/` redirect rather + than the GitHub API: unauthenticated API calls are rate-limited to 60/hour per + IP, which a provisioning run behind a shared NAT can genuinely exhaust. To hold + a box on a known build, pin the tag in `$RsyncUrl` + (`.../releases/download//`) instead. - Visual Studio is installed in three labelled passes (base workload, Clang/LLVM, XP toolset) so a failure identifies which component group is responsible. - The scripts were extracted from a native Windows project, so the component diff --git a/setup-windows-with-uac.ps1 b/setup-windows-with-uac.ps1 index 8da5c55..8d990f5 100644 --- a/setup-windows-with-uac.ps1 +++ b/setup-windows-with-uac.ps1 @@ -5,9 +5,13 @@ via Start-Process -Verb RunAs, or run manually from an Administrator prompt. What this installs / configures: + - OpenSSH Client capability (the ssh.exe rsync shells out to, and the + System32 libcrypto.dll the release's own ssh.exe links against) - ssh-agent set to automatic + started - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22 - - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine PATH + - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine + PATH: rsync.exe plus the ssh.exe it runs, out of the release zip for this + architecture - Visual Studio 2022 Community (C++ desktop workload, Spectre libs, WDK VSIX, Win11 SDK 26100, Clang/LLVM, and the v141 + Windows XP targeting toolset) - Windows Driver Kit 10.0.26100 @@ -105,6 +109,36 @@ try { # --------------------------------------------------------------------------- # Base tools via winget # --------------------------------------------------------------------------- + +# --------------------------------------------------------------------------- +# OpenSSH Client +# +# Present by default on Windows 10 1809+ / Windows 11, but removable, and absent +# from some Server images. Two things below want it: rsync does not speak ssh +# itself, it execs an ssh binary, and the release's own ssh.exe links against the +# libcrypto.dll this capability puts in System32. It also owns the ssh-agent +# service configured next, so a missing client is why that step would fail. +# +# Non-fatal, like the server half below: a box that cannot have it should still +# finish provisioning. +# --------------------------------------------------------------------------- +Write-Step 'OpenSSH Client' +try { + $sshc = Get-WindowsCapability -Online -Name 'OpenSSH.Client*' | + Select-Object -First 1 + if (-not $sshc) { + Write-Warning 'OpenSSH.Client capability not offered by this Windows image - skipping.' + } elseif ($sshc.State -eq 'Installed') { + Write-Host " OK: $($sshc.Name) already installed" + } else { + Write-Host " Installing $($sshc.Name) ..." + $r = Add-WindowsCapability -Online -Name $sshc.Name + if ($r.RestartNeeded) { Write-Host ' [reboot required after OpenSSH Client]' -ForegroundColor Yellow } + } +} catch { + Write-Warning "OpenSSH Client setup failed: $($_.Exception.Message)" +} + # --------------------------------------------------------------------------- # SSH agent # --------------------------------------------------------------------------- @@ -174,6 +208,21 @@ try { # Windows' OpenSSH ships the transport only - no rsync - so pushing test data # from a Linux box needs an rsync.exe on the Windows side. # +# The release is one zip per architecture - rsync-windows-x64.zip and +# rsync-windows-x86.zip - each holding rsync.exe, the ssh.exe it runs, and the +# licence texts under exactly those names. Both exes are installed, together: +# rsync.exe prefers an ssh.exe in its own directory, and the release's build is +# what makes a push FROM this box run at line rate. The ssh.exe Windows ships +# reads its stdin 3KB at a time, which holds a send at ~17MB/s however fast the +# link is. Nothing else about it differs - same ~/.ssh, same ssh-agent, same +# known_hosts - and a bare `ssh` still resolves to the in-box client, which sits +# ahead of C:\Tools\rsync on the machine PATH. +# +# That ssh.exe links against the libcrypto.dll the OpenSSH Client capability +# above puts in System32: Windows' own LibreSSL, and the fast one, since it uses +# AES-NI. No copy of it ships in the zip, so where the capability is missing we +# unpack rsync alone rather than an ssh.exe that will not start. +# # Installed to C:\Tools\rsync (NOT under "Program Files"): the remote end is # invoked as `rsync --server ...` through cmd.exe, and a path with spaces makes # the client-side --rsync-path escape hatch painful to quote. Added to the @@ -184,18 +233,78 @@ try { # Non-fatal: a download failure only warns. # --------------------------------------------------------------------------- Write-Step 'rsync for Windows' -$RsyncUrl = 'https://github.com/nuket/rsync-windows/releases/download/v3.5.0-g521ad8ad/rsync.exe' -$RsyncDir = 'C:\Tools\rsync' +$RsyncRepo = 'nuket/rsync-windows' +$RsyncAsset = if ([Environment]::Is64BitOperatingSystem) { 'rsync-windows-x64.zip' } else { 'rsync-windows-x86.zip' } +# The /releases/latest/download/ redirect rather than the API: unauthenticated +# API calls are rate-limited to 60/hour per IP, which a provisioning run behind a +# shared NAT can genuinely exhaust, and the redirect costs none of that budget. +# To hold a box on a known build, pin the tag instead: +# .../releases/download/v3.5.0-gABCDEF0/$RsyncAsset +$RsyncUrl = "https://github.com/$RsyncRepo/releases/latest/download/$RsyncAsset" +$RsyncDir = 'C:\Tools\rsync' try { New-Item -ItemType Directory -Force -Path $RsyncDir | Out-Null $RsyncExe = Join-Path $RsyncDir 'rsync.exe' - # Download to a temp name first so an interrupted transfer can't leave a - # truncated rsync.exe sitting on the PATH. - $tmpExe = "$RsyncExe.download" + + # Does the release's ssh.exe have the libcrypto it needs? Decided before the + # download so the answer can also gate what comes out of the zip. + $SysCrypto = Join-Path $env:WINDIR 'System32\libcrypto.dll' + $WantSsh = Test-Path $SysCrypto + if (-not $WantSsh) { + Write-Warning "$SysCrypto is missing - the OpenSSH Client capability is not installed - and the release's ssh.exe needs it. Installing rsync.exe only; rsync will use the ssh on the PATH." + } else { + $v = (Get-Item $SysCrypto).VersionInfo.FileVersion + if ($v -and ([version]($v -replace '[^0-9.]', '')) -lt [version]'3.8.2') { + Write-Warning "$SysCrypto is LibreSSL $v; the release's ssh.exe is built against 3.8.2 (Windows OpenSSH Client 9.5). Update Windows, or expect ssh.exe not to start." + } + } + + # Download and unpack beside the targets, not over them, so an interrupted + # transfer can't leave a truncated rsync.exe sitting on the PATH. The scratch + # name still has to END in .zip: Windows PowerShell 5.1's Expand-Archive + # refuses any other extension outright ("*.download is not a supported + # archive file format"), where PowerShell 7 just reads the file. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpExe -UseBasicParsing - Move-Item -Path $tmpExe -Destination $RsyncExe -Force - Write-Host " Downloaded rsync.exe to $RsyncExe" + $tmpZip = Join-Path $RsyncDir "download-$RsyncAsset" + Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpZip -UseBasicParsing + Write-Host " Downloaded $RsyncAsset ($([math]::Round((Get-Item $tmpZip).Length / 1MB, 2)) MB)" + + # Verify against the .sha256 published beside it. Same origin, so this is an + # integrity check on the transfer rather than a defence against a hostile + # release - but a truncated or proxy-mangled download is the failure that + # actually happens, and it fails here instead of mid-transfer later. + # + # -OutFile, not .Content: GitHub serves the .sha256 as + # application/octet-stream, and Invoke-WebRequest hands back a byte[] rather + # than a string for any non-text content type, so .Content would compare the + # first BYTE against the hash and fail on every correct download. + $tmpSha = "$tmpZip.sha256" + Invoke-WebRequest -Uri "$RsyncUrl.sha256" -OutFile $tmpSha -UseBasicParsing + $want = (((Get-Content $tmpSha -Raw) -split '\s+')[0]).Trim().ToLower() + Remove-Item $tmpSha -Force -ErrorAction SilentlyContinue + $got = (Get-FileHash $tmpZip -Algorithm SHA256).Hash.ToLower() + if ($want -and $want -ne $got) { + Remove-Item $tmpZip -Force + throw "SHA-256 mismatch for ${RsyncAsset}: expected $want, got $got" + } + Write-Host " SHA-256 verified: $got" + + # Unpack to a scratch directory and move out the files we asked for, rather + # than expanding straight over the install directory: the zip is the unit + # that was checksummed, and this way a future release adding something to it + # cannot quietly drop that something onto the machine PATH. + $unpack = Join-Path $RsyncDir '.unpack' + if (Test-Path $unpack) { Remove-Item -Recurse -Force $unpack } + Expand-Archive -Path $tmpZip -DestinationPath $unpack -Force + Remove-Item $tmpZip -Force + foreach ($f in 'rsync.exe', 'ssh.exe', 'COPYING.txt', 'NOTICE-ssh.txt') { + $src = Join-Path $unpack $f + if (-not (Test-Path $src)) { continue } + if ($f -eq 'ssh.exe' -and -not $WantSsh) { continue } + Move-Item -Path $src -Destination (Join-Path $RsyncDir $f) -Force + } + Remove-Item -Recurse -Force $unpack + Write-Host " Installed $RsyncExe$(if ($WantSsh) { ' and the ssh.exe it runs' })" # Machine PATH (HKLM environment). Idempotent: only appends if absent. $m = [Environment]::GetEnvironmentVariable('Path', 'Machine') @@ -217,7 +326,8 @@ try { & $RsyncExe --version | Select-Object -First 1 } catch { Write-Warning "rsync install failed: $($_.Exception.Message)" - Write-Warning "Download manually from $RsyncUrl and drop it in $RsyncDir." + Write-Warning "Download $RsyncAsset from https://github.com/$RsyncRepo/releases manually" + Write-Warning "and unpack it into $RsyncDir, keeping rsync.exe and ssh.exe together." } # ---------------------------------------------------------------------------