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 }