//go:build windows package cloudflared import ( "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "strings" "sync" "syscall" ) const maxCloudflaredBinaryBytes = 100 * 1024 * 1024 // 100 MB sanity cap const downloadURL = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe" var ( mu sync.Mutex running *exec.Cmd ) // Start runs cloudflared tunnel with the Zero Trust connector token (background, no service install). func Start(dataDir, deckRoot, token string) error { token = trimToken(token) if token == "" { return nil } bin, err := ensureBinary(deckRoot) if err != nil { return err } mu.Lock() defer mu.Unlock() if running != nil && running.Process != nil { log.Printf("[tunnel] Cloudflare connector already running (pid %d)", running.Process.Pid) return nil } if cloudflaredAlreadyRunning() { log.Printf("[tunnel] cloudflared.exe already running externally — skipping duplicate start") return nil } cmd := exec.Command(bin, "tunnel", "--no-autoupdate", "run", "--token", token) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} if err := cmd.Start(); err != nil { return fmt.Errorf("cloudflared start: %w", err) } running = cmd go func(c *exec.Cmd) { if err := c.Wait(); err != nil { log.Printf("[tunnel] cloudflared exited: %v", err) } mu.Lock() if running == c { running = nil } mu.Unlock() }(cmd) log.Printf("[tunnel] Cloudflare connector started (pid %d)", cmd.Process.Pid) return nil } // Stop terminates the background cloudflared process started by Start. func Stop() { mu.Lock() defer mu.Unlock() _ = stopLocked() } func stopLocked() error { if running == nil || running.Process == nil { return nil } _ = running.Process.Kill() _, _ = running.Process.Wait() running = nil return nil } func ensureBinary(deckRoot string) (string, error) { candidates := []string{ filepath.Join(deckRoot, "tools", "cloudflared.exe"), } if exe, err := os.Executable(); err == nil { candidates = append(candidates, filepath.Join(filepath.Dir(exe), "tools", "cloudflared.exe")) } for _, p := range candidates { if p == "" { continue } if st, err := os.Stat(p); err == nil && !st.IsDir() { return p, nil } } bin := candidates[0] if deckRoot == "" { if exe, err := os.Executable(); err == nil { bin = filepath.Join(filepath.Dir(exe), "tools", "cloudflared.exe") } } if err := os.MkdirAll(filepath.Dir(bin), 0755); err != nil { return "", err } log.Printf("[tunnel] Downloading cloudflared to %s", bin) resp, err := http.Get(downloadURL) if err != nil { return "", fmt.Errorf("download cloudflared: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("download cloudflared: HTTP %d", resp.StatusCode) } f, err := os.OpenFile(bin, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) if err != nil { return "", err } _, err = io.Copy(f, io.LimitReader(resp.Body, maxCloudflaredBinaryBytes)) closeErr := f.Close() if err != nil { return "", err } if closeErr != nil { return "", closeErr } return bin, nil } func cloudflaredAlreadyRunning() bool { out, err := exec.Command("tasklist", "/FI", "IMAGENAME eq cloudflared.exe", "/NH").Output() if err != nil { return false } return strings.Contains(strings.ToLower(string(out)), "cloudflared.exe") } func trimToken(s string) string { s = strings.TrimSpace(s) if len(s) >= 2 { if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') { s = strings.TrimSpace(s[1 : len(s)-1]) } } return s }