From 1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 Mon Sep 17 00:00:00 2001 From: SergeiEU <39683682+SergeiEU@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:17:15 +0400 Subject: Initial import --- cmd/installer/main.go | 257 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 cmd/installer/main.go (limited to 'cmd/installer/main.go') diff --git a/cmd/installer/main.go b/cmd/installer/main.go new file mode 100644 index 0000000..ac21dc9 --- /dev/null +++ b/cmd/installer/main.go @@ -0,0 +1,257 @@ +// vpnem-installer: Windows installer (GUI, no console window). +// Installs to Program Files, creates Task Scheduler task for UAC-free launch. +// Requires admin (one-time). Supports silent mode: vpnem-installer.exe /S +// Cross-compiles from Linux with -ldflags "-H windowsgui" +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +const ( + installDir = `C:\Program Files\vpnem` + dataDir = `C:\ProgramData\vpnem` + taskName = "vpnem" + + baseURL = "https://vpn.em-sysadmin.xyz/releases" + vpnemURL = baseURL + "/vpnem-windows-amd64.exe" + singboxURL = baseURL + "/sing-box.exe" + wintunURL = baseURL + "/wintun.dll" +) + +var ( + silent bool + noShortcut bool + launch bool + logFile *os.File +) + +func main() { + // Parse flags + for _, arg := range os.Args[1:] { + a := strings.ToLower(strings.TrimLeft(arg, "/-")) + switch a { + case "s", "silent": + silent = true + case "noshortcut": + noShortcut = true + case "launch": + launch = true + } + } + + os.MkdirAll(installDir, 0o755) + os.MkdirAll(dataDir, 0o755) + + // Log to data dir + lf, err := os.Create(filepath.Join(dataDir, "install.log")) + if err == nil { + logFile = lf + defer lf.Close() + log.SetOutput(lf) + } + + step("vpnem installer started") + step("install dir: " + installDir) + step("data dir: " + dataDir) + + // Kill running instances + step("stopping running instances") + exec.Command("taskkill", "/F", "/IM", "vpnem.exe").Run() + time.Sleep(time.Second) + + // Clean stale configs + step("cleaning old state") + for _, f := range []string{"state.json", "config.json", "cache.db"} { + os.Remove(filepath.Join(dataDir, f)) + os.Remove(filepath.Join(installDir, f)) + // Also clean old C:\ProxySwitcher location + os.Remove(filepath.Join(`C:\ProxySwitcher`, f)) + } + + // Download vpnem.exe + step("downloading vpnem") + if err := download(vpnemURL, filepath.Join(installDir, "vpnem.exe")); err != nil { + fatal("download vpnem: %v", err) + } + + // Download sing-box 1.11 (external subprocess, proven to work) + downloadIfMissing("sing-box.exe", singboxURL) + + // Download wintun.dll + downloadIfMissing("wintun.dll", wintunURL) + + // Create Task Scheduler task — runs vpnem as admin WITHOUT UAC prompt. + // This is the key: task created by admin runs with highest privileges silently. + step("creating scheduled task (UAC-free launch)") + createTask() + + // Desktop shortcut — launches via schtasks (no UAC popup) + if !noShortcut { + step("creating desktop shortcut") + createShortcut() + } + + step("installation complete") + + if launch { + step("launching vpnem") + exec.Command("schtasks", "/Run", "/TN", taskName).Run() + } + + if !silent { + showDoneMessage() + } +} + +// createTask sets up a scheduled task that: +// 1. Runs vpnem.exe with highest privileges (no UAC) +// 2. Starts at logon with 15s delay (autostart) +// The same task is used for both manual launch and autostart. +func createTask() { + exec.Command("schtasks", "/Delete", "/TN", taskName, "/F").Run() + + // Create XML task definition for full control + exePath := filepath.Join(installDir, "vpnem.exe") + xml := fmt.Sprintf(` + + + vpnem VPN client + + + + true + PT15S + + + + + InteractiveToken + HighestAvailable + + + + IgnoreNew + false + false + PT0S + true + true + + + + %s + --data "%s" + %s + + +`, exePath, dataDir, installDir) + + xmlPath := filepath.Join(dataDir, "task.xml") + os.WriteFile(xmlPath, []byte(xml), 0o644) + + cmd := exec.Command("schtasks", "/Create", "/TN", taskName, "/XML", xmlPath, "/F") + out, err := cmd.CombinedOutput() + if err != nil { + log.Printf("task create failed: %v\n%s", err, string(out)) + } else { + log.Println("task created ok") + } + os.Remove(xmlPath) +} + +// createShortcut makes a desktop shortcut that runs the scheduled task (no UAC) +// IconLocation points to vpnem.exe so the shortcut has the app icon +func createShortcut() { + exePath := filepath.Join(installDir, "vpnem.exe") + ps := ` +$ws = New-Object -ComObject WScript.Shell +$s = $ws.CreateShortcut("$env:USERPROFILE\Desktop\vpnem.lnk") +$s.TargetPath = "schtasks.exe" +$s.Arguments = "/Run /TN vpnem" +$s.WorkingDirectory = "` + installDir + `" +$s.IconLocation = "` + exePath + `,0" +$s.Description = "vpnem VPN client" +$s.Save() +` + cmd := exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps) + if err := cmd.Run(); err != nil { + log.Printf("shortcut failed: %v (non-critical)", err) + } +} + +func step(msg string) { + log.Println(msg) +} + +func fatal(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + log.Printf("FATAL: %s", msg) + if silent { + os.Exit(1) + } + showError(msg) + os.Exit(1) +} + +func download(url, dest string) error { + client := &http.Client{Timeout: 5 * time.Minute} + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP %d", resp.StatusCode) + } + + tmp := dest + ".tmp" + f, err := os.Create(tmp) + if err != nil { + return err + } + + written, err := io.Copy(f, resp.Body) + f.Close() + if err != nil { + os.Remove(tmp) + return err + } + + log.Printf(" %s (%.1f MB)", filepath.Base(dest), float64(written)/1024/1024) + return os.Rename(tmp, dest) +} + +func downloadIfMissing(filename, url string) { + path := filepath.Join(installDir, filename) + if _, err := os.Stat(path); os.IsNotExist(err) { + step("downloading " + filename) + if err := download(url, path); err != nil { + fatal("download %s: %v", filename, err) + } + } else { + step(filename + " already present, skipping") + } +} + +func showDoneMessage() { + msg := fmt.Sprintf("vpnem installed to %s\\n\\nDesktop shortcut created.\\nAutostart at logon enabled.\\n\\nNo admin prompts needed to launch.", installDir) + exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", + fmt.Sprintf(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show("%s", "vpnem installer", "OK", "Information")`, msg), + ).Run() +} + +func showError(msg string) { + exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", + fmt.Sprintf(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show("Installation failed:\n%s", "vpnem installer", "OK", "Error")`, msg), + ).Run() +} -- cgit v1.2.3