Add DNS-based domain/DC auto-discovery
This commit is contained in:
@@ -13,5 +13,6 @@
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.5" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="4.0.0" />
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+4
-1
@@ -18,7 +18,10 @@
|
||||
<Grid ColumnDefinitions="*,90,130,210" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="10" RowSpacing="8">
|
||||
<StackPanel Grid.Row="0" Grid.Column="0">
|
||||
<TextBlock Text="Domain controller / LDAP host" />
|
||||
<TextBox x:Name="LdapHostBox" PlaceholderText="dc01.contoso.local" />
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
|
||||
<TextBox x:Name="LdapHostBox" PlaceholderText="dc01.contoso.local" />
|
||||
<Button Grid.Column="1" x:Name="DiscoverButton" Content="Discover" Click="Discover_Click" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="1">
|
||||
|
||||
@@ -29,6 +29,74 @@ public partial class MainWindow : Window
|
||||
|
||||
RefreshConnectionHelp();
|
||||
RefreshActionHelp();
|
||||
|
||||
_ = AutoDiscoverAsync();
|
||||
}
|
||||
|
||||
private async void Discover_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
DiscoverButton.IsEnabled = false;
|
||||
SetStatus("Discovering domain and domain controller from DNS...");
|
||||
|
||||
try
|
||||
{
|
||||
var result = await DomainDiscovery.DiscoverAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
if (result.LdapHost is not null)
|
||||
{
|
||||
LdapHostBox.Text = result.LdapHost;
|
||||
}
|
||||
|
||||
if (result.SearchBaseDn is not null)
|
||||
{
|
||||
SearchBaseBox.Text = result.SearchBaseDn;
|
||||
}
|
||||
|
||||
if (result.Domain is not null)
|
||||
{
|
||||
var domainUpper = result.Domain.ToUpperInvariant();
|
||||
var domainParts = result.Domain.Split('.');
|
||||
var netbios = domainParts.Length > 0 ? domainParts[0].ToUpperInvariant() : domainUpper;
|
||||
var userPlaceholder = $@"{netbios}\adminuser or adminuser@{result.Domain}";
|
||||
BindUserBox.PlaceholderText = userPlaceholder;
|
||||
}
|
||||
|
||||
SetStatus(result.Message ?? "Discovery did not return a result.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus($"Discovery failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DiscoverButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AutoDiscoverAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await DomainDiscovery.DiscoverAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
if (result.LdapHost is not null && string.IsNullOrWhiteSpace(LdapHostBox.Text))
|
||||
{
|
||||
LdapHostBox.Text = result.LdapHost;
|
||||
}
|
||||
|
||||
if (result.SearchBaseDn is not null && string.IsNullOrWhiteSpace(SearchBaseBox.Text))
|
||||
{
|
||||
SearchBaseBox.Text = result.SearchBaseDn;
|
||||
}
|
||||
|
||||
if (result.LdapHost is not null || result.SearchBaseDn is not null)
|
||||
{
|
||||
SetStatus("Auto-discovered AD domain from DNS. Adjust if needed, then enter credentials.");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async void LoadCsv_Click(object? sender, RoutedEventArgs e)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ADBulkTool.Models;
|
||||
|
||||
public sealed record DiscoveryResult(
|
||||
string? Domain,
|
||||
string? DnsServer,
|
||||
string? LdapHost,
|
||||
int? LdapPort,
|
||||
string? SearchBaseDn,
|
||||
string? Message);
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user