Add DNS-based domain/DC auto-discovery

This commit is contained in:
mikisoq
2026-07-29 01:56:08 -01:00
parent 3987e453c2
commit b6a57a104e
5 changed files with 335 additions and 1 deletions
+253
View File
@@ -0,0 +1,253 @@
using System.Net;
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 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<string> GetDnsServers()
{
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;
}
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);
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<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) });
}
}