use std::fmt;
use std::num::NonZeroU16;
use std::sync::Arc;
use std::time::Duration;
use crate::ClientError;
use crate::ClientResult;
use crate::config_support::name_server_target::parse_legacy_namesrv_addr;
const DEFAULT_MIN_REFRESH: Duration = Duration::from_secs(5);
const DEFAULT_MAX_REFRESH: Duration = Duration::from_secs(60);
const DEFAULT_STALE_MAX: Duration = Duration::from_secs(5 * 60);
const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_ENDPOINT_LIMIT: usize = 64;
const MAX_ENDPOINT_LIMIT: usize = 64;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DnsName(Arc<str>);
impl DnsName {
pub fn parse(value: impl AsRef<str>) -> ClientResult<Self> {
let normalized = value.as_ref().trim().trim_end_matches('.').to_ascii_lowercase();
validate_dns_name(&normalized)?;
Ok(Self(Arc::from(normalized)))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for DnsName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct NameServerAuthority(Arc<str>);
impl NameServerAuthority {
pub fn parse(value: impl AsRef<str>) -> ClientResult<Self> {
let parsed = parse_legacy_namesrv_addr(value.as_ref())?;
let addresses = parsed.into_addresses();
if addresses.len() != 1 {
return Err(invalid_config(
"nameserver_discovery.static",
value.as_ref(),
"must contain exactly one host:port authority",
));
}
Ok(Self(Arc::from(addresses[0].as_str())))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for NameServerAuthority {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum NameServerSource {
Static(Vec<NameServerAuthority>),
Dns { host: DnsName, port: NonZeroU16 },
}
impl NameServerSource {
pub fn dns(host: impl AsRef<str>, port: u16) -> ClientResult<Self> {
let port = NonZeroU16::new(port)
.ok_or_else(|| invalid_config("nameserver_discovery.port", port, "must be between 1 and 65535"))?;
Ok(Self::Dns {
host: DnsName::parse(host)?,
port,
})
}
pub fn static_endpoints(endpoints: Vec<NameServerAuthority>) -> ClientResult<Self> {
if endpoints.is_empty() {
return Err(invalid_config(
"nameserver_discovery.static",
"[]",
"must contain at least one endpoint",
));
}
if endpoints.len() > MAX_ENDPOINT_LIMIT {
return Err(invalid_config(
"nameserver_discovery.static",
endpoints.len(),
"must not contain more than 64 endpoints",
));
}
Ok(Self::Static(endpoints))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NameServerDiscoveryConfig {
source: NameServerSource,
min_refresh: Duration,
max_refresh: Duration,
stale_max: Duration,
drain_timeout: Duration,
endpoint_limit: usize,
}
impl NameServerDiscoveryConfig {
#[must_use]
pub fn new(source: NameServerSource) -> Self {
Self {
source,
min_refresh: DEFAULT_MIN_REFRESH,
max_refresh: DEFAULT_MAX_REFRESH,
stale_max: DEFAULT_STALE_MAX,
drain_timeout: DEFAULT_DRAIN_TIMEOUT,
endpoint_limit: DEFAULT_ENDPOINT_LIMIT,
}
}
pub fn with_refresh_bounds(mut self, min: Duration, max: Duration) -> ClientResult<Self> {
if min.is_zero() || max < min {
return Err(invalid_config(
"nameserver_discovery.refresh",
format!("{min:?}..={max:?}"),
"minimum must be non-zero and no greater than maximum",
));
}
self.min_refresh = min;
self.max_refresh = max;
Ok(self)
}
pub fn with_stale_max(mut self, stale_max: Duration) -> ClientResult<Self> {
if stale_max.is_zero() {
return Err(invalid_config(
"nameserver_discovery.stale_max",
"0",
"must be non-zero",
));
}
self.stale_max = stale_max;
Ok(self)
}
pub fn with_drain_timeout(mut self, drain_timeout: Duration) -> ClientResult<Self> {
if drain_timeout.is_zero() {
return Err(invalid_config(
"nameserver_discovery.drain_timeout",
"0",
"must be non-zero",
));
}
self.drain_timeout = drain_timeout;
Ok(self)
}
pub fn with_endpoint_limit(mut self, endpoint_limit: usize) -> ClientResult<Self> {
if !(1..=MAX_ENDPOINT_LIMIT).contains(&endpoint_limit) {
return Err(invalid_config(
"nameserver_discovery.endpoint_limit",
endpoint_limit,
"must be between 1 and 64",
));
}
self.endpoint_limit = endpoint_limit;
Ok(self)
}
#[must_use]
pub fn source(&self) -> &NameServerSource {
&self.source
}
#[must_use]
pub(crate) fn min_refresh(&self) -> Duration {
self.min_refresh
}
#[must_use]
pub(crate) fn max_refresh(&self) -> Duration {
self.max_refresh
}
#[must_use]
pub(crate) fn stale_max(&self) -> Duration {
self.stale_max
}
#[must_use]
pub(crate) fn drain_timeout(&self) -> Duration {
self.drain_timeout
}
#[must_use]
pub(crate) fn endpoint_limit(&self) -> usize {
self.endpoint_limit
}
pub(crate) fn validate(&self) -> ClientResult<()> {
match &self.source {
NameServerSource::Static(endpoints) if endpoints.is_empty() => Err(invalid_config(
"nameserver_discovery.static",
"[]",
"must contain at least one endpoint",
)),
NameServerSource::Static(endpoints) if endpoints.len() > self.endpoint_limit => Err(invalid_config(
"nameserver_discovery.static",
endpoints.len(),
"exceeds the configured endpoint limit",
)),
_ => Ok(()),
}
}
pub(crate) fn fingerprint(&self) -> String {
let source = match &self.source {
NameServerSource::Static(endpoints) => {
let mut endpoints = endpoints.iter().map(NameServerAuthority::as_str).collect::<Vec<_>>();
endpoints.sort_unstable();
endpoints.dedup();
format!("static:{}", endpoints.join(";"))
}
NameServerSource::Dns { host, port } => format!("dns:{}:{}", host.as_str(), port.get()),
};
format!(
"{source}|refresh={}-{}|stale={}|drain={}|limit={}",
self.min_refresh.as_millis(),
self.max_refresh.as_millis(),
self.stale_max.as_millis(),
self.drain_timeout.as_millis(),
self.endpoint_limit
)
}
pub(crate) fn static_canonical(&self) -> Option<String> {
let NameServerSource::Static(endpoints) = &self.source else {
return None;
};
let mut seen = std::collections::HashSet::new();
Some(
endpoints
.iter()
.filter(|endpoint| seen.insert(endpoint.as_str()))
.map(NameServerAuthority::as_str)
.collect::<Vec<_>>()
.join(";"),
)
}
}
fn validate_dns_name(value: &str) -> ClientResult<()> {
if value.is_empty() || value.len() > 253 {
return Err(invalid_config(
"nameserver_discovery.dns_name",
value,
"must contain between 1 and 253 ASCII characters",
));
}
for label in value.split('.') {
if label.is_empty() || label.len() > 63 {
return Err(invalid_config(
"nameserver_discovery.dns_name",
value,
"each DNS label must contain between 1 and 63 characters",
));
}
if !label
.bytes()
.all(|character| character.is_ascii_alphanumeric() || matches!(character, b'-' | b'_'))
|| label.starts_with('-')
|| label.ends_with('-')
{
return Err(invalid_config(
"nameserver_discovery.dns_name",
value,
"contains an invalid DNS label",
));
}
}
Ok(())
}
fn invalid_config(key: &'static str, value: impl ToString, reason: impl Into<String>) -> ClientError {
ClientError::config_invalid(key, value.to_string(), reason.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dns_source_normalizes_name_and_rejects_invalid_port() {
let source = NameServerSource::dns("NameSrv.Default.Svc.", 9876).expect("DNS source");
assert!(matches!(
source,
NameServerSource::Dns { ref host, port }
if host.as_str() == "namesrv.default.svc" && port.get() == 9876
));
assert!(NameServerSource::dns("namesrv.default.svc", 0).is_err());
}
#[test]
fn discovery_fingerprint_is_stable_for_equivalent_static_sets() {
let first = NameServerDiscoveryConfig::new(
NameServerSource::static_endpoints(vec![
NameServerAuthority::parse("ns-b:9876").unwrap(),
NameServerAuthority::parse("ns-a:9876").unwrap(),
])
.unwrap(),
);
let second = NameServerDiscoveryConfig::new(
NameServerSource::static_endpoints(vec![
NameServerAuthority::parse("NS-A:9876").unwrap(),
NameServerAuthority::parse("ns-b:9876").unwrap(),
])
.unwrap(),
);
assert_eq!(first.fingerprint(), second.fingerprint());
}
}