A prior .exe(NSIS) install leaves Tailscale on disk but not on PATH; the MSI then fails with error 1603 trying to install over it. Now detect the existing binary at C:\Program Files\Tailscale\tailscale.exe and skip installation, and treat a nonzero msiexec exit as success when the binary is already present. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
538 lines
20 KiB
PowerShell
538 lines
20 KiB
PowerShell
# FARMQ Headscale Windows One-Click Installation Script
|
|
# Usage: Run in Administrator PowerShell
|
|
# iex ((New-Object System.Net.WebClient).DownloadString('https://git.0bin.in/.../farmq-install-en.ps1'))
|
|
|
|
param(
|
|
[switch]$Force,
|
|
[string]$HeadscaleServer = "http://head.pharmq.kr",
|
|
[string]$PreAuthKey = "b46923995afeaec90e588168f2e1bf99801775e8657ce003",
|
|
[string]$FarmqNetwork = "100.64.0.0/10"
|
|
)
|
|
|
|
# Set console to support Unicode characters
|
|
$PSDefaultParameterValues['*:Encoding'] = 'utf8'
|
|
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
|
|
}
|
|
|
|
# ================================
|
|
# Color Output Functions
|
|
# ================================
|
|
function Write-Header {
|
|
param([string]$Text)
|
|
Write-Host ""
|
|
Write-Host "============================================" -ForegroundColor Magenta
|
|
Write-Host $Text -ForegroundColor White
|
|
Write-Host "============================================" -ForegroundColor Magenta
|
|
Write-Host ""
|
|
}
|
|
|
|
function Write-Status {
|
|
param([string]$Message)
|
|
Write-Host ""
|
|
Write-Host "[*] $Message" -ForegroundColor Blue
|
|
}
|
|
|
|
function Write-Success {
|
|
param([string]$Message)
|
|
Write-Host ""
|
|
Write-Host "[+] $Message" -ForegroundColor Green
|
|
}
|
|
|
|
function Write-Error {
|
|
param([string]$Message)
|
|
Write-Host ""
|
|
Write-Host "[!] ERROR: $Message" -ForegroundColor Red
|
|
}
|
|
|
|
function Write-Warning {
|
|
param([string]$Message)
|
|
Write-Host ""
|
|
Write-Host "[!] WARNING: $Message" -ForegroundColor Yellow
|
|
}
|
|
|
|
function Write-Info {
|
|
param([string]$Message)
|
|
Write-Host ""
|
|
Write-Host "[i] $Message" -ForegroundColor Cyan
|
|
}
|
|
|
|
# ================================
|
|
# System Requirements Check
|
|
# ================================
|
|
function Test-Requirements {
|
|
Write-Status "Checking system requirements..."
|
|
|
|
# Check administrator privileges
|
|
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
|
|
|
if (-NOT $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
Write-Error "This script requires administrator privileges."
|
|
Write-Info "Please restart using one of these methods:"
|
|
Write-Info "1. Windows Key + X -> 'Windows PowerShell (Admin)'"
|
|
Write-Info "2. Right-click PowerShell -> 'Run as Administrator'"
|
|
Write-Host ""
|
|
Read-Host "Press any key to exit..."
|
|
exit 1
|
|
}
|
|
|
|
# Check Windows version
|
|
$osVersion = [System.Environment]::OSVersion.Version
|
|
if ($osVersion.Major -lt 10) {
|
|
Write-Warning "Windows 10 or later recommended. Current: Windows $($osVersion.Major).$($osVersion.Minor)"
|
|
}
|
|
|
|
# Check internet connection
|
|
try {
|
|
Microsoft.PowerShell.Management\Test-Connection "8.8.8.8" -Count 1 -Quiet | Out-Null
|
|
}
|
|
catch {
|
|
Write-Warning "Please check your internet connection."
|
|
}
|
|
|
|
Write-Success "System requirements check completed"
|
|
}
|
|
|
|
# ================================
|
|
# Install Tailscale
|
|
# ================================
|
|
function Install-Tailscale {
|
|
Write-Status "Checking Tailscale installation..."
|
|
|
|
# Check existing installation (PATH or the default install directory).
|
|
# IMPORTANT: a prior run may have installed Tailscale via the .exe (NSIS) even
|
|
# though it wasn't on PATH. Re-installing over it with the MSI fails with
|
|
# error 1603. So if the binary already exists, skip installation entirely and
|
|
# just make sure it's on PATH for this session.
|
|
$tailscaleExe = "C:\Program Files\Tailscale\tailscale.exe"
|
|
$tailscalePath = Get-Command "tailscale" -ErrorAction SilentlyContinue
|
|
if ($tailscalePath -or (Test-Path $tailscaleExe)) {
|
|
Write-Info "Tailscale is already installed. Skipping installation."
|
|
if (-not $tailscalePath -and ($env:Path -notlike "*Tailscale*")) {
|
|
$env:Path = "$env:Path;C:\Program Files\Tailscale"
|
|
}
|
|
$version = & $tailscaleExe version 2>$null | Select-Object -First 1
|
|
if ($version) { Write-Info "Current version: $version" }
|
|
return
|
|
}
|
|
|
|
Write-Info "Installing Tailscale for Windows..."
|
|
|
|
# Use the official MSI from the 'latest' stable channel and install it with
|
|
# msiexec /quiet. The previous approach (tailscale-setup-latest.exe /S) is the
|
|
# NSIS GUI installer; its silent switch does NOT reliably register the
|
|
# 'Tailscale' service or drop tailscale.exe before the script continues, which
|
|
# caused "service not found" / "executable not found" right after install.
|
|
# The MSI installs the service synchronously and is the supported unattended path.
|
|
# NOTE: GitHub's "latest release" tag and pkgs.tailscale.com/stable can be out
|
|
# of sync, so we use the version-less 'latest' alias which always exists.
|
|
$downloadUrl = "https://pkgs.tailscale.com/stable/tailscale-setup-latest-amd64.msi"
|
|
$tempPath = "$env:TEMP\tailscale-setup-latest.msi"
|
|
$logPath = "$env:TEMP\tailscale-install.log"
|
|
|
|
try {
|
|
Write-Status "Downloading Tailscale from: $downloadUrl"
|
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempPath -UseBasicParsing
|
|
|
|
Write-Status "Installing Tailscale... (please wait)"
|
|
$proc = Start-Process -FilePath "msiexec.exe" `
|
|
-ArgumentList "/i", "`"$tempPath`"", "/quiet", "/norestart", "/l*v", "`"$logPath`"" `
|
|
-Wait -PassThru
|
|
# 0 = success, 3010 = success but reboot required
|
|
if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) {
|
|
# 1603 etc. often means a conflicting/partial install already exists.
|
|
# If the binary is nonetheless present, treat it as installed and move on;
|
|
# otherwise surface the failure with the log path.
|
|
if (Test-Path $tailscaleExe) {
|
|
Write-Warning "msiexec exit code $($proc.ExitCode), but Tailscale is already present. Continuing."
|
|
} else {
|
|
throw "msiexec returned exit code $($proc.ExitCode). See log: $logPath"
|
|
}
|
|
}
|
|
|
|
# Refresh PATH environment variable
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
|
|
# Make sure the install dir is on PATH for this session
|
|
if (Test-Path $tailscaleExe) {
|
|
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
|
if ($currentPath -notlike "*Tailscale*") {
|
|
[Environment]::SetEnvironmentVariable("Path", "$currentPath;C:\Program Files\Tailscale", "Machine")
|
|
}
|
|
if ($env:Path -notlike "*Tailscale*") {
|
|
$env:Path = "$env:Path;C:\Program Files\Tailscale"
|
|
}
|
|
}
|
|
|
|
# Wait for tailscale.exe to actually appear (install can lag a few seconds)
|
|
Write-Status "Waiting for Tailscale to be ready..."
|
|
$ready = $false
|
|
for ($i = 0; $i -lt 15; $i++) {
|
|
if ((Get-Command "tailscale" -ErrorAction SilentlyContinue) -or (Test-Path $tailscaleExe)) {
|
|
$ready = $true
|
|
break
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
if (-not $ready) {
|
|
throw "Tailscale executable did not appear after installation. See log: $logPath"
|
|
}
|
|
|
|
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
|
|
Write-Success "Tailscale installation completed"
|
|
|
|
}
|
|
catch {
|
|
Write-Error "Tailscale installation failed: $($_.Exception.Message)"
|
|
throw
|
|
}
|
|
}
|
|
|
|
# ================================
|
|
# Start Tailscale Service
|
|
# ================================
|
|
function Start-TailscaleService {
|
|
Write-Status "Starting Tailscale service..."
|
|
|
|
try {
|
|
# The MSI registers a service named "Tailscale"; it may take a few seconds
|
|
# to appear, so poll for it before giving up.
|
|
$service = $null
|
|
for ($i = 0; $i -lt 15; $i++) {
|
|
$service = Get-Service -Name "Tailscale" -ErrorAction SilentlyContinue
|
|
if ($service) { break }
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
|
|
if ($service) {
|
|
if ($service.Status -ne "Running") {
|
|
Start-Service -Name "Tailscale"
|
|
Start-Sleep -Seconds 3
|
|
}
|
|
Write-Success "Tailscale service is running."
|
|
} else {
|
|
# Fall back to launching tailscaled directly if the service is missing
|
|
Write-Warning "Tailscale service not found. Attempting manual start..."
|
|
$tailscaled = "C:\Program Files\Tailscale\tailscaled.exe"
|
|
if (Test-Path $tailscaled) {
|
|
Start-Process -FilePath $tailscaled -ArgumentList "install" -Wait -ErrorAction SilentlyContinue
|
|
Start-Sleep -Seconds 3
|
|
$service = Get-Service -Name "Tailscale" -ErrorAction SilentlyContinue
|
|
if ($service -and $service.Status -ne "Running") {
|
|
Start-Service -Name "Tailscale" -ErrorAction SilentlyContinue
|
|
Start-Sleep -Seconds 3
|
|
}
|
|
if ($service) { Write-Success "Tailscale service is running." }
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Warning "Failed to start service: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# ================================
|
|
# Register with Headscale
|
|
# ================================
|
|
function Register-Headscale {
|
|
Write-Status "Registering with Headscale server..."
|
|
|
|
# Find Tailscale executable path
|
|
$tailscaleCmd = Get-Command "tailscale" -ErrorAction SilentlyContinue
|
|
if (-not $tailscaleCmd) {
|
|
$tailscaleExe = "C:\Program Files\Tailscale\tailscale.exe"
|
|
if (Test-Path $tailscaleExe) {
|
|
$tailscaleCmd = @{Source = $tailscaleExe}
|
|
} else {
|
|
Write-Error "Tailscale executable not found."
|
|
return $false
|
|
}
|
|
}
|
|
|
|
$tailscalePath = $tailscaleCmd.Source
|
|
|
|
# Check existing connection
|
|
try {
|
|
$status = & $tailscalePath status 2>$null
|
|
if ($LASTEXITCODE -eq 0 -and $status) {
|
|
Write-Warning "Already connected to Tailscale/Headscale network."
|
|
|
|
# Show current connection status
|
|
Write-Info "Current connection status:"
|
|
$status | Select-Object -First 5 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
# Check force registration option
|
|
if ($Force) {
|
|
Write-Warning "Force registration option is enabled."
|
|
Write-Info "Disconnecting existing connection and re-registering..."
|
|
} else {
|
|
$response = Read-Host "Disconnect existing connection and register with FARMQ Headscale? (Y/n)"
|
|
if ($response -eq "" -or $response -match "^[Yy]") {
|
|
Write-Info "Disconnecting existing connection..."
|
|
} else {
|
|
Write-Info "Skipping registration."
|
|
return $true
|
|
}
|
|
}
|
|
|
|
# Disconnect existing connection
|
|
try {
|
|
& $tailscalePath logout 2>$null
|
|
Start-Sleep -Seconds 3
|
|
Write-Success "Existing connection disconnected."
|
|
}
|
|
catch {
|
|
Write-Warning "Error during disconnection, but continuing..."
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
# Not connected (normal)
|
|
}
|
|
|
|
Write-Info "Headscale Server: $HeadscaleServer"
|
|
Write-Info "Pre-auth Key: $($PreAuthKey.Substring(0,8))***************"
|
|
|
|
# Attempt Headscale registration
|
|
Write-Status "Executing registration command..."
|
|
|
|
try {
|
|
$arguments = @(
|
|
"up",
|
|
"--login-server=$HeadscaleServer",
|
|
"--authkey=$PreAuthKey",
|
|
"--accept-routes",
|
|
"--accept-dns=false"
|
|
)
|
|
|
|
& $tailscalePath $arguments
|
|
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Success "Headscale registration successful!"
|
|
return $true
|
|
} else {
|
|
Write-Error "Automatic registration failed."
|
|
Write-Info "Manual registration command:"
|
|
Write-Host "tailscale up --login-server=`"$HeadscaleServer`" --authkey=`"$PreAuthKey`""
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-Error "Registration error: $($_.Exception.Message)"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# ================================
|
|
# Configure Firewall
|
|
# ================================
|
|
function Configure-Firewall {
|
|
Write-Status "Configuring firewall settings..."
|
|
|
|
try {
|
|
# Add Windows Defender firewall exception
|
|
$ruleName = "Tailscale-FARMQ"
|
|
$existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
|
|
|
|
if (-not $existingRule) {
|
|
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Protocol UDP -LocalPort 41641 -Action Allow -Profile Any | Out-Null
|
|
New-NetFirewallRule -DisplayName "$ruleName-Outbound" -Direction Outbound -Protocol UDP -LocalPort 41641 -Action Allow -Profile Any | Out-Null
|
|
Write-Info "Windows Defender firewall exceptions added."
|
|
}
|
|
|
|
Write-Success "Firewall configuration completed"
|
|
}
|
|
catch {
|
|
Write-Warning "Firewall configuration error: $($_.Exception.Message)"
|
|
Write-Info "Please manually allow Tailscale through Windows firewall."
|
|
}
|
|
}
|
|
|
|
# ================================
|
|
# Verify Connection
|
|
# ================================
|
|
function Test-NetworkConnection {
|
|
Write-Status "Verifying network connection..."
|
|
|
|
Start-Sleep -Seconds 5
|
|
|
|
# Find Tailscale executable path
|
|
$tailscaleCmd = Get-Command "tailscale" -ErrorAction SilentlyContinue
|
|
if (-not $tailscaleCmd) {
|
|
$tailscaleExe = "C:\Program Files\Tailscale\tailscale.exe"
|
|
if (Test-Path $tailscaleExe) {
|
|
$tailscaleCmd = @{Source = $tailscaleExe}
|
|
} else {
|
|
Write-Error "Tailscale executable not found."
|
|
return
|
|
}
|
|
}
|
|
|
|
$tailscalePath = $tailscaleCmd.Source
|
|
|
|
try {
|
|
$status = & $tailscalePath status 2>$null
|
|
if ($LASTEXITCODE -ne 0 -or -not $status) {
|
|
Write-Error "Tailscale connection issue detected."
|
|
return
|
|
}
|
|
|
|
# Get IP addresses
|
|
$ipv4 = & $tailscalePath ip -4 2>$null
|
|
$ipv6 = & $tailscalePath ip -6 2>$null
|
|
|
|
Write-Success "Headscale network connection completed!"
|
|
Write-Info "Assigned IPv4: $(if($ipv4){$ipv4}else{'N/A'})"
|
|
Write-Info "Assigned IPv6: $(if($ipv6){$ipv6}else{'N/A'})"
|
|
|
|
# Network connectivity test
|
|
Write-Status "Testing network connectivity..."
|
|
|
|
try {
|
|
Microsoft.PowerShell.Management\Test-Connection "100.64.0.1" -Count 2 -Quiet | Out-Null
|
|
Write-Success "FARMQ network ($FarmqNetwork) connection successful!"
|
|
}
|
|
catch {
|
|
Write-Warning "Network test failed. Please check firewall settings."
|
|
}
|
|
|
|
# Show connected nodes
|
|
Write-Info "Network status:"
|
|
$status | Select-Object -First 10 | ForEach-Object {
|
|
Write-Host " $_" -ForegroundColor Gray
|
|
}
|
|
}
|
|
catch {
|
|
Write-Error "Connection verification failed: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# ================================
|
|
# Cleanup
|
|
# ================================
|
|
function Complete-Installation {
|
|
Write-Status "Completing installation..."
|
|
|
|
# Clean temporary files
|
|
Get-ChildItem "$env:TEMP\tailscale*" -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
|
|
|
|
Write-Success "Cleanup completed"
|
|
}
|
|
|
|
# ================================
|
|
# Show Final Information
|
|
# ================================
|
|
function Show-FinalInfo {
|
|
Write-Header "FARMQ Headscale Windows Installation Complete!"
|
|
|
|
# System information
|
|
$computerName = $env:COMPUTERNAME
|
|
$tailscaleCmd = Get-Command "tailscale" -ErrorAction SilentlyContinue
|
|
if (-not $tailscaleCmd) {
|
|
$tailscaleExe = "C:\Program Files\Tailscale\tailscale.exe"
|
|
if (Test-Path $tailscaleExe) {
|
|
$tailscaleCmd = @{Source = $tailscaleExe}
|
|
}
|
|
}
|
|
|
|
if ($tailscaleCmd) {
|
|
$tailscaleIP = & $tailscaleCmd.Source ip -4 2>$null
|
|
}
|
|
|
|
$osVersion = [System.Environment]::OSVersion.Version
|
|
|
|
Write-Host "Installation completed successfully!" -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
Write-Host "System Information:" -ForegroundColor Cyan
|
|
Write-Host " Computer Name: $computerName"
|
|
Write-Host " Tailscale IP: $(if($tailscaleIP){$tailscaleIP}else{'N/A'})"
|
|
Write-Host " OS: Windows $($osVersion.Major).$($osVersion.Minor)"
|
|
Write-Host " Headscale Server: $HeadscaleServer"
|
|
|
|
Write-Host ""
|
|
Write-Host "Useful Commands:" -ForegroundColor Yellow
|
|
Write-Host " tailscale status # Check connection status"
|
|
Write-Host " tailscale ip # Show assigned IP"
|
|
Write-Host " tailscale ping <node> # Test connection to other nodes"
|
|
Write-Host " tailscale logout # Disconnect from network"
|
|
|
|
Write-Host ""
|
|
Write-Host "FARMQ Management Pages:" -ForegroundColor Magenta
|
|
Write-Host " http://192.168.0.151:5002"
|
|
Write-Host " http://192.168.0.151:5002/vms (VM Management)"
|
|
|
|
Write-Host ""
|
|
Write-Host "If you encounter issues, check:" -ForegroundColor White
|
|
Write-Host " 1. Windows Firewall settings"
|
|
Write-Host " 2. Antivirus software exceptions"
|
|
Write-Host " 3. Corporate network policies"
|
|
|
|
Write-Header "Installation Complete - You can now use FARMQ network!"
|
|
}
|
|
|
|
# ================================
|
|
# Main Function
|
|
# ================================
|
|
function Main {
|
|
# Stop on errors
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
Write-Header "FARMQ Headscale Windows One-Click Installation"
|
|
|
|
try {
|
|
# Installation process
|
|
Test-Requirements
|
|
Install-Tailscale
|
|
Start-TailscaleService
|
|
$registerSuccess = Register-Headscale
|
|
|
|
if ($registerSuccess) {
|
|
Configure-Firewall
|
|
Test-NetworkConnection
|
|
Complete-Installation
|
|
Show-FinalInfo
|
|
} else {
|
|
Write-Warning "Registration failed but Tailscale is installed."
|
|
Write-Info "Please complete registration manually."
|
|
}
|
|
|
|
}
|
|
catch {
|
|
Write-Error "Installation error occurred: $($_.Exception.Message)"
|
|
Write-Info "If the problem persists, please contact administrator."
|
|
Write-Host ""
|
|
Read-Host "Press any key to exit..."
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# ================================
|
|
# Script Execution
|
|
# ================================
|
|
|
|
# Handle parameters
|
|
if ($args -contains "--help" -or $args -contains "-h") {
|
|
Write-Host "FARMQ Headscale Windows Installation Script"
|
|
Write-Host ""
|
|
Write-Host "Usage:"
|
|
Write-Host " iex ((New-Object System.Net.WebClient).DownloadString('https://git.0bin.in/.../farmq-install-en.ps1'))"
|
|
Write-Host ""
|
|
Write-Host "Options:"
|
|
Write-Host " -Force Force disconnect existing connection and re-register"
|
|
Write-Host " -HeadscaleServer Server address (default: http://head.pharmq.kr)"
|
|
Write-Host ""
|
|
Write-Host "Examples:"
|
|
Write-Host " # Force re-registration"
|
|
Write-Host " `$Force = `$true; iex ((New-Object System.Net.WebClient).DownloadString('https://git.0bin.in/.../farmq-install-en.ps1'))"
|
|
exit 0
|
|
}
|
|
|
|
# Handle Force parameter from URL or variable
|
|
if ($MyInvocation.MyCommand.Path -like "*force=1*" -or (Get-Variable -Name "ForceInstall" -ErrorAction SilentlyContinue)) {
|
|
$Force = $true
|
|
}
|
|
|
|
# Execute main function
|
|
Main |