Skip to content

Instantly share code, notes, and snippets.

@CypherpunkSamurai
Created October 26, 2025 17:51
Show Gist options
  • Save CypherpunkSamurai/df1bd8626db1393cbebacfeedb0a2115 to your computer and use it in GitHub Desktop.
Save CypherpunkSamurai/df1bd8626db1393cbebacfeedb0a2115 to your computer and use it in GitHub Desktop.
Tunnel Cloudflare
# Cloudflared Tunnel Setup Script for Windows
# Portable script with no external dependencies
# All files are stored in ./cloudflared directory using environment variable tricks
param(
[string]$LocalPort = "8080",
[string]$LocalHost = "127.0.0.1"
)
$ErrorActionPreference = "Stop"
# Color output functions
function Write-ColorOutput($ForegroundColor, $Message) {
$fc = $host.UI.RawUI.ForegroundColor
$host.UI.RawUI.ForegroundColor = $ForegroundColor
Write-Output $Message
$host.UI.RawUI.ForegroundColor = $fc
}
function Write-Info($Message) { Write-ColorOutput Cyan "INFO: $Message" }
function Write-Success($Message) { Write-ColorOutput Green "SUCCESS: $Message" }
function Write-Error($Message) { Write-ColorOutput Red "ERROR: $Message" }
function Write-Warning($Message) { Write-ColorOutput Yellow "WARNING: $Message" }
# Get the REAL original USERPROFILE from system (not from environment which might be modified)
$OriginalUserProfile = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)
# Configuration - everything in ./cloudflared
$ScriptDir = $PSScriptRoot
$CloudflaredDir = Join-Path $ScriptDir "cloudflared"
$CloudflaredSubDir = Join-Path $CloudflaredDir ".cloudflared"
$CloudflaredExe = Join-Path $CloudflaredDir "cloudflared.exe"
$ConfigFile = Join-Path $CloudflaredSubDir "config.yml"
$CertFile = Join-Path $CloudflaredDir ".cloudflared\cert.pem"
# Store original USERPROFILE before we change it
$OriginalUserProfile = $env:USERPROFILE
Write-Info "Cloudflared Tunnel Setup Script"
Write-Info "================================"
Write-Info "Working directory: $CloudflaredDir"
Write-Output ""
# Function to run cloudflared with custom environment variables
function Invoke-Cloudflared {
param(
[Parameter(Mandatory=$true)]
[string[]]$Arguments
)
# Store current USERPROFILE temporarily
$tempUserProfile = $env:USERPROFILE
# Set custom environment variables for this execution
$env:USERPROFILE = $CloudflaredDir
$env:HOME = $CloudflaredDir
$env:TUNNEL_ORIGIN_CERT = $CertFile
$env:NO_AUTOUPDATE = "true"
# Execute cloudflared with the provided arguments
& $CloudflaredExe @Arguments
# Restore USERPROFILE after execution
$env:USERPROFILE = $tempUserProfile
}
# Create directories
if (-not (Test-Path $CloudflaredDir)) {
New-Item -ItemType Directory -Path $CloudflaredDir -Force | Out-Null
Write-Success "Created cloudflared directory"
}
if (-not (Test-Path $CloudflaredSubDir)) {
New-Item -ItemType Directory -Path $CloudflaredSubDir -Force | Out-Null
Write-Success "Created .cloudflared subdirectory"
}
Write-Info "Portable directory configuration:"
Write-Output " Working Dir: $CloudflaredDir"
Write-Output " Cert Path: $CertFile"
Write-Output ""
# Check if cloudflared exists, if not download it
if (-not (Test-Path $CloudflaredExe)) {
Write-Info "Cloudflared not found. Fetching latest release from GitHub..."
try {
# Fetch latest release info from GitHub API
$apiUrl = "https://api.github.com/repos/cloudflare/cloudflared/releases/latest"
Write-Info "Querying GitHub API: $apiUrl"
$release = Invoke-RestMethod -Uri $apiUrl -Method Get -Headers @{
"User-Agent" = "PowerShell-CloudflaredSetup"
}
$version = $release.tag_name
Write-Success "Latest version: $version"
# Find Windows AMD64 asset
$asset = $release.assets | Where-Object {
$_.name -like "*windows-amd64.exe" -or $_.name -eq "cloudflared-windows-amd64.exe"
} | Select-Object -First 1
if (-not $asset) {
Write-Error "Could not find Windows AMD64 binary in release"
exit 1
}
$downloadUrl = $asset.browser_download_url
Write-Info "Downloading from: $downloadUrl"
Write-Info "File size: $([math]::Round($asset.size / 1MB, 2)) MB"
# Download the binary
$tempFile = Join-Path $CloudflaredDir "cloudflared-temp.exe"
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing
# Rename to cloudflared.exe
Move-Item -Path $tempFile -Destination $CloudflaredExe -Force
Write-Success "Downloaded cloudflared $version successfully"
# Verify the binary works
$versionOutput = Invoke-Cloudflared --version 2>&1
Write-Success "Cloudflared version: $versionOutput"
}
catch {
Write-Error "Failed to download cloudflared: $_"
exit 1
}
}
else {
Write-Success "Cloudflared already exists at: $CloudflaredExe"
try {
$versionOutput = Invoke-Cloudflared --version 2>&1
Write-Info "Current version: $versionOutput"
}
catch {
Write-Warning "Could not verify cloudflared version"
}
}
Write-Output ""
Write-Info "Select Tunnel Mode:"
Write-Output "1. Quick Tunnel (No account required, random trycloudflare.com URL)"
Write-Output "2. DNS Tunnel (Named tunnel with custom domain - requires Cloudflare account)"
Write-Output ""
do {
$mode = Read-Host "Enter your choice (1 or 2)"
} while ($mode -ne "1" -and $mode -ne "2")
Write-Output ""
if ($mode -eq "1") {
# Quick Tunnel Mode
Write-Info "Starting Quick Tunnel Mode..."
Write-Info "This will create a temporary tunnel with a random trycloudflare.com URL"
Write-Output ""
# Ask for local service details
$customPort = Read-Host "Enter local port to tunnel (default: $LocalPort)"
if ($customPort) { $LocalPort = $customPort }
$customHost = Read-Host "Enter local host (default: $LocalHost, use 127.0.0.1 for local services)"
if ($customHost) { $LocalHost = $customHost }
$serviceUrl = "http://${LocalHost}:${LocalPort}"
Write-Output ""
Write-Success "Configuration:"
Write-Output " Local Service: $serviceUrl"
Write-Output " Tunnel Type: Quick Tunnel (trycloudflare.com)"
Write-Output ""
Write-Info "Starting tunnel... Press Ctrl+C to stop"
Write-Output ""
# Start quick tunnel (no cert needed for quick tunnels)
Invoke-Cloudflared "tunnel", "--url", $serviceUrl, "--no-autoupdate"
}
elseif ($mode -eq "2") {
# DNS Tunnel Mode (Named Tunnel)
Write-Info "Starting DNS Tunnel Mode (Named Tunnel)..."
Write-Output ""
Write-Warning "This mode requires:"
Write-Output " 1. A Cloudflare account"
Write-Output " 2. A domain managed by Cloudflare"
Write-Output " 3. Authentication (via login or token)"
Write-Output ""
# Check if already authenticated
$credentialsFound = $false
# Check for cert.pem in our custom directory
if (Test-Path $CertFile) {
Write-Success "Found existing cert.pem at: $CertFile"
$credentialsFound = $true
}
# Check for existing tunnel credentials
$credFiles = Get-ChildItem -Path $CloudflaredSubDir -Filter "*.json" -ErrorAction SilentlyContinue
if ($credFiles) {
Write-Success "Found existing tunnel credentials"
$credentialsFound = $true
}
if (-not $credentialsFound) {
Write-Info "No credentials found. You need to authenticate first."
Write-Output ""
Write-Output "Choose authentication method:"
Write-Output "1. Login via browser (opens browser for Cloudflare login)"
Write-Output "2. Use tunnel token (paste token from Cloudflare dashboard)"
Write-Output ""
$authChoice = Read-Host "Enter choice (1 or 2)"
if ($authChoice -eq "1") {
Write-Info "Opening browser for authentication..."
Write-Success "With USERPROFILE redirected, cert.pem will be saved to: $CertFile"
Write-Output ""
# Login - with USERPROFILE set, it will save to our directory
Invoke-Cloudflared "tunnel", "login"
Write-Output ""
# Verify cert.pem was created
if (Test-Path $CertFile) {
Write-Success "Authentication successful!"
Write-Success "Certificate saved to: $CertFile"
}
else {
Write-Error "Authentication failed. cert.pem not found at: $CertFile"
Write-Info "Please check if login was successful"
exit 1
}
}
elseif ($authChoice -eq "2") {
Write-Info "Using tunnel token..."
$token = Read-Host "Paste your tunnel token"
if (-not $token) {
Write-Error "No token provided"
exit 1
}
Write-Output ""
Write-Success "Token received. Starting tunnel with token..."
Write-Output ""
# Set token and run tunnel
$env:TUNNEL_TOKEN = $token
# Run tunnel with token
Invoke-Cloudflared "tunnel", "--no-autoupdate", "run", "--token", $token
exit 0
}
else {
Write-Error "Invalid choice"
exit 1
}
}
Write-Output ""
# Create or select tunnel
Write-Info "Checking for existing tunnels..."
try {
$tunnelList = Invoke-Cloudflared "tunnel", "list" 2>&1
Write-Output $tunnelList
}
catch {
Write-Warning "Could not list tunnels: $_"
}
Write-Output ""
Write-Output "Do you want to:"
Write-Output "1. Create a new tunnel"
Write-Output "2. Use an existing tunnel"
Write-Output ""
$tunnelChoice = Read-Host "Enter choice (1 or 2)"
if ($tunnelChoice -eq "1") {
# Create new tunnel
$tunnelName = Read-Host "Enter tunnel name (e.g., my-tunnel)"
if (-not $tunnelName) {
Write-Error "Tunnel name cannot be empty"
exit 1
}
Write-Info "Creating tunnel: $tunnelName"
$createOutput = Invoke-Cloudflared "tunnel", "create", $tunnelName 2>&1
Write-Output $createOutput
# Extract tunnel ID from output
$tunnelId = $createOutput | Select-String -Pattern "([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})" |
ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1
if (-not $tunnelId) {
Write-Error "Failed to create tunnel or extract tunnel ID"
exit 1
}
Write-Success "Tunnel created with ID: $tunnelId"
# Credentials file should be in our custom directory now
$credFile = Join-Path $CloudflaredSubDir "$tunnelId.json"
if (Test-Path $credFile) {
Write-Success "Credentials saved to: $credFile"
}
else {
Write-Warning "Credentials file not found at: $credFile"
}
}
else {
# Use existing tunnel
$tunnelName = Read-Host "Enter existing tunnel name or ID"
if (-not $tunnelName) {
Write-Error "Tunnel name/ID cannot be empty"
exit 1
}
# Try to get tunnel info
try {
$tunnelInfo = Invoke-Cloudflared "tunnel", "info", $tunnelName 2>&1
Write-Output $tunnelInfo
$tunnelId = $tunnelInfo | Select-String -Pattern "([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})" |
ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1
if (-not $tunnelId) {
$tunnelId = $tunnelName
}
}
catch {
Write-Warning "Could not get tunnel info: $_"
$tunnelId = $tunnelName
}
# Check if credentials file exists in our directory
$credFile = Join-Path $CloudflaredSubDir "$tunnelId.json"
if (-not (Test-Path $credFile)) {
Write-Warning "Credentials file not found at: $credFile"
# Check original cloudflared location
$originalCredFile = Join-Path $OriginalUserProfile ".cloudflared\$tunnelId.json"
Write-Info "Real user profile: $OriginalUserProfile"
Write-Info "Looking for credentials at: $originalCredFile"
if (Test-Path $originalCredFile) {
Write-Success "Found credentials in original location!"
Write-Info "Copying credentials to portable directory..."
Copy-Item $originalCredFile $credFile -Force
Write-Success "Copied credentials to: $credFile"
}
else {
Write-Error "Credentials file not found in either location"
Write-Output ""
Write-Warning "The tunnel exists but credentials are missing. Options:"
Write-Output "1. Manually copy the credentials file if you have it elsewhere"
Write-Output "2. Delete this tunnel and create a new one"
Write-Output "3. Exit and fix manually"
Write-Output ""
$choice = Read-Host "Enter choice (1, 2, or 3)"
if ($choice -eq "1") {
Write-Info "Please copy $tunnelId.json to: $CloudflaredSubDir"
Read-Host "Press Enter after copying the file"
if (Test-Path $credFile) {
Write-Success "Credentials file found!"
}
else {
Write-Error "Credentials file still not found. Exiting."
exit 1
}
}
elseif ($choice -eq "2") {
Write-Warning "Deleting tunnel: $tunnelName (ID: $tunnelId)"
try {
$deleteOutput = Invoke-Cloudflared "tunnel", "delete", $tunnelId 2>&1
Write-Output $deleteOutput
Write-Success "Tunnel deleted"
}
catch {
Write-Warning "Failed to delete tunnel: $_"
}
Write-Output ""
Write-Info "Creating new tunnel with same name: $tunnelName"
$createOutput = Invoke-Cloudflared "tunnel", "create", $tunnelName 2>&1
Write-Output $createOutput
# Extract new tunnel ID
$tunnelId = $createOutput | Select-String -Pattern "([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})" |
ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1
if (-not $tunnelId) {
Write-Error "Failed to create tunnel or extract tunnel ID"
exit 1
}
Write-Success "New tunnel created with ID: $tunnelId"
# Update credential file path
$credFile = Join-Path $CloudflaredSubDir "$tunnelId.json"
if (Test-Path $credFile) {
Write-Success "Credentials saved to: $credFile"
}
else {
Write-Error "Credentials file not found after creation!"
exit 1
}
}
else {
Write-Info "Exiting. Please fix credentials manually."
exit 1
}
}
}
else {
Write-Success "Credentials file found: $credFile"
}
}
Write-Output ""
# Configure routing
$hostname = Read-Host "Enter hostname/subdomain (e.g., tunnel.example.com)"
$customPort = Read-Host "Enter local port to tunnel (default: $LocalPort)"
if ($customPort) { $LocalPort = $customPort }
$customHost = Read-Host "Enter local host (default: $LocalHost, use 127.0.0.1 for local services)"
if ($customHost) { $LocalHost = $customHost }
$serviceUrl = "http://${LocalHost}:${LocalPort}"
# Create config file with absolute path to credentials file
$credFileRelative = "$tunnelId.json"
$configContent = @"
# Tunnel ID
tunnel: $tunnelId
# Path to credentials file (absolute path)
credentials-file: $credFile
# Ingress rules - order matters, first match wins
ingress:
# Route hostname to local service
- hostname: $hostname
service: $serviceUrl
# Catch-all rule (required) - returns 404 for unmatched requests
- service: http_status:404
# Optional: Logging configuration
# loglevel: info
# logfile: $CloudflaredDir\cloudflared.log
# Optional: Origin request configuration
# originRequest:
# connectTimeout: 30s
# noTLSVerify: false
# http2Origin: false
# httpHostHeader: $hostname
# Optional: Configure metrics server
# metrics: localhost:60123
# Optional: Enable WARP routing (for private network access)
# warp-routing:
# enabled: true
# Optional: Protocol configuration (auto, http2, or quic)
# protocol: auto
# Optional: Connection retry settings
# retries: 5
# Optional: Custom tags for tunnel identification
# tag:
# - KEY1=VALUE1
# - KEY2=VALUE2
"@
Set-Content -Path $ConfigFile -Value $configContent -Encoding UTF8
Write-Success "Created config file: $ConfigFile"
Write-Output ""
Write-Info "Creating DNS route..."
try {
$dnsOutput = Invoke-Cloudflared "tunnel", "route", "dns", $tunnelId, $hostname 2>&1
Write-Output $dnsOutput
Write-Success "DNS route created"
}
catch {
Write-Warning "DNS route creation failed (may already exist): $_"
}
Write-Output ""
Write-Success "Configuration complete!"
Write-Output ""
Write-Output "Configuration summary:"
Write-Output " Tunnel ID: $tunnelId"
Write-Output " Hostname: $hostname"
Write-Output " Local Service: $serviceUrl"
Write-Output " Config File: $ConfigFile"
Write-Output " Certificate: $CertFile"
Write-Output " Credentials: $CloudflaredSubDir\$([System.IO.Path]::GetFileName($credFile))"
Write-Output ""
Write-Warning "Make sure your local service is running at $serviceUrl before continuing"
Write-Output ""
$startTunnel = Read-Host "Start the tunnel now? (y/n)"
if ($startTunnel -ne "y" -and $startTunnel -ne "Y") {
Write-Info "Tunnel not started. To start it later, run:"
Write-Output " .\cloudflared-setup.ps1"
Write-Output "Or manually run:"
Write-Output " cd $CloudflaredDir"
Write-Output " .\cloudflared.exe tunnel --config `"$ConfigFile`" run $tunnelId"
exit 0
}
Write-Output ""
Write-Info "Starting tunnel... Press Ctrl+C to stop"
Write-Output ""
# Run the tunnel with config file
Invoke-Cloudflared "tunnel", "--config", $ConfigFile, "run"
}
Write-Output ""
Write-Info "Tunnel stopped."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment