# Tailscale Installation and Connection Script for Windows (with hardcoded auth key and optional custom login server) # This script installs Tailscale (if not present) and connects using a hardcoded auth key # Optionally supports custom login server (Headscale) - leave $LOGIN_SERVER empty to use default Tailscale server # WARNING: This file contains sensitive auth key and should NOT be committed to Git # Usage: .\install.ps1 $ErrorActionPreference = "Stop" # Trap to catch all errors and keep window open trap { Write-Host "" Write-Error "An error occurred: $_" Write-Error "Error details: $($_.Exception.Message)" if ($_.ScriptStackTrace) { Write-Error "Stack trace: $($_.ScriptStackTrace)" } Write-Host "" Write-Host "Closing in 20 seconds..." -ForegroundColor Yellow Start-Sleep -Seconds 20 exit 1 } # Custom login server (Headscale) - OPTIONAL: Set to your Headscale server URL or leave empty to use default Tailscale server # Example: $LOGIN_SERVER = "https://headscale.ovncr.vn" # Leave empty to use default Tailscale server: $LOGIN_SERVER = "" $LOGIN_SERVER = "https://headscale.ovncr.vn" # Auth key - REPLACE WITH YOUR ACTUAL AUTH KEY $AUTH_KEY = "26a0091c3f1aefa116faf081e25e1d488e33038d478e7cbd" # Logging functions function Write-Info { param([string]$Message) Write-Host "[INFO] $Message" -ForegroundColor Green } function Write-Warn { param([string]$Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow } function Write-Error { param([string]$Message) Write-Host "[ERROR] $Message" -ForegroundColor Red } # Function to exit with error and keep window open function Exit-WithError { param([string]$Message) if ($Message) { Write-Error $Message } Write-Host "" Write-Host "Closing in 20 seconds..." -ForegroundColor Yellow Start-Sleep -Seconds 20 exit 1 } # Check PowerShell version function Test-PowerShellVersion { if ($PSVersionTable.PSVersion.Major -lt 5) { Exit-WithError "PowerShell 5.1 or higher is required. Current version: $($PSVersionTable.PSVersion)" } Write-Info "PowerShell version: $($PSVersionTable.PSVersion)" } # Check if running as Administrator function Test-Administrator { $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Exit-WithError "This script must be run as Administrator" } Write-Info "Running with Administrator privileges" } # Check if Tailscale is already installed function Test-TailscaleInstalled { # Check if tailscale command exists if (Get-Command tailscale -ErrorAction SilentlyContinue) { return $true } # Check if Tailscale service exists $service = Get-Service -Name "Tailscale" -ErrorAction SilentlyContinue if ($service) { return $true } # Check registry for installation $regPath = "HKLM:\SOFTWARE\Tailscale" if (Test-Path $regPath) { return $true } return $false } # Refresh current session PATH from registry (so we see Tailscale after install) function Update-SessionPath { try { $machinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine") $userPath = [System.Environment]::GetEnvironmentVariable("Path", "User") $parts = @() if (-not [string]::IsNullOrEmpty($machinePath)) { $parts += $machinePath } if (-not [string]::IsNullOrEmpty($userPath)) { $parts += $userPath } if ($parts.Count -gt 0) { $env:Path = $parts -join ";" } } catch { Write-Warn "Could not refresh PATH from registry: $_" } } # Ensure tailscale is available in this session (refresh PATH and/or add known install path) function Ensure-TailscaleInPath { Update-SessionPath if (Get-Command tailscale -ErrorAction SilentlyContinue) { return $true } $knownPaths = @( "${env:ProgramFiles}\Tailscale", "${env:ProgramFiles(x86)}\Tailscale" ) foreach ($dir in $knownPaths) { $exe = Join-Path $dir "tailscale.exe" if (Test-Path $exe) { $env:Path = $dir + ";" + $env:Path Write-Info "Added Tailscale to PATH: $dir" return $true } } return $false } # Install Tailscale using winget function Install-TailscaleWithWinget { Write-Info "Attempting to install Tailscale using winget..." try { if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Warn "winget is not available" return $false } winget install --id Tailscale.Tailscale --silent --accept-package-agreements --accept-source-agreements Write-Info "Tailscale installed successfully using winget" return $true } catch { Write-Warn "Failed to install using winget: $_" return $false } } # Install Tailscale by downloading installer function Install-TailscaleWithInstaller { Write-Info "Downloading Tailscale installer..." $installerUrl = "https://pkgs.tailscale.com/stable/tailscale-setup-latest.exe" $installerPath = "$env:TEMP\tailscale-setup.exe" try { # Download installer Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing Write-Info "Installer downloaded to $installerPath" # Run silent install Write-Info "Installing Tailscale (this may take a moment)..." $process = Start-Process -FilePath $installerPath -ArgumentList "/S" -Wait -PassThru if ($process.ExitCode -eq 0) { Write-Info "Tailscale installed successfully" # Clean up installer Remove-Item $installerPath -Force -ErrorAction SilentlyContinue return $true } else { Write-Error "Installer exited with code: $($process.ExitCode)" return $false } } catch { Write-Error "Failed to download or install Tailscale: $_" return $false } } # Install Tailscale function Install-Tailscale { Write-Info "Installing Tailscale..." # Try winget first if available if (Install-TailscaleWithWinget) { return } # Fallback to direct download if (Install-TailscaleWithInstaller) { return } Exit-WithError "Failed to install Tailscale using both methods" } # Wait for Tailscale service to be ready function Wait-ForTailscaleService { Write-Info "Waiting for Tailscale service to be ready..." $maxAttempts = 30 $attempt = 0 while ($attempt -lt $maxAttempts) { $service = Get-Service -Name "Tailscale" -ErrorAction SilentlyContinue if ($service -and $service.Status -eq "Running") { Write-Info "Tailscale service is running" return } Start-Sleep -Seconds 2 $attempt++ } Write-Warn "Tailscale service may not be ready yet, but continuing..." } # Ensure Tailscale service is enabled function Enable-TailscaleService { Write-Info "Ensuring Tailscale service is enabled..." $service = Get-Service -Name "Tailscale" -ErrorAction SilentlyContinue if ($service) { if ($service.StartType -ne "Automatic") { Set-Service -Name "Tailscale" -StartupType Automatic Write-Info "Tailscale service set to start automatically" } else { Write-Info "Tailscale service is already set to start automatically" } if ($service.Status -ne "Running") { Start-Service -Name "Tailscale" Write-Info "Tailscale service started" } } else { Write-Warn "Tailscale service not found. It may start after installation." } } # Validate login server URL (only if provided) function Validate-LoginServer { # If LOGIN_SERVER is empty, skip validation (will use default Tailscale server) if ([string]::IsNullOrWhiteSpace($LOGIN_SERVER)) { Write-Info "No custom login server specified, will use default Tailscale server" return } if (-not ($LOGIN_SERVER -match "^https?://")) { Exit-WithError "Invalid login server URL format. URL should start with http:// or https://" } Write-Info "Login server validated: $LOGIN_SERVER" } # Validate auth key function Validate-AuthKey { if ($AUTH_KEY -eq "YOUR_AUTH_KEY_HERE") { Exit-WithError "Auth key not configured. Please replace YOUR_AUTH_KEY_HERE with your actual Tailscale auth key." } if ([string]::IsNullOrWhiteSpace($AUTH_KEY)) { Exit-WithError "Auth key cannot be empty" } # When using custom login server (Headscale), auth keys may not have tskey-auth- prefix # Only validate format for standard Tailscale auth keys if (-not [string]::IsNullOrWhiteSpace($LOGIN_SERVER) -and $LOGIN_SERVER -ne "https://login.tailscale.com") { Write-Info "Using custom login server, skipping standard auth key format validation" } elseif (-not $AUTH_KEY.StartsWith("tskey-auth-")) { Exit-WithError "Invalid auth key format. Auth key should start with 'tskey-auth-'" } Write-Info "Auth key validated" } # Disconnect from Tailscale if already connected function Disconnect-Tailscale { Write-Info "Checking current Tailscale connection status..." if (-not (Ensure-TailscaleInPath)) { return } if (Get-Command tailscale -ErrorAction SilentlyContinue) { try { $statusOutput = & tailscale status 2>&1 if ($LASTEXITCODE -eq 0 -and $statusOutput) { # Check if there's a connected device (status shows IP address) $firstLine = ($statusOutput -split "`n")[0] if ($firstLine -match "^\d+\.\d+\.\d+\.\d+") { Write-Warn "Tailscale is already connected. Disconnecting to switch account..." & tailscale down 2>&1 | Out-Null Start-Sleep -Seconds 2 # Stop service before removing state files Write-Info "Stopping Tailscale service..." Stop-Service -Name "Tailscale" -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 # Remove state to fully reset connection and switch to new account Write-Info "Removing Tailscale state files to fully reset connection..." # Remove state file in ProgramData $statePath = "$env:ProgramData\Tailscale\tailscaled.state" if (Test-Path $statePath) { Remove-Item $statePath -Force -ErrorAction SilentlyContinue Write-Info "Removed: $statePath" } # Remove state directory in ProgramData (may contain other files) $stateDir = "$env:ProgramData\Tailscale" if (Test-Path $stateDir) { Get-ChildItem -Path $stateDir -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue Write-Info "Cleaned: $stateDir" } # Remove user-specific state (if exists) $userStatePath = "$env:LOCALAPPDATA\Tailscale" if (Test-Path $userStatePath) { Get-ChildItem -Path $userStatePath -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue Write-Info "Cleaned: $userStatePath" } # Remove system profile state (older versions) $systemStatePath = "$env:SystemRoot\System32\config\systemprofile\AppData\Local\Tailscale" if (Test-Path $systemStatePath) { Get-ChildItem -Path $systemStatePath -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue Write-Info "Cleaned: $systemStatePath" } # Start service again Write-Info "Starting Tailscale service..." Start-Service -Name "Tailscale" -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 # Verify service is running after restart $service = Get-Service -Name "Tailscale" -ErrorAction SilentlyContinue if ($service -and $service.Status -eq "Running") { Write-Info "Tailscale service restarted successfully" } else { Write-Warn "Tailscale service may not be ready yet" } } } } catch { # If status check fails, assume not connected Write-Info "Tailscale appears to be disconnected" } } } # Connect to Tailscale with auth key function Connect-Tailscale { Write-Info "Connecting to Tailscale..." # Disconnect if already connected Disconnect-Tailscale if (-not (Ensure-TailscaleInPath)) { Exit-WithError "tailscale command not found. Please restart your PowerShell session or add Tailscale to PATH." } # Connect using auth key (and optionally custom login server) try { if (-not [string]::IsNullOrWhiteSpace($LOGIN_SERVER)) { Write-Info "Using custom login server: $LOGIN_SERVER" & tailscale up --force-reauth --login-server=$LOGIN_SERVER --authkey=$AUTH_KEY --accept-routes --accept-dns } else { Write-Info "Using default Tailscale server" & tailscale up --force-reauth --authkey=$AUTH_KEY --accept-routes --accept-dns } if ($LASTEXITCODE -eq 0) { Write-Info "Successfully connected to Tailscale!" # Display Tailscale status Write-Info "Tailscale status:" & tailscale status } else { $errorMsg = "Failed to connect to Tailscale. Exit code: $LASTEXITCODE`n" if (-not [string]::IsNullOrWhiteSpace($LOGIN_SERVER)) { $errorMsg += "Please check your login server URL and auth key and try again" } else { $errorMsg += "Please check your auth key and try again" } Exit-WithError $errorMsg } } catch { Exit-WithError "Error connecting to Tailscale: $_" } } # Main execution function Main { Write-Info "Starting Tailscale installation and connection script..." Test-PowerShellVersion Test-Administrator if (Test-TailscaleInstalled) { Write-Info "Tailscale is already installed" } else { Install-Tailscale } Enable-TailscaleService Wait-ForTailscaleService # Refresh PATH so tailscale is available in this session (e.g. after fresh install) if (-not (Ensure-TailscaleInPath)) { Exit-WithError "Tailscale appears installed but tailscale command is not found. Please restart PowerShell and run this script again." } Validate-LoginServer Validate-AuthKey Connect-Tailscale Write-Info "Script completed successfully!" } # Run main function with error handling try { Main } catch { Write-Error "An error occurred: $_" Write-Error "Error details: $($_.Exception.Message)" if ($_.ScriptStackTrace) { Write-Error "Stack trace: $($_.ScriptStackTrace)" } Write-Host "" Write-Host "Closing in 20 seconds..." -ForegroundColor Yellow Start-Sleep -Seconds 20 exit 1 } # Keep window open briefly on success Write-Host "" Write-Host "Closing in 20 seconds..." -ForegroundColor Green Start-Sleep -Seconds 20