diff --git a/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/Internal/LdapEndpoint.cs b/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/Internal/LdapEndpoint.cs
new file mode 100644
index 0000000..b70a524
--- /dev/null
+++ b/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/Internal/LdapEndpoint.cs
@@ -0,0 +1,60 @@
+namespace ZB.MOM.WW.Auth.Ldap.Internal;
+
+/// A single LDAP server endpoint resolved from configuration.
+internal sealed record LdapEndpoint(string Host, int Port)
+{
+ /// Renders the endpoint as host:port — the form used in failover log messages.
+ public override string ToString() => $"{Host}:{Port}";
+}
+
+///
+/// Parses FallbackServers entries: "host" (adopts the caller's default port)
+/// or "host:port". IPv6 literals are not supported — use a DNS name.
+///
+internal static class LdapEndpointParser
+{
+ ///
+ /// Attempts to parse a single FallbackServers entry.
+ ///
+ /// The configured entry, "host" or "host:port". Surrounding whitespace is tolerated.
+ /// Port adopted when the entry carries no explicit one.
+ /// The parsed endpoint on success; undefined otherwise.
+ /// A human-readable reason on failure; empty on success.
+ /// when the entry parsed.
+ public static bool TryParse(string entry, int defaultPort, out LdapEndpoint endpoint, out string error)
+ {
+ endpoint = null!;
+ error = "";
+
+ if (string.IsNullOrWhiteSpace(entry))
+ {
+ error = "entry is empty or whitespace";
+ return false;
+ }
+
+ var parts = entry.Trim().Split(':');
+ if (parts.Length > 2)
+ {
+ error = $"entry \"{entry.Trim()}\" contains more than one ':' " +
+ "(IPv6 literals are not supported — use a DNS name)";
+ return false;
+ }
+
+ var host = parts[0].Trim();
+ if (host.Length == 0)
+ {
+ error = $"entry \"{entry.Trim()}\" has an empty host";
+ return false;
+ }
+
+ var port = defaultPort;
+ if (parts.Length == 2 && (!int.TryParse(parts[1].Trim(), out port) || port is < 1 or > 65535))
+ {
+ error = $"entry \"{entry.Trim()}\" has an invalid port (must be 1–65535)";
+ return false;
+ }
+
+ endpoint = new LdapEndpoint(host, port);
+ return true;
+ }
+}
diff --git a/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/LdapEndpointParserTests.cs b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/LdapEndpointParserTests.cs
new file mode 100644
index 0000000..67aeedf
--- /dev/null
+++ b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/LdapEndpointParserTests.cs
@@ -0,0 +1,35 @@
+using ZB.MOM.WW.Auth.Ldap.Internal;
+
+namespace ZB.MOM.WW.Auth.Ldap.Tests;
+
+public class LdapEndpointParserTests
+{
+ [Theory]
+ [InlineData("dc2.example.com", 636, "dc2.example.com", 636)] // bare host -> default port
+ [InlineData("dc2.example.com:3894", 636, "dc2.example.com", 3894)] // explicit port wins
+ [InlineData(" dc2.example.com : 3894 ", 636, "dc2.example.com", 3894)] // whitespace tolerated
+ public void TryParse_ValidEntry_Succeeds(string entry, int defaultPort, string host, int port)
+ {
+ Assert.True(LdapEndpointParser.TryParse(entry, defaultPort, out var ep, out _));
+ Assert.Equal(host, ep.Host);
+ Assert.Equal(port, ep.Port);
+ }
+
+ [Theory]
+ [InlineData("")] // empty
+ [InlineData(" ")] // whitespace
+ [InlineData(":636")] // empty host
+ [InlineData("dc2:0")] // port below range
+ [InlineData("dc2:65536")] // port above range
+ [InlineData("dc2:abc")] // non-numeric port
+ [InlineData("2001:db8::1:636")] // IPv6 literal (unsupported, >1 colon)
+ public void TryParse_InvalidEntry_Fails(string entry)
+ {
+ Assert.False(LdapEndpointParser.TryParse(entry, 636, out _, out var error));
+ Assert.NotEmpty(error);
+ }
+
+ [Fact]
+ public void Endpoint_ToString_IsHostColonPort() =>
+ Assert.Equal("dc2:3894", new LdapEndpoint("dc2", 3894).ToString());
+}