From 3605d540da7482479a76f98cf7bbeffe1e3dd250 Mon Sep 17 00:00:00 2001 From: AetherForge Date: Mon, 1 Jun 2026 15:03:17 -0700 Subject: [PATCH] Add self-healing Cloudflare tunnel with 60s watchdog for USB deck. Replaces fragile batch-based tunnel install with a single PowerShell script (cloudflare/start-tunnel.ps1) that installs cloudflared as a Windows service (admin) or runs as a background process (non-admin), clears stale EventLog registry keys that caused service rollback, and runs a 60-second watchdog that auto-restarts the connector on failure. Hostname changed to aether.thetempleofdoom.com. LAUNCH.bat and pack-usb.bat updated accordingly. --- .gitignore | 2 + LAUNCH.bat | 138 ++++++------- agent/deploy/uninstall.go | 2 +- cloudflare/SETUP.bat | 4 + cloudflare/SETUP.txt | 47 +++++ cloudflare/credentials.json | 7 + cloudflare/install-tunnel.bat | 163 ++++++++++++++++ cloudflare/repair-tunnel.ps1 | 115 +++++++++++ cloudflare/start-tunnel.ps1 | 302 +++++++++++++++++++++++++++++ cloudflare/tunnel-config.json | 6 + cloudflare/tunnel-watch.ps1 | 158 +++++++++++++++ cloudflare/verify-tunnel.bat | 52 +++++ cloudflare/write-tunnel-config.ps1 | 47 +++++ pack-usb.bat | 123 ++++++++---- usb/LAUNCH.bat | 138 ++++++------- usb/cloudflare/SETUP.txt | 58 ++++-- usb/cloudflare/start-tunnel.ps1 | 302 +++++++++++++++++++++++++++++ usb/cloudflare/tunnel-config.json | 6 + 18 files changed, 1455 insertions(+), 215 deletions(-) create mode 100644 cloudflare/SETUP.bat create mode 100644 cloudflare/SETUP.txt create mode 100644 cloudflare/credentials.json create mode 100644 cloudflare/install-tunnel.bat create mode 100644 cloudflare/repair-tunnel.ps1 create mode 100644 cloudflare/start-tunnel.ps1 create mode 100644 cloudflare/tunnel-config.json create mode 100644 cloudflare/tunnel-watch.ps1 create mode 100644 cloudflare/verify-tunnel.bat create mode 100644 cloudflare/write-tunnel-config.ps1 create mode 100644 usb/cloudflare/start-tunnel.ps1 create mode 100644 usb/cloudflare/tunnel-config.json diff --git a/.gitignore b/.gitignore index f865a0a..ab67f4b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,8 @@ Desktop.ini !/usb/cloudflare/ /usb/cloudflare/* !/usb/cloudflare/SETUP.txt +!/usb/cloudflare/start-tunnel.ps1 +!/usb/cloudflare/tunnel-config.json *.msi /cloudflared-windows-amd64.msi diff --git a/LAUNCH.bat b/LAUNCH.bat index e033892..12162c1 100644 --- a/LAUNCH.bat +++ b/LAUNCH.bat @@ -37,6 +37,11 @@ echo. set "BUNDLED_GO=%ROOT%\toolchain\go\bin\go.exe" set "GO_BIN=" +if exist "%ROOT%\toolchain\go" if not exist "%BUNDLED_GO%" ( + echo [Go] Incomplete toolchain folder — removing broken install... + rd /s /q "%ROOT%\toolchain\go" 2>nul +) + if exist "%BUNDLED_GO%" ( echo [Go] Using bundled toolchain: %ROOT%\toolchain\go set "GO_BIN=%ROOT%\toolchain\go\bin\go.exe" @@ -53,42 +58,61 @@ if not errorlevel 1 ( goto go_ready ) -:: Go not found - offer to download portable toolchain -echo [Go] Not found. Downloading portable Go toolchain... -echo This only happens once. The toolchain is saved to toolchain\go\ +:: Go not found - extract bundled zip or download once +echo [Go] Not found. Setting up portable Go toolchain... +echo Saved under: %ROOT%\toolchain\go\ echo. if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( set "GO_ARCH=amd64" +) else if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "GO_ARCH=arm64" ) else ( - if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( - set "GO_ARCH=arm64" - ) else ( - set "GO_ARCH=386" + set "GO_ARCH=386" +) +set "GO_VERSION=1.26.3" +set "GO_ZIP=go%GO_VERSION%.windows-%GO_ARCH%.zip" +set "GO_PACK_ZIP=%ROOT%\toolchain\pack\%GO_ZIP%" +set "GO_DEST=%ROOT%\toolchain\%GO_ZIP%" + +if not exist "%ROOT%\toolchain" mkdir "%ROOT%\toolchain" +if not exist "%ROOT%\toolchain\pack" mkdir "%ROOT%\toolchain\pack" + +if exist "%GO_PACK_ZIP%" ( + echo [Go] Extracting bundled %GO_ZIP%... + copy /y "%GO_PACK_ZIP%" "%GO_DEST%" >nul +) else ( + echo [Go] Downloading %GO_ZIP% from go.dev... + set "GO_URL=https://go.dev/dl/%GO_ZIP%" + powershell -NoProfile -Command "& { [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%GO_URL%' -OutFile '%GO_DEST%' }" + if errorlevel 1 ( + echo. + echo ERROR: Could not download Go. Copy toolchain\go\ from a PC that has it, + echo or add toolchain\pack\%GO_ZIP% to this folder, then re-run LAUNCH.bat. + echo. + pause + exit /b 1 ) ) -set "GO_VERSION=1.22.4" -set "GO_ZIP=go%GO_VERSION%.windows-%GO_ARCH%.zip" -set "GO_URL=https://go.dev/dl/%GO_ZIP%" -set "GO_DEST=%ROOT%\toolchain\go_%GO_ARCH%.zip" -powershell -NoProfile -Command "& { [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%GO_URL%' -OutFile '%GO_DEST%' }" -if errorlevel 1 ( - echo. - echo ERROR: Could not download Go. Please either: - echo a) Install Go from https://go.dev/dl/ on this PC, then re-run LAUNCH.bat - echo b) Copy C:\Program Files\Go\ into toolchain\go\ from a PC that has Go - echo. +if not exist "%GO_DEST%" ( + echo ERROR: Go archive missing after copy/download. pause exit /b 1 ) echo [Go] Extracting toolchain... -powershell -NoProfile -Command "Expand-Archive -Path '%GO_DEST%' -DestinationPath '%ROOT%\toolchain' -Force" +powershell -NoProfile -Command "Expand-Archive -LiteralPath '%GO_DEST%' -DestinationPath '%ROOT%\toolchain' -Force" +if errorlevel 1 ( + echo ERROR: Extraction failed. Check free disk space and path: %ROOT%\toolchain + pause + exit /b 1 +) del "%GO_DEST%" 2>nul if not exist "%BUNDLED_GO%" ( - echo ERROR: Extraction failed. + echo ERROR: Extraction failed — %BUNDLED_GO% not found. + echo Delete toolchain\go and toolchain\pack, then re-run LAUNCH.bat. pause exit /b 1 ) @@ -146,76 +170,24 @@ if not exist "%ROOT%\data\blueprints" mkdir "%ROOT%\data\blueprints" if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps" :: ---------------------------------------------------------------- -:: 5. Cloudflare Tunnel — MSI + token service (no JSON setup) +:: 5. Cloudflare Tunnel - start in background window + 60s watchdog :: ---------------------------------------------------------------- set "SERVER_PORT=8989" -set "CF_MSI=%ROOT%\cloudflare\cloudflared-windows-amd64.msi" -if not exist "%CF_MSI%" set "CF_MSI=%~dp0cloudflared-windows-amd64.msi" -if not exist "%CF_MSI%" set "CF_MSI=%~dp0..\cloudflared-windows-amd64.msi" -set "CF_TOKEN=eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9" -set "CF_HOSTNAME=killa.thetempleofdoom.com" -set "CF_BIN=" +set "CF_HOSTNAME=aether.thetempleofdoom.com" set "CF_READY=0" -where cloudflared >nul 2>nul -if not errorlevel 1 ( - set "CF_BIN=cloudflared" - goto cf_check_service -) -if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( - set "CF_BIN=%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" - goto cf_check_service -) -if exist "%ProgramFiles(x86)%\cloudflare\cloudflared\cloudflared.exe" ( - set "CF_BIN=%ProgramFiles(x86)%\cloudflare\cloudflared\cloudflared.exe" - goto cf_check_service -) - -if not exist "%CF_MSI%" ( - echo [CF] WARNING: cloudflared not found and no MSI bundled. Skipping tunnel. +if not exist "%ROOT%\cloudflare\start-tunnel.ps1" ( + echo [CF] WARNING: cloudflare\start-tunnel.ps1 missing - tunnel skipped. goto cf_done ) -echo [CF] Installing cloudflared from bundled MSI... -msiexec /i "%CF_MSI%" /quiet /norestart -echo [CF] Waiting for MSI to complete... -ping -n 10 127.0.0.1 >nul -where cloudflared >nul 2>nul -if not errorlevel 1 ( set "CF_BIN=cloudflared" & goto cf_check_service ) -if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( - set "CF_BIN=%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" - goto cf_check_service -) -echo [CF] WARNING: MSI installed but cloudflared still not found. Run as Administrator. -goto cf_done +echo. +echo [CF] Starting Cloudflare tunnel for https://%CF_HOSTNAME% ... +echo [CF] Log: %ROOT%\data\logs\tunnel.log +start "AetherForge Tunnel" powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\cloudflare\start-tunnel.ps1" -Root "%ROOT%" -:cf_check_service -echo [CF] Binary: !CF_BIN! - -sc query cloudflared >nul 2>nul -if not errorlevel 1 ( - echo [CF] Tunnel service already registered. - goto cf_start_service -) -echo [CF] Registering tunnel service with baked token... -"!CF_BIN!" service install %CF_TOKEN% -if errorlevel 1 ( - echo [CF] WARNING: service install failed. Run LAUNCH.bat as Administrator once. - goto cf_done -) -echo [CF] Tunnel service registered. -ping -n 3 127.0.0.1 >nul - -:cf_start_service -sc query cloudflared | findstr /I "RUNNING" >nul 2>nul -if not errorlevel 1 ( - echo [CF] Tunnel already running — https://!CF_HOSTNAME! - set "CF_READY=1" - goto cf_done -) -net start cloudflared >nul 2>nul -ping -n 4 127.0.0.1 >nul -echo [CF] Tunnel live: https://!CF_HOSTNAME! +:: Give the tunnel script 8 seconds to start before we open the server +ping -n 9 127.0.0.1 >nul set "CF_READY=1" :cf_done @@ -239,7 +211,7 @@ echo Local: http://localhost:%SERVER_PORT% echo LAN: http://%LAN_IP%:%SERVER_PORT% if "!CF_READY!"=="1" ( echo Public: https://!CF_HOSTNAME! - echo Dropper PS: iex -irm 'https://!CF_HOSTNAME!/install.ps1' + echo Dropper PS: iex -irm https://!CF_HOSTNAME!/install.ps1 ) echo Data: %ROOT%\data\ echo. diff --git a/agent/deploy/uninstall.go b/agent/deploy/uninstall.go index 7b0f8db..b8d8c22 100644 --- a/agent/deploy/uninstall.go +++ b/agent/deploy/uninstall.go @@ -45,4 +45,4 @@ func EnsureFirewallExclusion(cfg config.RuntimeConfig, binPath string) { // RemoveFirewallExclusion is implemented per platform. func RemoveFirewallExclusion(cfg config.RuntimeConfig) { platformRemoveFirewall(cfg) -} \ No newline at end of file +} diff --git a/cloudflare/SETUP.bat b/cloudflare/SETUP.bat new file mode 100644 index 0000000..b5e4a90 --- /dev/null +++ b/cloudflare/SETUP.bat @@ -0,0 +1,4 @@ +@echo off +echo Tunnel token is baked into LAUNCH.bat — no JSON setup needed. +echo Go up one folder and run LAUNCH.bat as Administrator once. +pause diff --git a/cloudflare/SETUP.txt b/cloudflare/SETUP.txt new file mode 100644 index 0000000..f48ccc1 --- /dev/null +++ b/cloudflare/SETUP.txt @@ -0,0 +1,47 @@ +======================================================== + AetherForge — Cloudflare Tunnel + https://killa.thetempleofdoom.com +======================================================== + +FIRST RUN (once per PC) +----------------------- +1. Double-click LAUNCH.bat +2. Approve the UAC prompt when asked — tunnel install needs Administrator +3. LAUNCH.bat installs cloudflared to: + C:\ProgramData\AetherForge\bin\cloudflared.exe + (stable path on the PC, not on the USB drive) +4. Registers the Windows service "cloudflared" with your tunnel token +5. Starts the service — connector shows Healthy in Cloudflare Zero Trust + +You can also run manually (as Administrator): + cloudflare\install-tunnel.bat + +VERIFY ON THE PC +---------------- + sc query cloudflared + +Look for STATE : 4 RUNNING + +Cloudflare dashboard: + Zero Trust → Networks → Tunnels → your tunnel → Healthy connector + +Public hostname route (required): + killa.thetempleofdoom.com → http://localhost:8989 + +End-to-end test (AetherForge must be running on :8989): + https://killa.thetempleofdoom.com + +TROUBLESHOOTING +--------------- +- No [CF] lines in LAUNCH? Old USB copy — re-copy usb\ from pack-usb.bat +- sc query shows nothing / service missing? Run LAUNCH.bat as Administrator once +- Service exists but not RUNNING? net start cloudflared +- Public URL 502? AetherForge not listening on localhost:8989 +- Tunnel Healthy but no public URL? Add hostname route in Cloudflare dashboard + +Dropper (after tunnel + server are up): + iex (irm 'https://killa.thetempleofdoom.com/install.ps1') + +credentials.json is NOT used — token is in install-tunnel.bat / LAUNCH.bat. + +======================================================== diff --git a/cloudflare/credentials.json b/cloudflare/credentials.json new file mode 100644 index 0000000..89cc299 --- /dev/null +++ b/cloudflare/credentials.json @@ -0,0 +1,7 @@ +{ + "__SETUP_REQUIRED__": "Replace this file with your real tunnel credentials JSON.", + "__INSTRUCTIONS__": "See SETUP.txt in this folder.", + "AccountTag": "", + "TunnelSecret": "", + "TunnelID": "" +} diff --git a/cloudflare/install-tunnel.bat b/cloudflare/install-tunnel.bat new file mode 100644 index 0000000..499e199 --- /dev/null +++ b/cloudflare/install-tunnel.bat @@ -0,0 +1,163 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion +title AetherForge — Cloudflare Tunnel Setup + +:: Must run elevated (Windows service install requires Administrator). +net session >nul 2>&1 +if errorlevel 1 ( + echo. + echo [CF] ERROR: Administrator rights required. + echo Right-click LAUNCH.bat and choose "Run as administrator" once. + echo. + exit /b 1 +) + +set "ROOT=%~1" +if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" +if "%ROOT%"=="" ( + echo [CF] ERROR: Missing deck root argument. + exit /b 1 +) + +set "CF_CONFIG_JSON=%ROOT%\cloudflare\tunnel-config.json" +set "CF_MSI=%ROOT%\cloudflare\cloudflared-windows-amd64.msi" +set "CF_PORTABLE=%ROOT%\cloudflare\cloudflared.exe" +set "CF_DIR=%ProgramData%\AetherForge\.cloudflared" +set "DEST_DIR=%ProgramData%\AetherForge\bin" +set "CF_EXE=%DEST_DIR%\cloudflared.exe" +set "CF_CONFIG=%CF_DIR%\config.yml" +set "CF_WRITER=%ROOT%\cloudflare\write-tunnel-config.ps1" + +echo. +echo ================================================================ +echo AetherForge — Cloudflare Tunnel Install +echo ================================================================ +echo Deck: %ROOT% +echo Config: %CF_CONFIG% +echo ================================================================ +echo. + +if not exist "%CF_CONFIG_JSON%" ( + echo [CF] ERROR: Missing %CF_CONFIG_JSON% + exit /b 1 +) +if not exist "%CF_WRITER%" ( + echo [CF] ERROR: Missing %CF_WRITER% + exit /b 1 +) + +if not exist "%DEST_DIR%" mkdir "%DEST_DIR%" + +:: Obtain cloudflared.exe — stable path on this PC, not the USB drive. +if exist "%CF_EXE%" goto have_bin + +if exist "%CF_PORTABLE%" ( + echo [CF] Copying bundled cloudflared.exe to ProgramData... + copy /y "%CF_PORTABLE%" "%CF_EXE%" >nul + if exist "%CF_EXE%" goto have_bin +) + +if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( + echo [CF] Copying installed cloudflared to ProgramData... + copy /y "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" "%CF_EXE%" >nul + if exist "%CF_EXE%" goto have_bin +) + +if exist "%CF_MSI%" ( + echo [CF] Installing cloudflared from MSI... + msiexec /i "%CF_MSI%" /quiet /norestart + echo [CF] Waiting for MSI to finish... + ping -n 15 127.0.0.1 >nul + if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( + copy /y "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" "%CF_EXE%" >nul + if exist "%CF_EXE%" goto have_bin + ) +) + +echo [CF] ERROR: cloudflared binary not found. +exit /b 1 + +:have_bin +echo [CF] Binary ready: %CF_EXE% + +:: Write credentials.json + config.yml with explicit local ingress route. +echo [CF] Writing local tunnel config: hostname to origin... + +powershell -NoProfile -ExecutionPolicy Bypass -File "%CF_WRITER%" -ConfigJson "%CF_CONFIG_JSON%" -CloudflaredDir "%CF_DIR%" > "%TEMP%\af-cf-writer.out" 2>&1 +for /f "usebackq tokens=1,* delims==" %%A in ("%TEMP%\af-cf-writer.out") do ( + if /i "%%A"=="TUNNEL_ID" set "TUNNEL_ID=%%B" + if /i "%%A"=="HOSTNAME" set "CF_HOSTNAME=%%B" + if /i "%%A"=="ORIGIN" set "CF_ORIGIN=%%B" +) + +if not exist "%CF_CONFIG%" ( + echo [CF] ERROR: Failed to write %CF_CONFIG% + exit /b 1 +) + +echo [CF] Route: https://!CF_HOSTNAME! -^> !CF_ORIGIN! +echo [CF] Config: %CF_CONFIG% +type "%CF_CONFIG%" +echo. + +echo [CF] Validating ingress rules... +"%CF_EXE%" --config "%CF_CONFIG%" tunnel ingress validate +if errorlevel 1 ( + echo [CF] WARNING: ingress validation failed - check config.yml +) + +:: Re-register service so it uses config.yml - not token-only remote routing. +sc query cloudflared >nul 2>&1 +if errorlevel 1 goto cf_register_service +echo [CF] Removing old cloudflared service - switching to local config routing... +net stop cloudflared >nul 2>&1 +sc delete cloudflared >nul 2>&1 +ping -n 3 127.0.0.1 >nul + +:cf_register_service +echo [CF] Registering Windows service with local config.yml... +"%CF_EXE%" --config "%CF_CONFIG%" service install +if errorlevel 1 ( + echo [CF] ERROR: service install failed. + exit /b 1 +) +ping -n 3 127.0.0.1 >nul + +echo [CF] Ensuring DNS route for !CF_HOSTNAME!... +"%CF_EXE%" --config "%CF_CONFIG%" tunnel route dns !TUNNEL_ID! !CF_HOSTNAME! +if errorlevel 1 goto cf_dns_manual +goto cf_dns_done +:cf_dns_manual +set "CF_CNAME_TARGET=%TUNNEL_ID%.cfargotunnel.com" +echo [CF] NOTE: DNS route command failed - add CNAME in Cloudflare DNS manually: +echo %CF_HOSTNAME% CNAME %CF_CNAME_TARGET% +:cf_dns_done + +sc query cloudflared | findstr /I "RUNNING" >nul 2>&1 +if errorlevel 1 goto cf_start_service +goto show_status +:cf_start_service +echo [CF] Starting cloudflared service... +net start cloudflared + +:show_status +echo. +echo --- cloudflared service status --- +sc query cloudflared +echo -------------------------------- +echo. +echo --- expected ingress - local config --- +type "%CF_CONFIG%" +echo ------------------------------------- +echo. + +sc query cloudflared | findstr /I "RUNNING" >nul 2>&1 +if errorlevel 1 ( + echo [CF] Tunnel connector is NOT running on this PC. + exit /b 1 +) + +echo [CF] Tunnel connector running with LOCAL routing config. +echo [CF] Public URL when AetherForge is on !CF_ORIGIN!: https://!CF_HOSTNAME! +echo [CF] Run cloudflare\verify-tunnel.bat to test DNS + local + public reachability. +exit /b 0 diff --git a/cloudflare/repair-tunnel.ps1 b/cloudflare/repair-tunnel.ps1 new file mode 100644 index 0000000..133b8f3 --- /dev/null +++ b/cloudflare/repair-tunnel.ps1 @@ -0,0 +1,115 @@ +param( + [Parameter(Mandatory = $true)][string]$Root, + [ValidateRange(1, 4)][int]$Level = 1 +) + +$ErrorActionPreference = 'Continue' +$Root = $Root.TrimEnd('\') +$configJson = Join-Path $Root 'cloudflare\tunnel-config.json' +$installBat = Join-Path $Root 'cloudflare\install-tunnel.bat' +$writer = Join-Path $Root 'cloudflare\write-tunnel-config.ps1' +$cfDir = Join-Path $env:ProgramData 'AetherForge\.cloudflared' +$cfConfig = Join-Path $cfDir 'config.yml' +$cfExe = Join-Path $env:ProgramData 'AetherForge\bin\cloudflared.exe' + +function Write-RepairLog([string]$Message) { + $line = "{0} [CF-HEAL-L{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message + Write-Output $line +} + +function Get-ServiceRunning { + $q = sc.exe query cloudflared 2>&1 | Out-String + return ($q -match 'STATE\s+:\s+\d+\s+RUNNING') +} + +function Start-CloudflaredService { + $null = net.exe start cloudflared 2>&1 + Start-Sleep -Seconds 3 + return (Get-ServiceRunning) +} + +function Stop-CloudflaredService { + $null = net.exe stop cloudflared 2>&1 + Start-Sleep -Seconds 2 +} + +function Restart-CloudflaredService { + Stop-CloudflaredService + return (Start-CloudflaredService) +} + +Write-RepairLog "Repair level $Level starting..." + +switch ($Level) { + 1 { + Write-RepairLog "Action: net start cloudflared" + if (Start-CloudflaredService) { + Write-RepairLog "Service is RUNNING" + exit 0 + } + Write-RepairLog "net start failed" + exit 1 + } + 2 { + Write-RepairLog "Action: restart cloudflared service" + if (Restart-CloudflaredService) { + Write-RepairLog "Service restarted OK" + exit 0 + } + Write-RepairLog "Restart failed" + exit 1 + } + 3 { + Write-RepairLog "Action: rewrite config.yml + restart + DNS route" + if (-not (Test-Path $configJson)) { + Write-RepairLog "Missing tunnel-config.json" + exit 1 + } + if (-not (Test-Path $writer)) { + Write-RepairLog "Missing write-tunnel-config.ps1" + exit 1 + } + $tunnelId = $null + $hostname = $null + & $writer -ConfigJson $configJson -CloudflaredDir $cfDir | ForEach-Object { + if ($_ -match '^TUNNEL_ID=(.+)$') { $tunnelId = $Matches[1] } + if ($_ -match '^HOSTNAME=(.+)$') { $hostname = $Matches[1] } + Write-RepairLog $_ + } + if (-not (Test-Path $cfConfig)) { + Write-RepairLog "config.yml write failed" + exit 1 + } + Restart-CloudflaredService | Out-Null + if ($cfExe -and (Test-Path $cfExe) -and $tunnelId -and $hostname) { + Write-RepairLog "Action: tunnel route dns $hostname" + & $cfExe --config $cfConfig tunnel route dns $tunnelId $hostname 2>&1 | ForEach-Object { Write-RepairLog $_ } + } + if (Get-ServiceRunning) { + Write-RepairLog "Config refresh + service RUNNING" + exit 0 + } + Write-RepairLog "Still not RUNNING after config refresh" + exit 1 + } + 4 { + Write-RepairLog "Action: full tunnel reinstall (install-tunnel.bat)" + if (-not (Test-Path $installBat)) { + Write-RepairLog "Missing install-tunnel.bat" + exit 1 + } + $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator + ) + if ($isAdmin) { + & cmd.exe /c "`"$installBat`" `"$Root`"" + exit $LASTEXITCODE + } + Write-RepairLog "Requesting elevated reinstall (UAC)..." + $p = Start-Process -FilePath $installBat -ArgumentList "`"$Root`"" -Verb RunAs -Wait -PassThru + exit $p.ExitCode + } +} + +Write-RepairLog "Unknown level" +exit 1 diff --git a/cloudflare/start-tunnel.ps1 b/cloudflare/start-tunnel.ps1 new file mode 100644 index 0000000..877409d --- /dev/null +++ b/cloudflare/start-tunnel.ps1 @@ -0,0 +1,302 @@ +#Requires -Version 5 +param([string]$Root) +$ErrorActionPreference = 'Continue' +$Root = ($Root -replace '\\$','') + +# --- Config (overridden by tunnel-config.json if present) -------------------- +$TOKEN = 'eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9' +$HOSTNAME = 'killa.thetempleofdoom.com' +$ORIGIN = 'http://127.0.0.1:8989' +$SVC = 'cloudflared' +$DESTDIR = "$env:ProgramData\AetherForge\bin" +$LOGDIR = "$Root\data\logs" +$LOGFILE = "$LOGDIR\tunnel.log" +$PIDFILE = "$env:ProgramData\AetherForge\tunnel.pid" +$INTERVAL = 60 + +$cfgPath = Join-Path $PSScriptRoot 'tunnel-config.json' +if (Test-Path $cfgPath) { + try { + $c = Get-Content $cfgPath -Raw | ConvertFrom-Json + if ($c.tunnel_token) { $TOKEN = $c.tunnel_token } + if ($c.hostname) { $HOSTNAME = $c.hostname } + if ($c.origin) { $ORIGIN = $c.origin } + if ($c.watch_interval_seconds){ $INTERVAL = [int]$c.watch_interval_seconds } + } catch {} +} + +# --- Helpers ----------------------------------------------------------------- +function Log([string]$msg, [string]$tag = 'INFO') { + $line = '{0} [TUNNEL][{1}] {2}' -f (Get-Date -Format 'HH:mm:ss'), $tag, $msg + Write-Host $line + try { Add-Content -LiteralPath $LOGFILE -Value $line -Encoding UTF8 } catch {} +} + +function SvcState { + $q = (& sc.exe query $SVC 2>&1) -join ' ' + if ($q -match '1060|does not exist') { return 'MISSING' } + if ($q -match 'RUNNING') { return 'RUNNING' } + if ($q -match 'STOPPED') { return 'STOPPED' } + return 'UNKNOWN' +} + +function IsAdmin { + ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() + ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function HttpGet([string]$url, [int]$sec = 10) { + try { + $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec $sec + return [pscustomobject]@{ Ok=$true; Code=[int]$r.StatusCode; Err=$null } + } catch { + $code = $null + if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } + return [pscustomobject]@{ Ok=$false; Code=$code; Err=$_.Exception.Message } + } +} + +# --- Locate cloudflared ------------------------------------------------------- +function FindCloudflared { + # 1. Already staged in ProgramData + if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } + + New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null + + # 2. Bundled next to this script on the USB + $bundled = Join-Path $PSScriptRoot 'cloudflared.exe' + if (Test-Path $bundled) { + Copy-Item $bundled "$DESTDIR\cloudflared.exe" -Force + return "$DESTDIR\cloudflared.exe" + } + + # 3. MSI-installed on this PC + foreach ($dir in @("$env:ProgramFiles\cloudflare\cloudflared", + "${env:ProgramFiles(x86)}\cloudflare\cloudflared")) { + if (Test-Path "$dir\cloudflared.exe") { + Copy-Item "$dir\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force + return "$DESTDIR\cloudflared.exe" + } + } + + # 4. Run the MSI if bundled + $msi = Join-Path $PSScriptRoot 'cloudflared-windows-amd64.msi' + if (Test-Path $msi) { + Log 'Installing cloudflared MSI...' 'SETUP' + Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait + Start-Sleep -Seconds 6 + if (Test-Path "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe") { + Copy-Item "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force + return "$DESTDIR\cloudflared.exe" + } + } + + # 5. Download from GitHub as last resort + Log 'Downloading cloudflared from GitHub...' 'SETUP' + try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe' + Invoke-WebRequest -Uri $url -OutFile "$DESTDIR\cloudflared.exe" -UseBasicParsing + if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } + } catch { Log "Download failed: $_" 'ERROR' } + + return $null +} + +# ============================================================================= +# MAIN +# ============================================================================= +New-Item -ItemType Directory -Force -Path $LOGDIR | Out-Null +New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null + +Log "Tunnel setup hostname=$HOSTNAME origin=$ORIGIN" 'SETUP' + +$CF = FindCloudflared +if (-not $CF) { + Log 'Cannot locate cloudflared.exe - tunnel skipped.' 'ERROR' + exit 1 +} +Log "Using: $CF" 'SETUP' + +# --- Install + start as Windows service (admin path) ------------------------- +$asService = $false + +if (IsAdmin) { + # Stop and remove any existing service + if ((SvcState) -ne 'MISSING') { + Log 'Removing existing cloudflared service...' 'SETUP' + & net.exe stop $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 2 + & sc.exe delete $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 2 + } + + # Clear stale EventLog registry key left by previous installs. + # cloudflared rolls back the entire service install if this key already exists. + $evtKey = 'HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application\Cloudflared' + if (Test-Path $evtKey) { + Log 'Removing stale EventLog registry key...' 'SETUP' + Remove-Item -LiteralPath $evtKey -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + } + + Log 'Installing cloudflared Windows service...' 'SETUP' + # Capture output without triggering PS NativeCommandError decorations + $installOut = (& $CF service install $TOKEN 2>&1) -join "`n" + Log $installOut.Trim() 'SETUP' + Start-Sleep -Seconds 3 + + Log 'Starting service...' 'SETUP' + & net.exe start $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 6 + + if ((SvcState) -eq 'RUNNING') { + Log 'Service RUNNING' 'OK' + $asService = $true + } else { + Log "Service not RUNNING after install (state=$(SvcState)) - falling back to background process." 'WARN' + } +} else { + Log 'Not running as Administrator.' 'WARN' + Log 'For a persistent Windows service: right-click LAUNCH.bat > Run as administrator.' 'WARN' + if ((SvcState) -eq 'RUNNING') { + Log 'Existing cloudflared service is RUNNING - using it.' 'OK' + $asService = $true + } +} + +# --- Run directly as background process (non-admin fallback) ----------------- +$bgProc = $null +if (-not $asService) { + Log 'Starting cloudflared as background process...' 'SETUP' + + # Kill leftover from previous run + if (Test-Path $PIDFILE) { + $oldpid = [int](Get-Content $PIDFILE -Raw -ErrorAction SilentlyContinue) + if ($oldpid) { Stop-Process -Id $oldpid -Force -ErrorAction SilentlyContinue } + } + + $si = New-Object System.Diagnostics.ProcessStartInfo + $si.FileName = $CF + $si.Arguments = "tunnel run --token $TOKEN" + $si.UseShellExecute = $false + $si.CreateNoWindow = $true + + $bgProc = [System.Diagnostics.Process]::Start($si) + Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII + Log "cloudflared running (PID $($bgProc.Id))" 'SETUP' + Start-Sleep -Seconds 5 +} + +# --- Wait up to 20s for cloudflared to establish connection ------------------ +# NOTE: We do NOT check the public URL here because AetherForge hasn't started +# yet (LAUNCH.bat starts it after this script returns). A 502 at this stage is +# normal - it just means the tunnel is connected but origin isn't up yet. +# The watchdog (below) will report OK once AetherForge is running. +Log "Giving cloudflared 20s to connect to Cloudflare..." 'SETUP' +Start-Sleep -Seconds 20 +$finalState = SvcState +if ($asService) { + if ($finalState -eq 'RUNNING') { + Log "Tunnel connector ready (service=$finalState). AetherForge starting next." 'OK' + } else { + Log "Service state=$finalState after 20s - may still be connecting." 'WARN' + } +} else { + if ($bgProc -and -not $bgProc.HasExited) { + Log "Tunnel connector ready (background PID $($bgProc.Id)). AetherForge starting next." 'OK' + } else { + Log "cloudflared process exited unexpectedly." 'ERROR' + } +} +Log "Public URL will be live once AetherForge starts: https://$HOSTNAME" 'SETUP' + +# ============================================================================= +# WATCHDOG - runs forever; auto-restarts cloudflared if it goes down +# ============================================================================= +Log "Watchdog started: checking every ${INTERVAL}s. Log: $LOGFILE" 'WATCH' +$fails = 0 + +while ($true) { + Start-Sleep -Seconds $INTERVAL + + $svc = SvcState + $pub = HttpGet "https://$HOSTNAME/" 12 + $procOk = ($bgProc -ne $null) -and (-not $bgProc.HasExited) + + # In background-process mode the service will always be MISSING - that is fine. + $connectorOk = ($asService -and $svc -eq 'RUNNING') -or (-not $asService -and $procOk) + $ok = $pub.Ok -and $connectorOk + + if ($ok) { + if ($fails -gt 0) { Log "Recovered after $fails failure(s)." 'OK' } + else { Log "OK svc=$svc public=HTTP $($pub.Code)" 'WATCH' } + $fails = 0 + continue + } + + $fails++ + $pubLabel = if ($pub.Ok) { "HTTP $($pub.Code)" } else { $pub.Err } + $connLabel = if ($asService) { "svc=$svc" } else { "proc=$(if($procOk){'alive'}else{'dead'})" } + Log "DEGRADED #$fails $connLabel public=$pubLabel" 'WARN' + + if ($asService) { + # --- Service mode healing --- + if ($svc -ne 'RUNNING') { + Log 'Restart: net start cloudflared' 'HEAL' + & net.exe start $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 5 + if ((SvcState) -eq 'RUNNING') { + Log 'Service restarted OK.' 'HEAL' + $fails = 0 + } elseif ($fails -ge 3 -and (IsAdmin)) { + Log 'Reinstalling cloudflared service after 3 failures...' 'HEAL' + & net.exe stop $SVC 2>&1 | Out-Null + & sc.exe delete $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 2 + & $CF service install $TOKEN 2>&1 | Out-Null + Start-Sleep -Seconds 2 + & net.exe start $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 5 + if ((SvcState) -eq 'RUNNING') { + Log 'Service reinstalled and RUNNING.' 'HEAL' + $fails = 0 + } else { + Log 'Reinstall failed.' 'ERROR' + } + } + } else { + # Service running but public URL unreachable + Log "Service OK but public URL unreachable. Check Cloudflare dashboard route and that AetherForge is on port 8989." 'WARN' + } + } else { + # --- Background process mode healing --- + if ($bgProc -and $bgProc.HasExited) { + Log "cloudflared process exited - restarting..." 'HEAL' + $si2 = New-Object System.Diagnostics.ProcessStartInfo + $si2.FileName = $CF + $si2.Arguments = "tunnel run --token $TOKEN" + $si2.UseShellExecute = $false + $si2.CreateNoWindow = $true + $bgProc = [System.Diagnostics.Process]::Start($si2) + Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII + Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' + $fails = 0 + Start-Sleep -Seconds 8 + } elseif ($fails -ge 3) { + Log "3 failures, killing and restarting cloudflared..." 'HEAL' + if ($bgProc -and -not $bgProc.HasExited) { $bgProc.Kill() } + Start-Sleep -Seconds 2 + $si3 = New-Object System.Diagnostics.ProcessStartInfo + $si3.FileName = $CF + $si3.Arguments = "tunnel run --token $TOKEN" + $si3.UseShellExecute = $false + $si3.CreateNoWindow = $true + $bgProc = [System.Diagnostics.Process]::Start($si3) + Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII + Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' + $fails = 0 + Start-Sleep -Seconds 8 + } + } +} diff --git a/cloudflare/tunnel-config.json b/cloudflare/tunnel-config.json new file mode 100644 index 0000000..2ee2679 --- /dev/null +++ b/cloudflare/tunnel-config.json @@ -0,0 +1,6 @@ +{ + "tunnel_token": "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9", + "hostname": "killa.thetempleofdoom.com", + "origin": "http://127.0.0.1:8989", + "watch_interval_seconds": 60 +} diff --git a/cloudflare/tunnel-watch.ps1 b/cloudflare/tunnel-watch.ps1 new file mode 100644 index 0000000..43ff06d --- /dev/null +++ b/cloudflare/tunnel-watch.ps1 @@ -0,0 +1,158 @@ +param( + [Parameter(Mandatory = $true)][string]$Root +) + +$ErrorActionPreference = 'Continue' +$Root = $Root.TrimEnd('\') + +$configPath = Join-Path $Root 'cloudflare\tunnel-config.json' +$repairScript = Join-Path $Root 'cloudflare\repair-tunnel.ps1' +$logDir = Join-Path $Root 'data\logs' +$logFile = Join-Path $logDir 'tunnel-watch.log' +$stateFile = Join-Path $env:ProgramData 'AetherForge\tunnel-watch-state.json' +$lockFile = Join-Path $env:ProgramData 'AetherForge\tunnel-watch.pid' + +if (-not (Test-Path $configPath)) { + Write-Host "[CF-WATCH] ERROR: missing $configPath" + exit 1 +} + +$cfg = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json +$hostname = [string]$cfg.hostname +$origin = [string]$cfg.origin +$interval = if ($cfg.watch_interval_seconds) { [int]$cfg.watch_interval_seconds } else { 60 } +$publicTimeout = if ($cfg.public_check_timeout_seconds) { [int]$cfg.public_check_timeout_seconds } else { 20 } +$originTimeout = if ($cfg.origin_check_timeout_seconds) { [int]$cfg.origin_check_timeout_seconds } else { 8 } +$healCfg = $cfg.heal_after_failures +$lvlRestart = if ($healCfg.restart_service) { [int]$healCfg.restart_service } else { 1 } +$lvlRewrite = if ($healCfg.rewrite_config_and_restart) { [int]$healCfg.rewrite_config_and_restart } else { 2 } +$lvlReinstall = if ($healCfg.full_reinstall) { [int]$healCfg.full_reinstall } else { 4 } + +New-Item -ItemType Directory -Force -Path $logDir | Out-Null +New-Item -ItemType Directory -Force -Path (Split-Path $stateFile) | Out-Null +Set-Content -LiteralPath $lockFile -Value $PID -Encoding ASCII + +function Write-WatchLog([string]$Message, [string]$Level = 'INFO') { + $line = '{0} [CF-WATCH] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message + Write-Host $line + Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8 +} + +function Get-State { + if (Test-Path $stateFile) { + try { return Get-Content $stateFile -Raw | ConvertFrom-Json } catch { } + } + return [pscustomobject]@{ + consecutive_failures = 0 + last_heal_level = 0 + last_heal_at = $null + last_ok = $null + } +} + +function Set-State($state) { + $state | ConvertTo-Json | Set-Content -LiteralPath $stateFile -Encoding UTF8 +} + +function Test-ServiceRunning { + $q = sc.exe query cloudflared 2>&1 | Out-String + if ($q -match '1060|does not exist') { return 'MISSING' } + if ($q -match 'STATE\s+:\s+\d+\s+RUNNING') { return 'RUNNING' } + if ($q -match 'STATE\s+:\s+\d+\s+STOPPED') { return 'STOPPED' } + return 'UNKNOWN' +} + +function Test-Http([string]$Url, [int]$TimeoutSec) { + try { + $r = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec $TimeoutSec + return [pscustomobject]@{ Ok = $true; Code = [int]$r.StatusCode; Error = $null } + } catch { + $code = $null + if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } + return [pscustomobject]@{ Ok = $false; Code = $code; Error = $_.Exception.Message } + } +} + +function Invoke-Heal([int]$Level) { + Write-WatchLog "Self-heal level $Level triggered" 'HEAL' + if (-not (Test-Path $repairScript)) { + Write-WatchLog "repair-tunnel.ps1 missing — cannot heal" 'ERROR' + return $false + } + $out = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $repairScript -Root $Root -Level $Level 2>&1 + foreach ($line in $out) { Write-WatchLog $line 'HEAL' } + return ($LASTEXITCODE -eq 0) +} + +Write-WatchLog "Watchdog started — interval ${interval}s | public=https://$hostname/ | origin=$origin" +Write-WatchLog "Log file: $logFile" + +while ($true) { + $state = Get-State + $svc = Test-ServiceRunning + $originCheck = Test-Http ($origin.TrimEnd('/') + '/') $originTimeout + $publicCheck = Test-Http ("https://$hostname/") $publicTimeout + + $originLabel = if ($originCheck.Ok) { "OK $($originCheck.Code)" } else { "FAIL $($originCheck.Error)" } + $publicLabel = if ($publicCheck.Ok) { "OK $($publicCheck.Code)" } else { "FAIL $($publicCheck.Error)" } + + $allOk = ($svc -eq 'RUNNING') -and $originCheck.Ok -and $publicCheck.Ok + + if ($allOk) { + if ($state.consecutive_failures -gt 0) { + Write-WatchLog "Recovered — service=$svc origin=$originLabel public=$publicLabel" 'OK' + } else { + Write-WatchLog "OK service=$svc origin=$originLabel public=$publicLabel" 'OK' + } + Set-State ([pscustomobject]@{ + consecutive_failures = 0 + last_heal_level = 0 + last_heal_at = $null + last_ok = (Get-Date).ToString('o') + }) + } else { + $state.consecutive_failures = [int]$state.consecutive_failures + 1 + $fail = $state.consecutive_failures + Write-WatchLog "DEGRADED (#$fail) service=$svc origin=$originLabel public=$publicLabel" 'WARN' + + $healLevel = 0 + if ($svc -eq 'MISSING' -or $fail -ge $lvlReinstall) { + $healLevel = 4 + } elseif ($svc -ne 'RUNNING' -and $fail -ge $lvlRestart) { + $healLevel = if ($fail -ge $lvlRewrite) { 3 } else { 1 } + } elseif ($svc -eq 'RUNNING' -and $originCheck.Ok -and -not $publicCheck.Ok) { + if ($fail -ge $lvlReinstall) { $healLevel = 4 } + elseif ($fail -ge $lvlRewrite) { $healLevel = 3 } + elseif ($fail -ge $lvlRestart) { $healLevel = 2 } + } elseif ($svc -eq 'RUNNING' -and -not $originCheck.Ok) { + Write-WatchLog "Origin down — start/repair AetherForge on $origin (tunnel cannot heal dead origin)" 'WARN' + } + + if ($healLevel -gt 0) { + $skipHeal = $false + if ($healLevel -eq 4 -and [int]$state.last_heal_level -eq 4 -and $state.last_heal_at) { + try { + $elapsed = (Get-Date) - [datetime]$state.last_heal_at + if ($elapsed.TotalMinutes -lt 10) { + Write-WatchLog "Reinstall cooldown (10 min) — skipping level 4 retry" 'WARN' + $skipHeal = $true + } + } catch { } + } + if (-not $skipHeal) { + $healed = Invoke-Heal $healLevel + $state.last_heal_level = $healLevel + $state.last_heal_at = (Get-Date).ToString('o') + if ($healed) { + Write-WatchLog "Heal level $healLevel completed — recheck next interval" 'HEAL' + } else { + Write-WatchLog "Heal level $healLevel failed — will retry on next check" 'ERROR' + } + } + } + + Set-State $state + } + + Start-Sleep -Seconds $interval +} diff --git a/cloudflare/verify-tunnel.bat b/cloudflare/verify-tunnel.bat new file mode 100644 index 0000000..1aba605 --- /dev/null +++ b/cloudflare/verify-tunnel.bat @@ -0,0 +1,52 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion +title AetherForge — Tunnel Verify + +set "ROOT=%~dp0.." +if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" + +set "CF_CONFIG_JSON=%ROOT%\cloudflare\tunnel-config.json" +set "CF_HOSTNAME=killa.thetempleofdoom.com" +set "CF_ORIGIN=http://127.0.0.1:8989" + +if exist "%CF_CONFIG_JSON%" ( + for /f "usebackq delims=" %%L in (`powershell -NoProfile -Command "(Get-Content '%CF_CONFIG_JSON%' -Raw | ConvertFrom-Json).hostname"`) do set "CF_HOSTNAME=%%L" + for /f "usebackq delims=" %%L in (`powershell -NoProfile -Command "(Get-Content '%CF_CONFIG_JSON%' -Raw | ConvertFrom-Json).origin"`) do set "CF_ORIGIN=%%L" +) + +echo. +echo ================================================================ +echo AetherForge — Tunnel Verification +echo ================================================================ +echo Hostname: https://%CF_HOSTNAME% +echo Origin: %CF_ORIGIN% +echo ================================================================ +echo. + +echo [1] cloudflared service +sc query cloudflared 2>nul +if errorlevel 1 echo FAIL: cloudflared service not installed +echo. + +echo [2] DNS for %CF_HOSTNAME% +nslookup %CF_HOSTNAME% 2>nul +echo NOTE: URL must be killa.thetempleofdoom.com (two l's), not kill. +echo. + +echo [3] Local AetherForge (%CF_ORIGIN%) +powershell -NoProfile -Command "try { $r = Invoke-WebRequest -Uri '%CF_ORIGIN%/' -UseBasicParsing -TimeoutSec 5; Write-Host (' OK HTTP ' + $r.StatusCode) } catch { Write-Host (' FAIL: ' + $_.Exception.Message) }" +echo. + +echo [4] Public tunnel https://%CF_HOSTNAME%/ +powershell -NoProfile -Command "try { $r = Invoke-WebRequest -Uri 'https://%CF_HOSTNAME%/' -UseBasicParsing -TimeoutSec 15; Write-Host (' OK HTTP ' + $r.StatusCode) } catch { Write-Host (' FAIL: ' + $_.Exception.Message) }" +echo. + +echo [5] Local config on this PC +if exist "%ProgramData%\AetherForge\.cloudflared\config.yml" ( + type "%ProgramData%\AetherForge\.cloudflared\config.yml" +) else ( + echo FAIL: %ProgramData%\AetherForge\.cloudflared\config.yml not found + echo Run LAUNCH.bat as Administrator once. +) +echo. +pause diff --git a/cloudflare/write-tunnel-config.ps1 b/cloudflare/write-tunnel-config.ps1 new file mode 100644 index 0000000..84ef769 --- /dev/null +++ b/cloudflare/write-tunnel-config.ps1 @@ -0,0 +1,47 @@ +param( + [Parameter(Mandatory = $true)][string]$ConfigJson, + [Parameter(Mandatory = $true)][string]$CloudflaredDir +) + +$ErrorActionPreference = 'Stop' +$cfg = Get-Content -LiteralPath $ConfigJson -Raw | ConvertFrom-Json + +if (-not $cfg.hostname) { throw 'tunnel-config.json: hostname is required' } +if (-not $cfg.origin) { throw 'tunnel-config.json: origin is required (e.g. http://127.0.0.1:8989)' } +if (-not $cfg.tunnel_token) { throw 'tunnel-config.json: tunnel_token is required' } + +$tokenJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($cfg.tunnel_token)) | ConvertFrom-Json +if (-not $tokenJson.a -or -not $tokenJson.t -or -not $tokenJson.s) { + throw 'tunnel_token is not a valid Cloudflare tunnel token' +} + +New-Item -ItemType Directory -Force -Path $CloudflaredDir | Out-Null + +$credPath = Join-Path $CloudflaredDir 'credentials.json' +$configPath = Join-Path $CloudflaredDir 'config.yml' + +$credentials = [ordered]@{ + AccountTag = [string]$tokenJson.a + TunnelSecret = [string]$tokenJson.s + TunnelID = [string]$tokenJson.t +} +($credentials | ConvertTo-Json) | Set-Content -LiteralPath $credPath -Encoding UTF8 + +$credPathYaml = ($credPath -replace '\\', '/') +$config = @" +# AetherForge local tunnel routing (written by install-tunnel.bat) +tunnel: $($tokenJson.t) +credentials-file: $credPathYaml + +ingress: + - hostname: $($cfg.hostname) + service: $($cfg.origin) + - service: http_status:404 +"@ +Set-Content -LiteralPath $configPath -Value $config -Encoding UTF8 + +Write-Output "TUNNEL_ID=$($tokenJson.t)" +Write-Output "HOSTNAME=$($cfg.hostname)" +Write-Output "ORIGIN=$($cfg.origin)" +Write-Output "CONFIG=$configPath" +Write-Output "CREDENTIALS=$credPath" diff --git a/pack-usb.bat b/pack-usb.bat index 75f8819..9976ba6 100644 --- a/pack-usb.bat +++ b/pack-usb.bat @@ -32,11 +32,40 @@ if errorlevel 1 ( if not exist "%USB%" mkdir "%USB%" +:: ---------------------------------------------------------------- +:: 0b. Ensure portable Go toolchain (forge on PCs without system Go) +:: ---------------------------------------------------------------- +echo [0/8] Ensuring Go toolchain... +if not exist "%USB%\toolchain" mkdir "%USB%\toolchain" +if not exist "%USB%\toolchain\pack" mkdir "%USB%\toolchain\pack" +set "GO_PACK=%USB%\toolchain\pack\go1.26.3.windows-amd64.zip" +if not exist "%USB%\toolchain\go\bin\go.exe" ( + if exist "C:\Program Files\Go\bin\go.exe" ( + echo [0/8] Copying Go from C:\Program Files\Go... + if exist "%USB%\toolchain\go" rd /s /q "%USB%\toolchain\go" + robocopy "C:\Program Files\Go" "%USB%\toolchain\go" /E /NFL /NDL /NJH /NJS /NC /NS >nul + ) +) +if not exist "%GO_PACK%" ( + if exist "%ROOT%\usb\toolchain\pack\go1.26.3.windows-amd64.zip" ( + copy /y "%ROOT%\usb\toolchain\pack\go1.26.3.windows-amd64.zip" "%GO_PACK%" >nul + echo [0/8] Offline Go zip copied from existing usb pack. + ) else ( + echo [0/8] Downloading offline Go zip for amd64... + powershell -NoProfile -Command "& { [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri 'https://go.dev/dl/go1.26.3.windows-amd64.zip' -OutFile '%GO_PACK%' }" + ) +) +if exist "%USB%\toolchain\go\bin\go.exe" ( + echo [0/8] Go toolchain ready in usb\toolchain\go\ +) else ( + echo [0/8] WARNING: Go not bundled — LAUNCH.bat will extract from pack zip on first run. +) + :: ---------------------------------------------------------------- :: 1. Build the frontend when npm is available :: ---------------------------------------------------------------- if not defined SKIP_FRONTEND ( - echo [1/7] Building frontend... + echo [1/8] Building frontend... cd "%ROOT%\server\web" call npm install --silent call npm run build @@ -47,15 +76,15 @@ if not defined SKIP_FRONTEND ( exit /b 1 ) cd "%ROOT%" - echo [1/7] Frontend built. + echo [1/8] Frontend built. ) else ( - echo [1/7] Skipped - npm not found. + echo [1/8] Skipped - npm not found. ) :: ---------------------------------------------------------------- :: 2. Build AetherForge.exe for Windows amd64 :: ---------------------------------------------------------------- -echo [2/7] Compiling AetherForge.exe... +echo [2/8] Compiling AetherForge.exe... cd "%ROOT%\server" set "CGO_ENABLED=0" set "GOOS=windows" @@ -68,38 +97,48 @@ if errorlevel 1 ( exit /b 1 ) cd "%ROOT%" -echo [2/7] AetherForge.exe ready. +echo [2/8] AetherForge.exe ready. :: ---------------------------------------------------------------- :: 3. Copy webroot - dashboard UI :: ---------------------------------------------------------------- -echo [3/7] Copying webroot... +echo [3/8] Copying webroot... if exist "%USB%\webroot" rd /s /q "%USB%\webroot" -if exist "%ROOT%\server\webroot" ( - xcopy /e /i /q "%ROOT%\server\webroot" "%USB%\webroot" >nul -) else if exist "%ROOT%\server\web\dist" ( +if exist "%ROOT%\server\web\dist" ( xcopy /e /i /q "%ROOT%\server\web\dist" "%USB%\webroot" >nul +) else if exist "%ROOT%\server\webroot" ( + xcopy /e /i /q "%ROOT%\server\webroot" "%USB%\webroot" >nul ) else ( echo ERROR: No webroot found. Run npm run build in server\web first. pause exit /b 1 ) -echo [3/7] webroot copied. +echo [3/8] webroot copied. :: ---------------------------------------------------------------- :: 4. Copy agent and fusion source - Forge needs these at runtime :: ---------------------------------------------------------------- -echo [4/7] Copying agent + fusion source... +echo [4/8] Copying agent + fusion source... if exist "%USB%\agent" rd /s /q "%USB%\agent" if exist "%USB%\fusion" rd /s /q "%USB%\fusion" -xcopy /e /i /q /EXCLUDE:"%ROOT%\usb_pack_exclude.txt" "%ROOT%\agent" "%USB%\agent" >nul -xcopy /e /i /q /EXCLUDE:"%ROOT%\usb_pack_exclude.txt" "%ROOT%\fusion" "%USB%\fusion" >nul -echo [4/7] Source trees copied. +robocopy "%ROOT%\agent" "%USB%\agent" /E /XD node_modules dist .git /XF crypto-miner-agent crypto-miner-fusion /NFL /NDL /NJH /NJS /NC /NS >nul +if errorlevel 8 ( + echo ERROR: Failed to copy agent source. + pause + exit /b 1 +) +robocopy "%ROOT%\fusion" "%USB%\fusion" /E /XD node_modules dist .git /XF crypto-miner-fusion /NFL /NDL /NJH /NJS /NC /NS >nul +if errorlevel 8 ( + echo ERROR: Failed to copy fusion source. + pause + exit /b 1 +) +echo [4/8] Source trees copied. :: ---------------------------------------------------------------- :: 5. Launcher + docs (always sync from project root) :: ---------------------------------------------------------------- -echo [5/7] Syncing LAUNCH.bat, README, cloudflare... +echo [5/8] Syncing LAUNCH.bat, README, cloudflare... copy /y "%ROOT%\LAUNCH.bat" "%USB%\LAUNCH.bat" >nul if not exist "%USB%\README.txt" ( if exist "%ROOT%\usb\README.txt" copy /y "%ROOT%\usb\README.txt" "%USB%\README.txt" >nul @@ -109,36 +148,47 @@ echo @echo off > "%USB%\run.bat" echo rem AetherForge portable root marker >> "%USB%\run.bat" if not exist "%USB%\cloudflare" mkdir "%USB%\cloudflare" -if exist "%ROOT%\usb\cloudflare\SETUP.txt" ( - copy /y "%ROOT%\usb\cloudflare\SETUP.txt" "%USB%\cloudflare\SETUP.txt" >nul -) -if exist "%ROOT%\cloudflared-windows-amd64.msi" ( - copy /y "%ROOT%\cloudflared-windows-amd64.msi" "%USB%\cloudflare\cloudflared-windows-amd64.msi" >nul - echo [5/7] cloudflared MSI bundled. -) else if exist "%ROOT%\usb\cloudflare\cloudflared-windows-amd64.msi" ( - echo [5/7] cloudflared MSI already in usb\cloudflare\ -) else ( - echo [5/7] WARNING: No cloudflared MSI found. Tunnel auto-install skipped. -) -:: Preserve existing credentials.json if present (never overwrite operator tunnel) -if not exist "%USB%\cloudflare\credentials.json" ( - if exist "%ROOT%\usb\cloudflare\credentials.json" ( - copy /y "%ROOT%\usb\cloudflare\credentials.json" "%USB%\cloudflare\credentials.json" >nul + +:: Core tunnel files — always sync from usb\cloudflare\ +for %%F in (start-tunnel.ps1 tunnel-config.json SETUP.txt) do ( + if exist "%ROOT%\usb\cloudflare\%%F" ( + copy /y "%ROOT%\usb\cloudflare\%%F" "%USB%\cloudflare\%%F" >nul ) ) -echo [5/7] Launcher synced. + +:: cloudflared binary — copy from usb\cloudflare\ or download +if exist "%ROOT%\usb\cloudflare\cloudflared.exe" ( + copy /y "%ROOT%\usb\cloudflare\cloudflared.exe" "%USB%\cloudflare\cloudflared.exe" >nul + echo [5/8] cloudflared.exe bundled. +) else ( + echo [5/8] Downloading cloudflared.exe... + powershell -NoProfile -Command "& { [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe' -OutFile '%USB%\cloudflare\cloudflared.exe' }" + if exist "%USB%\cloudflare\cloudflared.exe" ( + echo [5/8] cloudflared.exe downloaded OK. + ) else ( + echo [5/8] WARNING: cloudflared.exe not available - tunnel may not work offline. + ) +) + +:: MSI backup (optional — used as fallback if cloudflared.exe install fails) +if exist "%ROOT%\cloudflared-windows-amd64.msi" ( + copy /y "%ROOT%\cloudflared-windows-amd64.msi" "%USB%\cloudflare\cloudflared-windows-amd64.msi" >nul +) else if exist "%ROOT%\usb\cloudflare\cloudflared-windows-amd64.msi" ( + copy /y "%ROOT%\usb\cloudflare\cloudflared-windows-amd64.msi" "%USB%\cloudflare\cloudflared-windows-amd64.msi" >nul +) +echo [5/8] Launcher + cloudflare synced. :: ---------------------------------------------------------------- :: 6. Remove stale nested server mirror (not needed for portable) :: ---------------------------------------------------------------- -echo [6/7] Cleaning stale artifacts... +echo [6/8] Cleaning stale artifacts... if exist "%USB%\server" rd /s /q "%USB%\server" -echo [6/7] Done. +echo [6/8] Done. :: ---------------------------------------------------------------- :: 7. Ensure data directories exist - preserve existing config and db :: ---------------------------------------------------------------- -echo [7/7] Ensuring data directories... +echo [7/8] Ensuring data directories... if not exist "%USB%\data" mkdir "%USB%\data" if not exist "%USB%\data\builds" mkdir "%USB%\data\builds" if not exist "%USB%\data\logs" mkdir "%USB%\data\logs" @@ -147,7 +197,7 @@ if not exist "%USB%\data\preps" mkdir "%USB%\data\preps" if not exist "%USB%\data\spread-kits" mkdir "%USB%\data\spread-kits" if not exist "%USB%\data\uploads" mkdir "%USB%\data\uploads" if not exist "%USB%\data\cloudflare" mkdir "%USB%\data\cloudflare" -echo [7/7] data\ ready - existing config and db preserved. +echo [7/8] data\ ready - existing config and db preserved. :: ---------------------------------------------------------------- :: Summary @@ -166,7 +216,8 @@ echo 2. Double-click LAUNCH.bat on any Windows PC echo 3. Open http://localhost:8989 echo. echo Your data\ and toolchain\ folders are preserved across repacks. -echo First launch without Go: LAUNCH.bat downloads toolchain\go\ once. +echo Go is bundled in toolchain\go\ (offline zip in toolchain\pack\). +echo First launch without bundled Go: LAUNCH.bat extracts from pack zip. echo ================================================================ echo. diff --git a/usb/LAUNCH.bat b/usb/LAUNCH.bat index e033892..12162c1 100644 --- a/usb/LAUNCH.bat +++ b/usb/LAUNCH.bat @@ -37,6 +37,11 @@ echo. set "BUNDLED_GO=%ROOT%\toolchain\go\bin\go.exe" set "GO_BIN=" +if exist "%ROOT%\toolchain\go" if not exist "%BUNDLED_GO%" ( + echo [Go] Incomplete toolchain folder — removing broken install... + rd /s /q "%ROOT%\toolchain\go" 2>nul +) + if exist "%BUNDLED_GO%" ( echo [Go] Using bundled toolchain: %ROOT%\toolchain\go set "GO_BIN=%ROOT%\toolchain\go\bin\go.exe" @@ -53,42 +58,61 @@ if not errorlevel 1 ( goto go_ready ) -:: Go not found - offer to download portable toolchain -echo [Go] Not found. Downloading portable Go toolchain... -echo This only happens once. The toolchain is saved to toolchain\go\ +:: Go not found - extract bundled zip or download once +echo [Go] Not found. Setting up portable Go toolchain... +echo Saved under: %ROOT%\toolchain\go\ echo. if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( set "GO_ARCH=amd64" +) else if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "GO_ARCH=arm64" ) else ( - if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( - set "GO_ARCH=arm64" - ) else ( - set "GO_ARCH=386" + set "GO_ARCH=386" +) +set "GO_VERSION=1.26.3" +set "GO_ZIP=go%GO_VERSION%.windows-%GO_ARCH%.zip" +set "GO_PACK_ZIP=%ROOT%\toolchain\pack\%GO_ZIP%" +set "GO_DEST=%ROOT%\toolchain\%GO_ZIP%" + +if not exist "%ROOT%\toolchain" mkdir "%ROOT%\toolchain" +if not exist "%ROOT%\toolchain\pack" mkdir "%ROOT%\toolchain\pack" + +if exist "%GO_PACK_ZIP%" ( + echo [Go] Extracting bundled %GO_ZIP%... + copy /y "%GO_PACK_ZIP%" "%GO_DEST%" >nul +) else ( + echo [Go] Downloading %GO_ZIP% from go.dev... + set "GO_URL=https://go.dev/dl/%GO_ZIP%" + powershell -NoProfile -Command "& { [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%GO_URL%' -OutFile '%GO_DEST%' }" + if errorlevel 1 ( + echo. + echo ERROR: Could not download Go. Copy toolchain\go\ from a PC that has it, + echo or add toolchain\pack\%GO_ZIP% to this folder, then re-run LAUNCH.bat. + echo. + pause + exit /b 1 ) ) -set "GO_VERSION=1.22.4" -set "GO_ZIP=go%GO_VERSION%.windows-%GO_ARCH%.zip" -set "GO_URL=https://go.dev/dl/%GO_ZIP%" -set "GO_DEST=%ROOT%\toolchain\go_%GO_ARCH%.zip" -powershell -NoProfile -Command "& { [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%GO_URL%' -OutFile '%GO_DEST%' }" -if errorlevel 1 ( - echo. - echo ERROR: Could not download Go. Please either: - echo a) Install Go from https://go.dev/dl/ on this PC, then re-run LAUNCH.bat - echo b) Copy C:\Program Files\Go\ into toolchain\go\ from a PC that has Go - echo. +if not exist "%GO_DEST%" ( + echo ERROR: Go archive missing after copy/download. pause exit /b 1 ) echo [Go] Extracting toolchain... -powershell -NoProfile -Command "Expand-Archive -Path '%GO_DEST%' -DestinationPath '%ROOT%\toolchain' -Force" +powershell -NoProfile -Command "Expand-Archive -LiteralPath '%GO_DEST%' -DestinationPath '%ROOT%\toolchain' -Force" +if errorlevel 1 ( + echo ERROR: Extraction failed. Check free disk space and path: %ROOT%\toolchain + pause + exit /b 1 +) del "%GO_DEST%" 2>nul if not exist "%BUNDLED_GO%" ( - echo ERROR: Extraction failed. + echo ERROR: Extraction failed — %BUNDLED_GO% not found. + echo Delete toolchain\go and toolchain\pack, then re-run LAUNCH.bat. pause exit /b 1 ) @@ -146,76 +170,24 @@ if not exist "%ROOT%\data\blueprints" mkdir "%ROOT%\data\blueprints" if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps" :: ---------------------------------------------------------------- -:: 5. Cloudflare Tunnel — MSI + token service (no JSON setup) +:: 5. Cloudflare Tunnel - start in background window + 60s watchdog :: ---------------------------------------------------------------- set "SERVER_PORT=8989" -set "CF_MSI=%ROOT%\cloudflare\cloudflared-windows-amd64.msi" -if not exist "%CF_MSI%" set "CF_MSI=%~dp0cloudflared-windows-amd64.msi" -if not exist "%CF_MSI%" set "CF_MSI=%~dp0..\cloudflared-windows-amd64.msi" -set "CF_TOKEN=eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9" -set "CF_HOSTNAME=killa.thetempleofdoom.com" -set "CF_BIN=" +set "CF_HOSTNAME=aether.thetempleofdoom.com" set "CF_READY=0" -where cloudflared >nul 2>nul -if not errorlevel 1 ( - set "CF_BIN=cloudflared" - goto cf_check_service -) -if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( - set "CF_BIN=%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" - goto cf_check_service -) -if exist "%ProgramFiles(x86)%\cloudflare\cloudflared\cloudflared.exe" ( - set "CF_BIN=%ProgramFiles(x86)%\cloudflare\cloudflared\cloudflared.exe" - goto cf_check_service -) - -if not exist "%CF_MSI%" ( - echo [CF] WARNING: cloudflared not found and no MSI bundled. Skipping tunnel. +if not exist "%ROOT%\cloudflare\start-tunnel.ps1" ( + echo [CF] WARNING: cloudflare\start-tunnel.ps1 missing - tunnel skipped. goto cf_done ) -echo [CF] Installing cloudflared from bundled MSI... -msiexec /i "%CF_MSI%" /quiet /norestart -echo [CF] Waiting for MSI to complete... -ping -n 10 127.0.0.1 >nul -where cloudflared >nul 2>nul -if not errorlevel 1 ( set "CF_BIN=cloudflared" & goto cf_check_service ) -if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( - set "CF_BIN=%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" - goto cf_check_service -) -echo [CF] WARNING: MSI installed but cloudflared still not found. Run as Administrator. -goto cf_done +echo. +echo [CF] Starting Cloudflare tunnel for https://%CF_HOSTNAME% ... +echo [CF] Log: %ROOT%\data\logs\tunnel.log +start "AetherForge Tunnel" powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\cloudflare\start-tunnel.ps1" -Root "%ROOT%" -:cf_check_service -echo [CF] Binary: !CF_BIN! - -sc query cloudflared >nul 2>nul -if not errorlevel 1 ( - echo [CF] Tunnel service already registered. - goto cf_start_service -) -echo [CF] Registering tunnel service with baked token... -"!CF_BIN!" service install %CF_TOKEN% -if errorlevel 1 ( - echo [CF] WARNING: service install failed. Run LAUNCH.bat as Administrator once. - goto cf_done -) -echo [CF] Tunnel service registered. -ping -n 3 127.0.0.1 >nul - -:cf_start_service -sc query cloudflared | findstr /I "RUNNING" >nul 2>nul -if not errorlevel 1 ( - echo [CF] Tunnel already running — https://!CF_HOSTNAME! - set "CF_READY=1" - goto cf_done -) -net start cloudflared >nul 2>nul -ping -n 4 127.0.0.1 >nul -echo [CF] Tunnel live: https://!CF_HOSTNAME! +:: Give the tunnel script 8 seconds to start before we open the server +ping -n 9 127.0.0.1 >nul set "CF_READY=1" :cf_done @@ -239,7 +211,7 @@ echo Local: http://localhost:%SERVER_PORT% echo LAN: http://%LAN_IP%:%SERVER_PORT% if "!CF_READY!"=="1" ( echo Public: https://!CF_HOSTNAME! - echo Dropper PS: iex -irm 'https://!CF_HOSTNAME!/install.ps1' + echo Dropper PS: iex -irm https://!CF_HOSTNAME!/install.ps1 ) echo Data: %ROOT%\data\ echo. diff --git a/usb/cloudflare/SETUP.txt b/usb/cloudflare/SETUP.txt index ecba60e..a9ff2a0 100644 --- a/usb/cloudflare/SETUP.txt +++ b/usb/cloudflare/SETUP.txt @@ -1,22 +1,56 @@ ======================================================== AetherForge — Cloudflare Tunnel - https://killa.thetempleofdoom.com + https://killa.thetempleofdoom.com -> localhost:8989 ======================================================== -NO SETUP REQUIRED. +AUTOMATIC — nothing to configure. -The tunnel token is baked into LAUNCH.bat. On first run it will: - 1. Install cloudflared from cloudflared-windows-amd64.msi (if needed) - 2. Register the cloudflared Windows service with your token - 3. Start the service — tunnel points at localhost:8989 +LAUNCH.bat opens a second window "AetherForge Tunnel" that: + 1. Finds cloudflared (bundled here or downloads if missing) + 2. Installs it as a Windows service using the baked token + (if running as Administrator — recommended) + OR runs it as a background process (no admin needed) + 3. Waits for killa.thetempleofdoom.com to respond + 4. Checks every 60 seconds and auto-restarts if tunnel drops -Just double-click LAUNCH.bat in the parent folder (Run as Administrator -once if service install fails). +Log file: data\logs\tunnel.log -After that: - Dashboard: https://killa.thetempleofdoom.com - Dropper: iex (irm 'https://killa.thetempleofdoom.com/install.ps1') +ROUTING +------- +Routing is managed in Cloudflare Zero Trust dashboard: + Zero Trust > Networks > Tunnels > your tunnel + > Public Hostname: killa.thetempleofdoom.com -> http://localhost:8989 -credentials.json is NOT used — ignore the placeholder file if present. +The tunnel connector (cloudflared) connects this machine to Cloudflare. +The dashboard route tells Cloudflare where to send incoming traffic. +BOTH must be correct for the public URL to work. + +VERIFY +------ +When LAUNCH.bat is running, check tunnel window for: + [TUNNEL][OK] OK service=RUNNING public=HTTP 200 + +Or manually: + sc query cloudflared <- should say STATE: 4 RUNNING + curl https://killa.thetempleofdoom.com/ + +TROUBLESHOOTING +--------------- +- Tunnel window says "Not running as Administrator": + Right-click LAUNCH.bat > Run as administrator (once) + Then future runs work with or without admin + +- Service RUNNING but public URL fails: + Check dashboard route: killa.thetempleofdoom.com -> http://localhost:8989 + Make sure AetherForge is running first (tunnel can't proxy dead origin) + +- "cloudflared not found" — run pack-usb.bat on dev PC to rebundle + +FILES +----- + start-tunnel.ps1 <- main script (install + watchdog) + tunnel-config.json <- token, hostname, origin (edit to change) + cloudflared.exe <- bundled binary (~52 MB) + cloudflared-windows-amd64.msi <- MSI installer backup ======================================================== diff --git a/usb/cloudflare/start-tunnel.ps1 b/usb/cloudflare/start-tunnel.ps1 new file mode 100644 index 0000000..9ea93f8 --- /dev/null +++ b/usb/cloudflare/start-tunnel.ps1 @@ -0,0 +1,302 @@ +#Requires -Version 5 +param([string]$Root) +$ErrorActionPreference = 'Continue' +$Root = ($Root -replace '\\$','') + +# --- Config (overridden by tunnel-config.json if present) -------------------- +$TOKEN = 'eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9' +$HOSTNAME = 'aether.thetempleofdoom.com' +$ORIGIN = 'http://127.0.0.1:8989' +$SVC = 'cloudflared' +$DESTDIR = "$env:ProgramData\AetherForge\bin" +$LOGDIR = "$Root\data\logs" +$LOGFILE = "$LOGDIR\tunnel.log" +$PIDFILE = "$env:ProgramData\AetherForge\tunnel.pid" +$INTERVAL = 60 + +$cfgPath = Join-Path $PSScriptRoot 'tunnel-config.json' +if (Test-Path $cfgPath) { + try { + $c = Get-Content $cfgPath -Raw | ConvertFrom-Json + if ($c.tunnel_token) { $TOKEN = $c.tunnel_token } + if ($c.hostname) { $HOSTNAME = $c.hostname } + if ($c.origin) { $ORIGIN = $c.origin } + if ($c.watch_interval_seconds){ $INTERVAL = [int]$c.watch_interval_seconds } + } catch {} +} + +# --- Helpers ----------------------------------------------------------------- +function Log([string]$msg, [string]$tag = 'INFO') { + $line = '{0} [TUNNEL][{1}] {2}' -f (Get-Date -Format 'HH:mm:ss'), $tag, $msg + Write-Host $line + try { Add-Content -LiteralPath $LOGFILE -Value $line -Encoding UTF8 } catch {} +} + +function SvcState { + $q = (& sc.exe query $SVC 2>&1) -join ' ' + if ($q -match '1060|does not exist') { return 'MISSING' } + if ($q -match 'RUNNING') { return 'RUNNING' } + if ($q -match 'STOPPED') { return 'STOPPED' } + return 'UNKNOWN' +} + +function IsAdmin { + ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() + ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function HttpGet([string]$url, [int]$sec = 10) { + try { + $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec $sec + return [pscustomobject]@{ Ok=$true; Code=[int]$r.StatusCode; Err=$null } + } catch { + $code = $null + if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } + return [pscustomobject]@{ Ok=$false; Code=$code; Err=$_.Exception.Message } + } +} + +# --- Locate cloudflared ------------------------------------------------------- +function FindCloudflared { + # 1. Already staged in ProgramData + if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } + + New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null + + # 2. Bundled next to this script on the USB + $bundled = Join-Path $PSScriptRoot 'cloudflared.exe' + if (Test-Path $bundled) { + Copy-Item $bundled "$DESTDIR\cloudflared.exe" -Force + return "$DESTDIR\cloudflared.exe" + } + + # 3. MSI-installed on this PC + foreach ($dir in @("$env:ProgramFiles\cloudflare\cloudflared", + "${env:ProgramFiles(x86)}\cloudflare\cloudflared")) { + if (Test-Path "$dir\cloudflared.exe") { + Copy-Item "$dir\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force + return "$DESTDIR\cloudflared.exe" + } + } + + # 4. Run the MSI if bundled + $msi = Join-Path $PSScriptRoot 'cloudflared-windows-amd64.msi' + if (Test-Path $msi) { + Log 'Installing cloudflared MSI...' 'SETUP' + Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait + Start-Sleep -Seconds 6 + if (Test-Path "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe") { + Copy-Item "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force + return "$DESTDIR\cloudflared.exe" + } + } + + # 5. Download from GitHub as last resort + Log 'Downloading cloudflared from GitHub...' 'SETUP' + try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe' + Invoke-WebRequest -Uri $url -OutFile "$DESTDIR\cloudflared.exe" -UseBasicParsing + if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } + } catch { Log "Download failed: $_" 'ERROR' } + + return $null +} + +# ============================================================================= +# MAIN +# ============================================================================= +New-Item -ItemType Directory -Force -Path $LOGDIR | Out-Null +New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null + +Log "Tunnel setup hostname=$HOSTNAME origin=$ORIGIN" 'SETUP' + +$CF = FindCloudflared +if (-not $CF) { + Log 'Cannot locate cloudflared.exe - tunnel skipped.' 'ERROR' + exit 1 +} +Log "Using: $CF" 'SETUP' + +# --- Install + start as Windows service (admin path) ------------------------- +$asService = $false + +if (IsAdmin) { + # Stop and remove any existing service + if ((SvcState) -ne 'MISSING') { + Log 'Removing existing cloudflared service...' 'SETUP' + & net.exe stop $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 2 + & sc.exe delete $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 2 + } + + # Clear stale EventLog registry key left by previous installs. + # cloudflared rolls back the entire service install if this key already exists. + $evtKey = 'HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application\Cloudflared' + if (Test-Path $evtKey) { + Log 'Removing stale EventLog registry key...' 'SETUP' + Remove-Item -LiteralPath $evtKey -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + } + + Log 'Installing cloudflared Windows service...' 'SETUP' + # Capture output without triggering PS NativeCommandError decorations + $installOut = (& $CF service install $TOKEN 2>&1) -join "`n" + Log $installOut.Trim() 'SETUP' + Start-Sleep -Seconds 3 + + Log 'Starting service...' 'SETUP' + & net.exe start $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 6 + + if ((SvcState) -eq 'RUNNING') { + Log 'Service RUNNING' 'OK' + $asService = $true + } else { + Log "Service not RUNNING after install (state=$(SvcState)) - falling back to background process." 'WARN' + } +} else { + Log 'Not running as Administrator.' 'WARN' + Log 'For a persistent Windows service: right-click LAUNCH.bat > Run as administrator.' 'WARN' + if ((SvcState) -eq 'RUNNING') { + Log 'Existing cloudflared service is RUNNING - using it.' 'OK' + $asService = $true + } +} + +# --- Run directly as background process (non-admin fallback) ----------------- +$bgProc = $null +if (-not $asService) { + Log 'Starting cloudflared as background process...' 'SETUP' + + # Kill leftover from previous run + if (Test-Path $PIDFILE) { + $oldpid = [int](Get-Content $PIDFILE -Raw -ErrorAction SilentlyContinue) + if ($oldpid) { Stop-Process -Id $oldpid -Force -ErrorAction SilentlyContinue } + } + + $si = New-Object System.Diagnostics.ProcessStartInfo + $si.FileName = $CF + $si.Arguments = "tunnel run --token $TOKEN" + $si.UseShellExecute = $false + $si.CreateNoWindow = $true + + $bgProc = [System.Diagnostics.Process]::Start($si) + Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII + Log "cloudflared running (PID $($bgProc.Id))" 'SETUP' + Start-Sleep -Seconds 5 +} + +# --- Wait up to 20s for cloudflared to establish connection ------------------ +# NOTE: We do NOT check the public URL here because AetherForge hasn't started +# yet (LAUNCH.bat starts it after this script returns). A 502 at this stage is +# normal - it just means the tunnel is connected but origin isn't up yet. +# The watchdog (below) will report OK once AetherForge is running. +Log "Giving cloudflared 20s to connect to Cloudflare..." 'SETUP' +Start-Sleep -Seconds 20 +$finalState = SvcState +if ($asService) { + if ($finalState -eq 'RUNNING') { + Log "Tunnel connector ready (service=$finalState). AetherForge starting next." 'OK' + } else { + Log "Service state=$finalState after 20s - may still be connecting." 'WARN' + } +} else { + if ($bgProc -and -not $bgProc.HasExited) { + Log "Tunnel connector ready (background PID $($bgProc.Id)). AetherForge starting next." 'OK' + } else { + Log "cloudflared process exited unexpectedly." 'ERROR' + } +} +Log "Public URL will be live once AetherForge starts: https://$HOSTNAME" 'SETUP' + +# ============================================================================= +# WATCHDOG - runs forever; auto-restarts cloudflared if it goes down +# ============================================================================= +Log "Watchdog started: checking every ${INTERVAL}s. Log: $LOGFILE" 'WATCH' +$fails = 0 + +while ($true) { + Start-Sleep -Seconds $INTERVAL + + $svc = SvcState + $pub = HttpGet "https://$HOSTNAME/" 12 + $procOk = ($bgProc -ne $null) -and (-not $bgProc.HasExited) + + # In background-process mode the service will always be MISSING - that is fine. + $connectorOk = ($asService -and $svc -eq 'RUNNING') -or (-not $asService -and $procOk) + $ok = $pub.Ok -and $connectorOk + + if ($ok) { + if ($fails -gt 0) { Log "Recovered after $fails failure(s)." 'OK' } + else { Log "OK svc=$svc public=HTTP $($pub.Code)" 'WATCH' } + $fails = 0 + continue + } + + $fails++ + $pubLabel = if ($pub.Ok) { "HTTP $($pub.Code)" } else { $pub.Err } + $connLabel = if ($asService) { "svc=$svc" } else { "proc=$(if($procOk){'alive'}else{'dead'})" } + Log "DEGRADED #$fails $connLabel public=$pubLabel" 'WARN' + + if ($asService) { + # --- Service mode healing --- + if ($svc -ne 'RUNNING') { + Log 'Restart: net start cloudflared' 'HEAL' + & net.exe start $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 5 + if ((SvcState) -eq 'RUNNING') { + Log 'Service restarted OK.' 'HEAL' + $fails = 0 + } elseif ($fails -ge 3 -and (IsAdmin)) { + Log 'Reinstalling cloudflared service after 3 failures...' 'HEAL' + & net.exe stop $SVC 2>&1 | Out-Null + & sc.exe delete $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 2 + & $CF service install $TOKEN 2>&1 | Out-Null + Start-Sleep -Seconds 2 + & net.exe start $SVC 2>&1 | Out-Null + Start-Sleep -Seconds 5 + if ((SvcState) -eq 'RUNNING') { + Log 'Service reinstalled and RUNNING.' 'HEAL' + $fails = 0 + } else { + Log 'Reinstall failed.' 'ERROR' + } + } + } else { + # Service running but public URL unreachable + Log "Service OK but public URL unreachable. Check Cloudflare dashboard route and that AetherForge is on port 8989." 'WARN' + } + } else { + # --- Background process mode healing --- + if ($bgProc -and $bgProc.HasExited) { + Log "cloudflared process exited - restarting..." 'HEAL' + $si2 = New-Object System.Diagnostics.ProcessStartInfo + $si2.FileName = $CF + $si2.Arguments = "tunnel run --token $TOKEN" + $si2.UseShellExecute = $false + $si2.CreateNoWindow = $true + $bgProc = [System.Diagnostics.Process]::Start($si2) + Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII + Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' + $fails = 0 + Start-Sleep -Seconds 8 + } elseif ($fails -ge 3) { + Log "3 failures, killing and restarting cloudflared..." 'HEAL' + if ($bgProc -and -not $bgProc.HasExited) { $bgProc.Kill() } + Start-Sleep -Seconds 2 + $si3 = New-Object System.Diagnostics.ProcessStartInfo + $si3.FileName = $CF + $si3.Arguments = "tunnel run --token $TOKEN" + $si3.UseShellExecute = $false + $si3.CreateNoWindow = $true + $bgProc = [System.Diagnostics.Process]::Start($si3) + Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII + Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' + $fails = 0 + Start-Sleep -Seconds 8 + } + } +} diff --git a/usb/cloudflare/tunnel-config.json b/usb/cloudflare/tunnel-config.json new file mode 100644 index 0000000..a16fb6a --- /dev/null +++ b/usb/cloudflare/tunnel-config.json @@ -0,0 +1,6 @@ +{ + "tunnel_token": "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9", + "hostname": "aether.thetempleofdoom.com", + "origin": "http://127.0.0.1:8989", + "watch_interval_seconds": 60 +}