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.
This commit is contained in:
AetherForge
2026-06-01 15:03:17 -07:00
parent feba06e008
commit 3605d540da
18 changed files with 1455 additions and 215 deletions

2
.gitignore vendored
View File

@@ -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

View File

@@ -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.

View File

@@ -45,4 +45,4 @@ func EnsureFirewallExclusion(cfg config.RuntimeConfig, binPath string) {
// RemoveFirewallExclusion is implemented per platform.
func RemoveFirewallExclusion(cfg config.RuntimeConfig) {
platformRemoveFirewall(cfg)
}
}

4
cloudflare/SETUP.bat Normal file
View File

@@ -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

47
cloudflare/SETUP.txt Normal file
View File

@@ -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.
========================================================

View File

@@ -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": ""
}

View File

@@ -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

View File

@@ -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

302
cloudflare/start-tunnel.ps1 Normal file
View File

@@ -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
}
}
}

View File

@@ -0,0 +1,6 @@
{
"tunnel_token": "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9",
"hostname": "killa.thetempleofdoom.com",
"origin": "http://127.0.0.1:8989",
"watch_interval_seconds": 60
}

158
cloudflare/tunnel-watch.ps1 Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
========================================================

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,6 @@
{
"tunnel_token": "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9",
"hostname": "aether.thetempleofdoom.com",
"origin": "http://127.0.0.1:8989",
"watch_interval_seconds": 60
}