MissingHostname,
NumericHostname,
InvalidPart(String),
+ TooLong(usize),
}
impl fmt::Display for FqdnParseError {
f,
"FQDN must only consist of alphanumeric characters and dashes. Invalid part: '{part}'",
),
+ TooLong(len) => write!(f, "FQDN too long: {len} > {}", Fqdn::MAX_LENGTH),
}
}
}
+/// A type for safely representing fully-qualified domain names (FQDNs).
+///
+/// It considers following RFCs:
+/// https://www.ietf.org/rfc/rfc952.txt (sec. "ASSUMPTIONS", 1.)
+/// https://www.ietf.org/rfc/rfc1035.txt (sec. 2.3. "Conventions")
+/// https://www.ietf.org/rfc/rfc1123.txt (sec. 2.1. "Host Names and Numbers")
+/// https://www.ietf.org/rfc/rfc3492.txt
+/// https://www.ietf.org/rfc/rfc4343.txt
+///
+/// .. and applies some restriction given by Debian, e.g. 253 instead of 255
+/// maximum total length and maximum 63 characters per label.
+/// https://manpages.debian.org/stable/manpages/hostname.7.en.html
+///
+/// Additionally:
+/// - It enforces the restriction as per Bugzilla #1054, in that
+/// purely numeric hostnames are not allowed - against RFC1123 sec. 2.1.
+///
+/// Some terminology:
+/// - "label" - a single part of a FQDN, e.g. <label>.<label>.<tld>
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Fqdn {
parts: Vec<String>,
}
impl Fqdn {
+ /// Maximum length of a single label of the FQDN
+ const MAX_LABEL_LENGTH: usize = 63;
+ /// Maximum total length of the FQDN
+ const MAX_LENGTH: usize = 253;
+
pub fn from(fqdn: &str) -> Result<Self, FqdnParseError> {
+ if fqdn.len() > Self::MAX_LENGTH {
+ return Err(FqdnParseError::TooLong(fqdn.len()));
+ }
+
let parts = fqdn
.split('.')
.map(ToOwned::to_owned)
if parts.len() < 2 {
Err(FqdnParseError::MissingHostname)
} else if parts[0].chars().all(|c| c.is_ascii_digit()) {
- // Not allowed/supported on Debian systems.
+ // Do not allow a purely numeric hostname, see:
+ // https://bugzilla.proxmox.com/show_bug.cgi?id=1054
Err(FqdnParseError::NumericHostname)
} else {
Ok(Self { parts })
fn validate_single(s: &String) -> bool {
!s.is_empty()
+ && s.len() <= Self::MAX_LABEL_LENGTH
// First character must be alphanumeric
&& s.chars()
.next()
assert_eq!(Fqdn::from("foo.com-"), Err(InvalidPart("com-".to_owned())));
assert_eq!(Fqdn::from("-o-.com"), Err(InvalidPart("-o-".to_owned())));
+ // https://bugzilla.proxmox.com/show_bug.cgi?id=1054
assert_eq!(Fqdn::from("123.com"), Err(NumericHostname));
assert!(Fqdn::from("foo123.com").is_ok());
assert!(Fqdn::from("123foo.com").is_ok());
+
+ assert!(Fqdn::from(&format!("{}.com", "a".repeat(63))).is_ok());
+ assert_eq!(
+ Fqdn::from(&format!("{}.com", "a".repeat(250))),
+ Err(TooLong(254)),
+ );
+ assert_eq!(
+ Fqdn::from(&format!("{}.com", "a".repeat(64))),
+ Err(InvalidPart("a".repeat(64))),
+ );
}
#[test]