summaryrefslogtreecommitdiff
path: root/cmd/installer
diff options
context:
space:
mode:
authorSergeiEU <39683682+SergeiEU@users.noreply.github.com>2026-04-01 10:17:15 +0400
committerSergeiEU <39683682+SergeiEU@users.noreply.github.com>2026-04-01 10:17:15 +0400
commit1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 (patch)
treed8c85273ede547e03a5727bf185f5d07e87b4a08 /cmd/installer
downloadvpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.gz
vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.bz2
vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.zip
Initial importHEADmain
Diffstat (limited to 'cmd/installer')
-rw-r--r--cmd/installer/main.go257
1 files changed, 257 insertions, 0 deletions
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(`<?xml version="1.0" encoding="UTF-16"?>
+<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <RegistrationInfo>
+ <Description>vpnem VPN client</Description>
+ </RegistrationInfo>
+ <Triggers>
+ <LogonTrigger>
+ <Enabled>true</Enabled>
+ <Delay>PT15S</Delay>
+ </LogonTrigger>
+ </Triggers>
+ <Principals>
+ <Principal>
+ <LogonType>InteractiveToken</LogonType>
+ <RunLevel>HighestAvailable</RunLevel>
+ </Principal>
+ </Principals>
+ <Settings>
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
+ <AllowStartOnDemand>true</AllowStartOnDemand>
+ <AllowHardTerminate>true</AllowHardTerminate>
+ </Settings>
+ <Actions>
+ <Exec>
+ <Command>%s</Command>
+ <Arguments>--data "%s"</Arguments>
+ <WorkingDirectory>%s</WorkingDirectory>
+ </Exec>
+ </Actions>
+</Task>`, 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()
+}