3a3e3ed202
- Replace hardcoded AGENT_VERSION constant with injected version variable - Bump agent version to 0.3.2 - Increase tailscale up/status timeout from 60s to 120s
201 lines
4.5 KiB
Go
201 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
tsCmd *exec.Cmd
|
|
tsCmdMu sync.Mutex
|
|
tsIP string
|
|
tsDataDir string
|
|
tsSocket string
|
|
)
|
|
|
|
type tailscaleStatus struct {
|
|
Self struct {
|
|
TailscaleIPs []string `json:"TailscaleIPs"`
|
|
} `json:"Self"`
|
|
}
|
|
|
|
func tailscaleBin(name string) string {
|
|
// Prefer bundled binaries (tailscale-bin/<os>/tailscaled etc.).
|
|
ex, err := os.Executable()
|
|
if err == nil {
|
|
bundled := filepath.Join(filepath.Dir(ex), "tailscale-bin", runtime.GOOS, name)
|
|
if runtime.GOOS == "windows" {
|
|
bundled += ".exe"
|
|
}
|
|
if _, err := os.Stat(bundled); err == nil {
|
|
return bundled
|
|
}
|
|
}
|
|
if p, err := exec.LookPath(name); err == nil {
|
|
return p
|
|
}
|
|
return name
|
|
}
|
|
|
|
func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, error) {
|
|
tsCmdMu.Lock()
|
|
defer tsCmdMu.Unlock()
|
|
|
|
if tsCmd != nil {
|
|
return tsIP, nil
|
|
}
|
|
|
|
if dataDir == "" {
|
|
return "", fmt.Errorf("tailscale data dir is empty")
|
|
}
|
|
tsDataDir = filepath.Join(dataDir, "tailscale")
|
|
if err := os.MkdirAll(tsDataDir, 0700); err != nil {
|
|
return "", fmt.Errorf("create tailscale dir: %w", err)
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
// Windows uses named pipes for tailscaled IPC, not Unix sockets.
|
|
tsSocket = `\\.\pipe\studioe5-tailscaled`
|
|
} else {
|
|
tsSocket = filepath.Join(tsDataDir, "tailscaled.sock")
|
|
}
|
|
stateFile := filepath.Join(tsDataDir, "tailscaled.state")
|
|
|
|
log.Printf("Starting tailscaled for node %s (socket=%s)", nodeID, tsSocket)
|
|
tsCmd = exec.Command(tailscaleBin("tailscaled"),
|
|
"--state="+stateFile,
|
|
"--socket="+tsSocket,
|
|
"--tun=userspace-networking",
|
|
)
|
|
hideWindow(tsCmd)
|
|
// Redirect tailscaled output to a dedicated log file.
|
|
tsLogPath := filepath.Join(tsDataDir, "tailscaled.log")
|
|
if tsLogFile, err := os.OpenFile(tsLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
|
|
tsCmd.Stdout = tsLogFile
|
|
tsCmd.Stderr = tsLogFile
|
|
} else {
|
|
log.Printf("Cannot open tailscaled log file %s: %v", tsLogPath, err)
|
|
}
|
|
if err := tsCmd.Start(); err != nil {
|
|
tsCmd = nil
|
|
return "", fmt.Errorf("start tailscaled: %w", err)
|
|
}
|
|
|
|
// Give tailscaled a moment to start listening.
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Bring the interface up with the auth key.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
defer cancel()
|
|
|
|
upArgs := []string{
|
|
"--socket=" + tsSocket,
|
|
"up",
|
|
"--authkey=" + authKey,
|
|
"--login-server=" + headscaleURL,
|
|
"--hostname=" + nodeID,
|
|
"--accept-dns=false",
|
|
}
|
|
if runtime.GOOS != "windows" {
|
|
// --operator is only meaningful on Unix systems.
|
|
upArgs = append(upArgs, "--operator=root")
|
|
}
|
|
upCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"), upArgs...)
|
|
hideWindow(upCmd)
|
|
upCmd.Stdout = log.Writer()
|
|
upCmd.Stderr = log.Writer()
|
|
if err := upCmd.Run(); err != nil {
|
|
_ = tsCmd.Process.Kill()
|
|
_ = tsCmd.Wait()
|
|
tsCmd = nil
|
|
return "", fmt.Errorf("tailscale up: %w", err)
|
|
}
|
|
|
|
// Wait for an IP address.
|
|
for {
|
|
statusCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
|
"--socket="+tsSocket,
|
|
"status", "--json",
|
|
)
|
|
hideWindow(statusCmd)
|
|
out, err := statusCmd.Output()
|
|
if err != nil {
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = tsCmd.Process.Kill()
|
|
_ = tsCmd.Wait()
|
|
tsCmd = nil
|
|
return "", fmt.Errorf("tailscale status: %w", err)
|
|
default:
|
|
time.Sleep(1 * time.Second)
|
|
continue
|
|
}
|
|
}
|
|
var st tailscaleStatus
|
|
if err := json.Unmarshal(out, &st); err == nil && len(st.Self.TailscaleIPs) > 0 {
|
|
tsIP = st.Self.TailscaleIPs[0]
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = tsCmd.Process.Kill()
|
|
_ = tsCmd.Wait()
|
|
tsCmd = nil
|
|
return "", fmt.Errorf("tailscale IP timeout")
|
|
default:
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
|
|
log.Printf("Tailscale started with IP: %s", tsIP)
|
|
return tsIP, nil
|
|
}
|
|
|
|
func stopTailscale() {
|
|
tsCmdMu.Lock()
|
|
defer tsCmdMu.Unlock()
|
|
stopTailscaleLocked()
|
|
}
|
|
|
|
func stopTailscaleLocked() {
|
|
if tsCmd == nil || tsCmd.Process == nil {
|
|
return
|
|
}
|
|
if tsSocket != "" {
|
|
downCmd := exec.Command(tailscaleBin("tailscale"), "--socket="+tsSocket, "down")
|
|
hideWindow(downCmd)
|
|
_ = downCmd.Run()
|
|
}
|
|
_ = tsCmd.Process.Kill()
|
|
_ = tsCmd.Wait()
|
|
tsCmd = nil
|
|
tsIP = ""
|
|
log.Printf("Tailscale stopped")
|
|
}
|
|
|
|
func isTailscaleRunning() bool {
|
|
tsCmdMu.Lock()
|
|
defer tsCmdMu.Unlock()
|
|
if tsCmd == nil || tsCmd.Process == nil {
|
|
return false
|
|
}
|
|
// Signal 0 checks process existence without affecting it.
|
|
return tsCmd.Process.Signal(syscall.Signal(0)) == nil
|
|
}
|
|
|
|
func getTailscaleIP() string {
|
|
tsCmdMu.Lock()
|
|
defer tsCmdMu.Unlock()
|
|
return tsIP
|
|
}
|
|
|
|
|