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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
138
LAUNCH.bat
138
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.
|
||||
|
||||
@@ -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
4
cloudflare/SETUP.bat
Normal 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
47
cloudflare/SETUP.txt
Normal 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.
|
||||
|
||||
========================================================
|
||||
7
cloudflare/credentials.json
Normal file
7
cloudflare/credentials.json
Normal 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": ""
|
||||
}
|
||||
163
cloudflare/install-tunnel.bat
Normal file
163
cloudflare/install-tunnel.bat
Normal 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
|
||||
115
cloudflare/repair-tunnel.ps1
Normal file
115
cloudflare/repair-tunnel.ps1
Normal 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
302
cloudflare/start-tunnel.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
6
cloudflare/tunnel-config.json
Normal file
6
cloudflare/tunnel-config.json
Normal 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
158
cloudflare/tunnel-watch.ps1
Normal 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
|
||||
}
|
||||
52
cloudflare/verify-tunnel.bat
Normal file
52
cloudflare/verify-tunnel.bat
Normal 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
|
||||
47
cloudflare/write-tunnel-config.ps1
Normal file
47
cloudflare/write-tunnel-config.ps1
Normal 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"
|
||||
123
pack-usb.bat
123
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.
|
||||
|
||||
|
||||
138
usb/LAUNCH.bat
138
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
========================================================
|
||||
|
||||
302
usb/cloudflare/start-tunnel.ps1
Normal file
302
usb/cloudflare/start-tunnel.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
6
usb/cloudflare/tunnel-config.json
Normal file
6
usb/cloudflare/tunnel-config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"tunnel_token": "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9",
|
||||
"hostname": "aether.thetempleofdoom.com",
|
||||
"origin": "http://127.0.0.1:8989",
|
||||
"watch_interval_seconds": 60
|
||||
}
|
||||
Reference in New Issue
Block a user