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.
This commit is contained in:
175
agent/deploy/tunnel_manager_windows.go
Normal file
175
agent/deploy/tunnel_manager_windows.go
Normal file
@@ -0,0 +1,175 @@
|
||||
//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"))
|
||||
}
|
||||
Reference in New Issue
Block a user