# One-step ephemnet-agent installer for Windows — the GUI/desktop # counterpart to install.sh (that script's own header comment explains # the fetch/verify/install shape this one shares; read it first if you # haven't): # # iwr -useb https://eph.network/agent/install.ps1 | iex # # Why a separate script rather than teaching the bash one to also run # under WSL/Git-Bash on Windows: this one asks its questions (where to # install, whether to start at login) with a real native folder-picker # and yes/no boxes, not a terminal prompt — most Windows users running # this via `iwr | iex` from a plain PowerShell window have no # terminal-prompting convention to meet them halfway, unlike # install.sh's own /dev/tty prompts, which assume a real terminal is # already in play. # # ephemnet-agent itself has no GUI and no system-tray icon (unlike # cinder's own cindertunnel, whose Windows installer this one is # modeled after) — it's a pure background process, same as the # systemd/launchd path install.sh already sets up on Linux/macOS. So # "start at login" here means a per-user Startup-folder shortcut # running the binary fully hidden (no window to show at all — the # -status-addr admin page, not a window, is how you interact with # it), not a tray-icon app launched minimized. No admin elevation is # asked for anywhere in this script, matching install.sh's own # per-user scope on every platform. # # Overrides, all optional env vars — same names/meanings install.sh's # own copy of this comment already documents: # EPHEMNET_AGENT_BASE_URL default https://eph.network/agent # EPHEMNET_AGENT_VERSION default: manifest.json's own "latest" # EPHEMNET_AGENT_INSTALL_DIR skips the folder-picker entirely # # Needs PowerShell 5+ (ships with every Windows 10/11) and tar.exe # (ships standard since Windows 10 1803) — no extra install for either. $ErrorActionPreference = 'Stop' function Get-EnvOrDefault($name, $default) { $v = [Environment]::GetEnvironmentVariable($name) if ([string]::IsNullOrEmpty($v)) { return $default } return $v } $BaseUrl = Get-EnvOrDefault 'EPHEMNET_AGENT_BASE_URL' 'https://eph.network/agent' $PinnedVersion = Get-EnvOrDefault 'EPHEMNET_AGENT_VERSION' '' $InstallDirOverride = Get-EnvOrDefault 'EPHEMNET_AGENT_INSTALL_DIR' '' # The -status-addr this installer's own New-DefaultConfig writes, and # the one the desktop-shortcut step points at by default — one # constant so the two can never drift apart, same reasoning # install.sh's own copy of this comment gives. $DefaultStatusAddr = '127.0.0.1:8093' switch ([System.Environment]::Is64BitOperatingSystem) { $true { $platform = 'windows-amd64' } $false { $platform = 'windows-386' } } # Where to install: EPHEMNET_AGENT_INSTALL_DIR skips asking entirely, # same convention as install.sh. Otherwise a native folder browser, # defaulting to %LocalAppData%\ephemnet-agent — writable with no # elevation, rather than Program Files (which would need an admin # prompt for what install.sh's own Linux/macOS paths never require # either). $defaultInstallDir = Join-Path $env:LOCALAPPDATA 'ephemnet-agent' $installDir = $InstallDirOverride if ($installDir) { Write-Host "==> EPHEMNET_AGENT_INSTALL_DIR is set — installing to $installDir" } else { Add-Type -AssemblyName System.Windows.Forms $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = "Choose a folder for ephemnet-agent (Cancel to use the default: $defaultInstallDir)" $dialog.SelectedPath = $defaultInstallDir $dialog.ShowNewFolderButton = $true $result = $dialog.ShowDialog() if ($result -eq [System.Windows.Forms.DialogResult]::OK -and $dialog.SelectedPath) { $installDir = $dialog.SelectedPath } else { $installDir = $defaultInstallDir } } New-Item -ItemType Directory -Force -Path $installDir | Out-Null Write-Host "==> Fetching $BaseUrl/downloads/manifest.json ..." $manifest = Invoke-RestMethod -Uri "$BaseUrl/downloads/manifest.json" $version = $PinnedVersion if (-not $version) { $version = $manifest.latest } Write-Host "==> Installing ephemnet-agent $version for $platform..." $entry = $manifest.versions | Where-Object { $_.version -eq $version } if (-not $entry) { Write-Error "version $version not found in manifest" exit 1 } $relPath = $entry.platforms.$platform if (-not $relPath) { Write-Error "no $platform build for version $version — see $BaseUrl/ for what's actually available." exit 1 } $expectedSha = $entry.sha256.$platform $tmp = Join-Path $env:TEMP "ephemnet-agent-install-$([guid]::NewGuid())" New-Item -ItemType Directory -Force -Path $tmp | Out-Null try { $archiveName = Split-Path -Leaf $relPath $archivePath = Join-Path $tmp $archiveName Write-Host "==> Downloading $relPath..." Invoke-WebRequest -Uri "$BaseUrl/downloads/$relPath" -OutFile $archivePath if ($expectedSha) { Write-Host "==> Verifying SHA-256..." $actualSha = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLower() if ($actualSha -ne $expectedSha) { Write-Error "checksum mismatch — expected $expectedSha, got $actualSha. Not installing." exit 1 } Write-Host " OK ($actualSha)" } else { Write-Warning "manifest has no checksum for this platform/version — installing unverified." } tar -xzf $archivePath -C $tmp if ($LASTEXITCODE -ne 0) { throw "tar extraction failed" } Copy-Item -Path (Join-Path $tmp 'ephemnet-agent.exe') -Destination (Join-Path $installDir 'ephemnet-agent.exe') -Force } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } $exePath = Join-Path $installDir 'ephemnet-agent.exe' Write-Host "==> Installed $version to $exePath" # New-DefaultConfig creates a bare-minimum, real config.json at path # (relay-addr/acme-api pointed at the real production relay, plus # status-addr so the admin UI is reachable to add real routes # afterward) if nothing exists there yet — same shape, same reasoning, # as install.sh's own ensure_default_config. Never touches a file that # already exists. function New-DefaultConfig($path) { if (Test-Path $path) { return } New-Item -ItemType Directory -Force -Path (Split-Path -Parent $path) | Out-Null $json = @" { "relay-addr": "mera.network:443", "status-addr": "$DefaultStatusAddr", "acme-api": "https://api.mera.network", "#status-auth-token": "set this to require HTTP Basic Auth on the local admin page above once it's reachable beyond a single trusted operator — see that page's own API reference tab" } "@ Set-Content -Path $path -Value $json Write-Host " Created default config at $path (relay-addr=mera.network:443, status-addr=$DefaultStatusAddr, acme-api=https://api.mera.network) — add real domains/routes via that local admin page (http://$DefaultStatusAddr/) once it's running, or edit this file directly first." } # New-RunHiddenLauncher writes a tiny .vbs that launches # ephemnet-agent.exe fully hidden (WshShell.Run's own 0 = SW_HIDE), # and points the Startup-folder shortcut at THAT instead of the .exe # directly — a real correctness fix, not cosmetic: ephemnet-agent.exe # is an ordinary console-subsystem binary (no -H=windowsgui build # flag, unlike cinder's own cindertunnel, which carries a real # system-tray icon and so has no console window to begin with), and a # plain .lnk's own WindowStyle property (Normal/Maximized/Minimized — # there is no "Hidden" option at all) does not reliably suppress a # console window for a shortcut Windows itself launches at login. The # WshShell.Run(...,0,...) trick is the standard, reliable way to # launch any console app with truly zero visible window. function New-RunHiddenLauncher($configPath) { $vbsPath = Join-Path $installDir 'run-hidden.vbs' $vbs = @" Set WshShell = CreateObject("WScript.Shell") WshShell.CurrentDirectory = "$installDir" WshShell.Run """$exePath"" -config ""$configPath""", 0, False "@ Set-Content -Path $vbsPath -Value $vbs return $vbsPath } # New-AdminShortcut writes a .url Internet Shortcut to the Desktop # pointing at the local admin page — the Windows equivalent of # install.sh's own create_linux_shortcut/create_darwin_shortcut (that # script's own comment once called this "the natural equivalent # whenever [Windows support] lands" — this is that). No icon asset, # no GUI toolkit — a .url file is just plain text, and every version # of Windows already knows how to open one in the default browser. function New-AdminShortcut($addr) { $desktop = [Environment]::GetFolderPath('Desktop') $shortcutPath = Join-Path $desktop 'EphemNet Agent Admin.url' $content = "[InternetShortcut]`r`nURL=http://$addr/`r`n" Set-Content -Path $shortcutPath -Value $content Write-Host " Wrote $shortcutPath — double-click it any time to open the admin page." } $startupDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Startup' $startupShortcutPath = Join-Path $startupDir 'ephemnet-agent.lnk' $defaultConfigPath = Join-Path $installDir 'config.json' # Offer-Restart handles re-running this script against a machine that # already has a Startup entry from a previous run — same real gap # install.sh's own offer_restart_if_already_set_up was written to # close: an install that only ever replaces the binary, never # restarts anything, leaves the old version running indefinitely # after an "update". Returns $true if a Startup entry already existed # (whether or not it was restarted just now) — the fresh-setup prompt # below only makes sense when nothing is set up yet. function Offer-Restart { if (-not (Test-Path $startupShortcutPath)) { return $false } Add-Type -AssemblyName System.Windows.Forms $restart = [System.Windows.Forms.MessageBox]::Show( "An ephemnet-agent startup entry already exists — restart it now to use the just-installed $version?", 'ephemnet-agent', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question, [System.Windows.Forms.MessageBoxDefaultButton]::Button2 ) if ($restart -ne [System.Windows.Forms.DialogResult]::Yes) { Write-Host " Not restarted — it'll keep running the previous version until you do (re-run this script, or restart it by hand)." return $true } Get-Process -Name 'ephemnet-agent' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $exePath } | Stop-Process -Force -ErrorAction SilentlyContinue New-DefaultConfig $defaultConfigPath Start-Process -FilePath $exePath -ArgumentList "-config `"$defaultConfigPath`"" -WorkingDirectory $installDir -WindowStyle Hidden Write-Host " Restarted." return $true } Write-Host "" $alreadySetUp = Offer-Restart if (-not $alreadySetUp) { Add-Type -AssemblyName System.Windows.Forms $startAtLogin = [System.Windows.Forms.MessageBox]::Show( 'Start ephemnet-agent automatically when you log in?', 'ephemnet-agent', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question, [System.Windows.Forms.MessageBoxDefaultButton]::Button2 ) if ($startAtLogin -eq [System.Windows.Forms.DialogResult]::Yes) { New-DefaultConfig $defaultConfigPath $vbsPath = New-RunHiddenLauncher $defaultConfigPath New-Item -ItemType Directory -Force -Path $startupDir | Out-Null $shell = New-Object -ComObject WScript.Shell $shortcut = $shell.CreateShortcut($startupShortcutPath) $shortcut.TargetPath = (Join-Path $env:WINDIR 'System32\wscript.exe') $shortcut.Arguments = "//B `"$vbsPath`"" $shortcut.WorkingDirectory = $installDir $shortcut.Description = 'EphemNet reverse-tunnel agent (runs hidden — see run-hidden.vbs)' $shortcut.Save() Write-Host "==> Wrote $startupShortcutPath" Start-Process -FilePath $exePath -ArgumentList "-config `"$defaultConfigPath`"" -WorkingDirectory $installDir -WindowStyle Hidden Write-Host " Started now, and will start automatically at every future login." Add-Type -AssemblyName System.Windows.Forms $wantShortcut = [System.Windows.Forms.MessageBox]::Show( "Create a desktop shortcut to the local admin page (http://$DefaultStatusAddr/)?", 'ephemnet-agent', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question, [System.Windows.Forms.MessageBoxDefaultButton]::Button1 ) if ($wantShortcut -eq [System.Windows.Forms.DialogResult]::Yes) { New-AdminShortcut $DefaultStatusAddr } } } Write-Host "Run '$exePath -h' to see every flag. Re-run this script any time to update."