375 lines
11 KiB
C#
375 lines
11 KiB
C#
using System.Net;
|
|
using System.Net.NetworkInformation;
|
|
using System.Runtime.InteropServices;
|
|
using ADBulkTool.Models;
|
|
using DnsClient;
|
|
|
|
namespace ADBulkTool.Services;
|
|
|
|
public static class DomainDiscovery
|
|
{
|
|
public static async Task<DiscoveryResult> DiscoverAsync(TimeSpan timeout)
|
|
{
|
|
var steps = new List<string>();
|
|
|
|
try
|
|
{
|
|
var domain = await GetSystemDomainAsync(timeout);
|
|
if (string.IsNullOrWhiteSpace(domain))
|
|
{
|
|
return new DiscoveryResult(null, null, null, null, null,
|
|
"Could not detect a DNS domain. Ensure the machine is on a domain network with a proper DNS configuration.");
|
|
}
|
|
|
|
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 found: {dnsServer}"
|
|
: "No DNS server found, using system default resolver.");
|
|
|
|
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<string> GetDnsServers()
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
return GetDnsServersWindows();
|
|
}
|
|
|
|
return GetDnsServersLinux();
|
|
}
|
|
|
|
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<string?> GetSystemDomainAsync(TimeSpan timeout)
|
|
{
|
|
try
|
|
{
|
|
using var cts = new CancellationTokenSource(timeout);
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
var resolvDomain = await Task.Run(() => GetDomainFromResolvConf(), cts.Token);
|
|
if (!string.IsNullOrWhiteSpace(resolvDomain))
|
|
{
|
|
return resolvDomain;
|
|
}
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
var winDomain = await Task.Run(() => GetDomainFromWindows(), cts.Token);
|
|
if (!string.IsNullOrWhiteSpace(winDomain))
|
|
{
|
|
return winDomain;
|
|
}
|
|
}
|
|
|
|
var hostDomain = await Task.Run(() => GetDomainFromHostname(), cts.Token);
|
|
if (!string.IsNullOrWhiteSpace(hostDomain))
|
|
{
|
|
return hostDomain;
|
|
}
|
|
|
|
var ipDomain = await Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
return 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 List<string> GetDnsServersWindows()
|
|
{
|
|
var servers = new List<string>();
|
|
|
|
try
|
|
{
|
|
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
|
|
{
|
|
if (ni.OperationalStatus != OperationalStatus.Up)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var props = ni.GetIPProperties();
|
|
if (props is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var dns in props.DnsAddresses)
|
|
{
|
|
if (dns.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && !servers.Contains(dns.ToString()))
|
|
{
|
|
servers.Add(dns.ToString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
return servers;
|
|
}
|
|
|
|
private static List<string> GetDnsServersLinux()
|
|
{
|
|
var servers = new List<string>();
|
|
|
|
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;
|
|
}
|
|
|
|
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? GetDomainFromWindows()
|
|
{
|
|
try
|
|
{
|
|
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
|
|
{
|
|
if (ni.OperationalStatus != OperationalStatus.Up)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var props = ni.GetIPProperties();
|
|
if (props is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var suffix = props.DnsSuffix;
|
|
if (!string.IsNullOrWhiteSpace(suffix))
|
|
{
|
|
return suffix.TrimEnd('.');
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
#pragma warning disable CA1416 // Registry access is guarded by platform check
|
|
try
|
|
{
|
|
using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
|
|
@"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters");
|
|
if (key is not null)
|
|
{
|
|
var domain = key.GetValue("Domain") as string;
|
|
if (!string.IsNullOrWhiteSpace(domain))
|
|
{
|
|
return domain.TrimEnd('.');
|
|
}
|
|
|
|
var dhcpDomain = key.GetValue("DhcpDomain") as string;
|
|
if (!string.IsNullOrWhiteSpace(dhcpDomain))
|
|
{
|
|
return dhcpDomain.TrimEnd('.');
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
#pragma warning restore CA1416
|
|
|
|
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 = 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<string?> 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<string?> 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) });
|
|
}
|
|
}
|