using System.Net; using ADBulkTool.Models; using DnsClient; namespace ADBulkTool.Services; public static class DomainDiscovery { public static async Task DiscoverAsync(TimeSpan timeout) { var steps = new List(); try { var domain = await GetSystemDomainAsync(timeout); if (string.IsNullOrWhiteSpace(domain)) { return new DiscoveryResult(null, null, null, null, null, "Could not detect a DNS domain from this machine. Check that you are on a domain-joined network and that /etc/resolv.conf has a search or domain entry."); } steps.Add($"Detected domain: {domain}"); var searchBase = DomainToSearchBase(domain); steps.Add($"Derived search base: {searchBase}"); var dnsServers = GetDnsServers(); var dnsServer = dnsServers.FirstOrDefault(); steps.Add(dnsServer is not null ? $"DNS server from /etc/resolv.conf: {dnsServer}" : "No DNS server found in /etc/resolv.conf, using system default."); var ldapHost = await FindDomainControllerViaSrvAsync(domain, dnsServer, timeout); if (ldapHost is not null) { steps.Add($"Found DC via SRV record: {ldapHost}"); return new DiscoveryResult(domain, dnsServer, ldapHost, 636, searchBase, string.Join(" | ", steps)); } ldapHost = await FindDomainControllerViaLdapSrvAsync(domain, dnsServer, timeout); if (ldapHost is not null) { steps.Add($"Found DC via generic LDAP SRV: {ldapHost}"); return new DiscoveryResult(domain, dnsServer, ldapHost, 636, searchBase, string.Join(" | ", steps)); } var guessedHost = $"dc01.{domain}"; steps.Add($"SRV lookup failed. Guessed DC: {guessedHost}"); return new DiscoveryResult(domain, dnsServer, guessedHost, 636, searchBase, string.Join(" | ", steps)); } catch (Exception ex) { return new DiscoveryResult(null, null, null, null, null, $"Discovery failed: {ex.Message}"); } } public static List GetDnsServers() { var servers = new List(); try { var lines = File.ReadAllLines("/etc/resolv.conf"); foreach (var line in lines) { var trimmed = line.Trim(); if (trimmed.StartsWith("nameserver ", StringComparison.OrdinalIgnoreCase)) { var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length >= 2 && IPAddress.TryParse(parts[1], out _)) { servers.Add(parts[1]); } } } } catch { } return servers; } public static string? DomainToSearchBase(string? domain) { if (string.IsNullOrWhiteSpace(domain)) { return null; } var parts = domain.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length == 0) { return null; } return string.Join(",", parts.Select(p => $"DC={p}")); } private static async Task GetSystemDomainAsync(TimeSpan timeout) { try { using var cts = new CancellationTokenSource(timeout); var resolvDomain = await Task.Run(() => GetDomainFromResolvConf(), cts.Token); if (!string.IsNullOrWhiteSpace(resolvDomain)) { return resolvDomain; } var hostDomain = await Task.Run(() => GetDomainFromHostname(), cts.Token); if (!string.IsNullOrWhiteSpace(hostDomain)) { return hostDomain; } var ipDomain = await Task.Run(() => { try { return System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; } catch { return string.Empty; } }, cts.Token); if (!string.IsNullOrWhiteSpace(ipDomain) && !string.Equals(ipDomain, "localdomain", StringComparison.OrdinalIgnoreCase)) { return ipDomain.TrimEnd('.'); } return null; } catch { return null; } } private static string? GetDomainFromResolvConf() { try { var lines = File.ReadAllLines("/etc/resolv.conf"); foreach (var line in lines) { var trimmed = line.Trim(); if (trimmed.StartsWith("domain ", StringComparison.OrdinalIgnoreCase)) { var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length >= 2 && !string.IsNullOrWhiteSpace(parts[1])) { return parts[1].TrimEnd('.'); } } if (trimmed.StartsWith("search ", StringComparison.OrdinalIgnoreCase)) { var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length >= 2 && !string.IsNullOrWhiteSpace(parts[1])) { return parts[1].TrimEnd('.'); } } } } catch { } return null; } private static string? GetDomainFromHostname() { try { var hostname = Dns.GetHostEntry("localhost").HostName; var dotIndex = hostname.IndexOf('.'); if (dotIndex > 0 && dotIndex < hostname.Length - 1) { var domain = hostname[(dotIndex + 1)..].TrimEnd('.'); return string.IsNullOrWhiteSpace(domain) ? null : domain; } } catch { } try { var fullHostname = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().HostName; var dotIndex = fullHostname.IndexOf('.'); if (dotIndex > 0 && dotIndex < fullHostname.Length - 1) { var domain = fullHostname[(dotIndex + 1)..].TrimEnd('.'); return string.IsNullOrWhiteSpace(domain) ? null : domain; } } catch { } return null; } private static async Task FindDomainControllerViaSrvAsync(string domain, string? dnsServer, TimeSpan timeout) { try { var srvQuery = $"_ldap._tcp.dc._msdcs.{domain}"; var lookup = CreateLookup(dnsServer); using var cts = new CancellationTokenSource(timeout); var result = await lookup.QueryAsync(srvQuery, QueryType.SRV, QueryClass.IN, cts.Token); var record = result.Answers.SrvRecords().FirstOrDefault(); return record?.DomainName.Value.TrimEnd('.'); } catch { return null; } } private static async Task FindDomainControllerViaLdapSrvAsync(string domain, string? dnsServer, TimeSpan timeout) { try { var srvQuery = $"_ldap._tcp.{domain}"; var lookup = CreateLookup(dnsServer); using var cts = new CancellationTokenSource(timeout); var result = await lookup.QueryAsync(srvQuery, QueryType.SRV, QueryClass.IN, cts.Token); var record = result.Answers.SrvRecords().FirstOrDefault(); return record?.DomainName.Value.TrimEnd('.'); } catch { return null; } } private static LookupClient CreateLookup(string? dnsServer) { if (dnsServer is not null && IPAddress.TryParse(dnsServer, out var ip)) { return new LookupClient(new LookupClientOptions(ip) { UseCache = false, Timeout = TimeSpan.FromSeconds(5) }); } return new LookupClient(new LookupClientOptions { UseCache = false, Timeout = TimeSpan.FromSeconds(5) }); } }