fix: dashboard funnel crash, comrade user, Cloudflare tunnel auto-start

Add runtime comrade account, null-safe spread funnel API/UI, server-started
cloudflared connector with Calibrate token field and builtin fallback, and
simplify LAUNCH to delegate tunnel startup to AetherForge.
This commit is contained in:
AetherForge
2026-06-04 14:14:34 -07:00
parent 5fc601b564
commit 6bfce5d5ab
16 changed files with 390 additions and 106 deletions

View File

@@ -0,0 +1,9 @@
//go:build !windows
package cloudflared
// Start is a no-op on non-Windows builds.
func Start(_, _, _ string) error { return nil }
// Stop is a no-op on non-Windows builds.
func Stop() {}

View File

@@ -0,0 +1,139 @@
//go:build windows
package cloudflared
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"syscall"
)
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
}
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, resp.Body)
closeErr := f.Close()
if err != nil {
return "", err
}
if closeErr != nil {
return "", closeErr
}
return bin, nil
}
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
}