diff --git a/ADBulkTool.csproj b/ADBulkTool.csproj
index bd04cb1..3420104 100644
--- a/ADBulkTool.csproj
+++ b/ADBulkTool.csproj
@@ -13,5 +13,6 @@
+
diff --git a/MainWindow.axaml b/MainWindow.axaml
index 1970d13..1578f66 100644
--- a/MainWindow.axaml
+++ b/MainWindow.axaml
@@ -18,7 +18,10 @@
-
+
+
+
+
diff --git a/MainWindow.axaml.cs b/MainWindow.axaml.cs
index 2d9a513..e4cb0fa 100644
--- a/MainWindow.axaml.cs
+++ b/MainWindow.axaml.cs
@@ -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)
diff --git a/Models/DiscoveryResult.cs b/Models/DiscoveryResult.cs
new file mode 100644
index 0000000..17b88e8
--- /dev/null
+++ b/Models/DiscoveryResult.cs
@@ -0,0 +1,9 @@
+namespace ADBulkTool.Models;
+
+public sealed record DiscoveryResult(
+ string? Domain,
+ string? DnsServer,
+ string? LdapHost,
+ int? LdapPort,
+ string? SearchBaseDn,
+ string? Message);
diff --git a/Services/DomainDiscovery.cs b/Services/DomainDiscovery.cs
new file mode 100644
index 0000000..16f611d
--- /dev/null
+++ b/Services/DomainDiscovery.cs
@@ -0,0 +1,253 @@
+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) });
+ }
+}