Files
AetherForge/agent/deploy/tunnel_manager_windows.go
AetherForge 5fc601b564 feat: fleet ops, KEV scan, tunnels, beacon fallback, persistence
Extend owned-fleet control with scheduled tasks, audit log, file browser,
HTTPS beacon when WS drops, protocol tunnels, registry/autostart forge
options, KEV exposure in full sys check with Telegram alerts, and UI/tests.
2026-06-04 09:34:33 -07:00

176 lines
4.0 KiB
Go

//go:build windows
package deploy
import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"syscall"
)
// TunnelKind identifies a tracked tunnel process.
type TunnelKind string
const (
TunnelCloudflared TunnelKind = "cloudflared"
TunnelSSHForward TunnelKind = "ssh_forward"
)
// SSHForwardMeta describes one local port forward on the agent.
type SSHForwardMeta struct {
LocalPort int `json:"local_port"`
RemoteHost string `json:"remote_host"`
RemotePort int `json:"remote_port"`
SSHUser string `json:"ssh_user,omitempty"`
JumpHost string `json:"jump_host,omitempty"`
}
type tunnelEntry struct {
kind TunnelKind
pid int
meta string // JSON metadata for ssh forwards; URL for cloudflared
}
var (
tunnelMu sync.Mutex
trackedTunnels []tunnelEntry
)
// RegisterTunnelPID records a background tunnel process for later stop/status.
func RegisterTunnelPID(kind TunnelKind, pid int, meta string) {
if pid <= 0 {
return
}
tunnelMu.Lock()
defer tunnelMu.Unlock()
if kind == TunnelCloudflared {
next := make([]tunnelEntry, 0, len(trackedTunnels)+1)
for _, t := range trackedTunnels {
if t.kind != TunnelCloudflared {
next = append(next, t)
}
}
trackedTunnels = append(next, tunnelEntry{kind: kind, pid: pid, meta: meta})
return
}
trackedTunnels = append(trackedTunnels, tunnelEntry{kind: kind, pid: pid, meta: meta})
}
const processQueryLimitedInformation = 0x1000
var processAliveFn = processAliveImpl
func processAlive(pid int) bool {
return processAliveFn(pid)
}
func processAliveImpl(pid int) bool {
if pid <= 0 {
return false
}
h, err := syscall.OpenProcess(processQueryLimitedInformation, false, uint32(pid))
if err != nil {
return false
}
defer syscall.CloseHandle(h)
var code uint32
err = syscall.GetExitCodeProcess(h, &code)
if err != nil {
return false
}
return code == 259 // STILL_ACTIVE
}
func killPID(pid int) error {
if pid <= 0 {
return fmt.Errorf("invalid pid")
}
return HiddenRun("taskkill", "/F", "/PID", fmt.Sprintf("%d", pid))
}
func pruneDead() {
alive := trackedTunnels[:0]
for _, t := range trackedTunnels {
if processAlive(t.pid) {
alive = append(alive, t)
}
}
trackedTunnels = alive
}
// TunnelStatus returns JSON describing tracked tunnel processes.
func TunnelStatus() string {
tunnelMu.Lock()
pruneDead()
snap := append([]tunnelEntry(nil), trackedTunnels...)
tunnelMu.Unlock()
st := TunnelStatusJSON{SSHForwards: []SSHForwardLive{}}
for _, t := range snap {
switch t.kind {
case TunnelCloudflared:
st.CloudflaredRunning = true
st.CloudflaredURL = t.meta
st.CloudflaredPID = t.pid
case TunnelSSHForward:
var meta SSHForwardMeta
_ = json.Unmarshal([]byte(t.meta), &meta)
st.SSHForwards = append(st.SSHForwards, SSHForwardLive{
LocalPort: meta.LocalPort,
RemoteHost: meta.RemoteHost,
RemotePort: meta.RemotePort,
SSHUser: meta.SSHUser,
JumpHost: meta.JumpHost,
PID: t.pid,
Running: true,
})
}
}
b, _ := json.Marshal(st)
return string(b)
}
// StopTunnels stops tracked tunnel kinds. Empty kinds stops all.
func StopTunnels(kinds ...TunnelKind) (stopped int, msgs []string) {
tunnelMu.Lock()
defer tunnelMu.Unlock()
pruneDead()
wantAll := len(kinds) == 0
want := map[TunnelKind]bool{}
for _, k := range kinds {
want[k] = true
}
remaining := trackedTunnels[:0]
for _, t := range trackedTunnels {
if wantAll || want[t.kind] {
if err := killPID(t.pid); err != nil {
msgs = append(msgs, fmt.Sprintf("%s pid %d: %v", t.kind, t.pid, err))
} else {
stopped++
msgs = append(msgs, fmt.Sprintf("stopped %s pid %d", t.kind, t.pid))
}
continue
}
remaining = append(remaining, t)
}
trackedTunnels = remaining
return stopped, msgs
}
// ResetTrackedTunnels clears registry without killing (tests only).
func ResetTrackedTunnels() {
tunnelMu.Lock()
trackedTunnels = nil
tunnelMu.Unlock()
}
// CloudflaredTargetFromEnv returns trimmed URL or empty.
func CloudflaredTargetFromEnv() string {
return strings.TrimSpace(os.Getenv("AETHERFORGE_TUNNEL_URL"))
}