Skip to content

Commit

Permalink
add kumo-address crate for HostAddress and SocketAddress types
Browse files Browse the repository at this point in the history
One frustration I have with the Rust standard library types is
that while it has a SocketAddr type, that type doesn't include
unix domain addressing like the underlying unix OS does in the
underlying system type.

I understand why it is that way, it's still a bit of a PITA.

Since we want to enable the use of unix domain sockets for
outbound LMTP support, our internal addressing type needs to
be able to represent a unix domain address.

This commit introduces our own HostAddress (equivalent to IpAddr,
but not limited to IP addresses) and SocketAddress (equivalent to
SockAddr, but not limited to IP sockets) types.

The ResolvedAddress type has been cut over from IpAddr to HostAddress
and the fan out addressed.

This commit does not add support for establishing unix domain
connections, it is merely the ability to recognize/report on them.

refs: #267
  • Loading branch information
wez committed Dec 17, 2024
1 parent 8be4ad8 commit 4b12c4c
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 31 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ members = [
"crates/kumo-dmarc",
"crates/kumo-template",
"crates/summarize-memory",
"crates/kumo-address",
]
resolver = "2"

Expand Down
10 changes: 5 additions & 5 deletions crates/dns-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ pub async fn resolve_a_or_aaaa(domain_name: &str) -> anyhow::Result<Vec<Resolved
Ok(addr) => {
return Ok(vec![ResolvedAddress {
name: domain_name.to_string(),
addr: std::net::IpAddr::V6(addr),
addr: std::net::IpAddr::V6(addr).into(),
}]);
}
Err(err) => {
Expand All @@ -202,7 +202,7 @@ pub async fn resolve_a_or_aaaa(domain_name: &str) -> anyhow::Result<Vec<Resolved
Ok(addr) => {
return Ok(vec![ResolvedAddress {
name: domain_name.to_string(),
addr,
addr: addr.into(),
}]);
}
Err(err) => {
Expand All @@ -217,7 +217,7 @@ pub async fn resolve_a_or_aaaa(domain_name: &str) -> anyhow::Result<Vec<Resolved
.iter()
.map(|&addr| ResolvedAddress {
name: domain_name.to_string(),
addr,
addr: addr.into(),
})
.collect();
Ok(addrs)
Expand Down Expand Up @@ -377,7 +377,7 @@ impl MailExchanger {
if let Ok(addr) = mx_host.parse::<IpAddr>() {
by_pref.push(ResolvedAddress {
name: mx_host.to_string(),
addr,
addr: addr.into(),
});
continue;
}
Expand All @@ -391,7 +391,7 @@ impl MailExchanger {
for addr in addresses.iter() {
by_pref.push(ResolvedAddress {
name: mx_host.to_string(),
addr: *addr,
addr: (*addr).into(),
});
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/kumo-address/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "kumo-address"
version = "0.1.0"
edition = "2021"

[dependencies]
serde.workspace = true
thiserror.workspace = true
218 changes: 218 additions & 0 deletions crates/kumo-address/src/host.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::os::unix::net::SocketAddr as UnixSocketAddr;
use std::path::Path;
use std::str::FromStr;
use thiserror::Error;

#[derive(Error, Debug)]
#[error(
"Failed to parse '{candidate}' as an address. \
Got '{net_err}' when considering it as an IP address and \
'{unix_err}' when considering it as a unix domain socket path."
)]
pub struct AddressParseError {
pub(crate) candidate: String,
pub(crate) net_err: std::net::AddrParseError,
pub(crate) unix_err: std::io::Error,
}

impl PartialEq for AddressParseError {
fn eq(&self, other: &Self) -> bool {
self.to_string().eq(&other.to_string())
}
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(try_from = "String", into = "String")]
pub enum HostAddress {
UnixDomain(Box<UnixSocketAddr>),
V4(std::net::Ipv4Addr),
V6(std::net::Ipv6Addr),
}

impl HostAddress {
/// Returns the unix domain socket representation of the address
pub fn unix(&self) -> Option<UnixSocketAddr> {
match self {
Self::V4(_) | Self::V6(_) => None,
Self::UnixDomain(unix) => Some((**unix).clone()),
}
}

/// Returns the ip representation of the address
pub fn ip(&self) -> Option<IpAddr> {
match self {
Self::V4(a) => Some(a.clone().into()),
Self::V6(a) => Some(a.clone().into()),
Self::UnixDomain(_) => None,
}
}
}

impl PartialEq for HostAddress {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::UnixDomain(a), Self::UnixDomain(b)) => {
match (a.as_pathname(), b.as_pathname()) {
(Some(a), Some(b)) => a.eq(b),
(None, None) => true,
_ => false,
}
}
(Self::V4(a), Self::V4(b)) => a.eq(b),
(Self::V6(a), Self::V6(b)) => a.eq(b),
_ => false,
}
}
}

impl Eq for HostAddress {}

impl From<HostAddress> for String {
fn from(a: HostAddress) -> String {
format!("{a}")
}
}

impl std::fmt::Debug for HostAddress {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
<Self as std::fmt::Display>::fmt(self, fmt)
}
}

impl std::fmt::Display for HostAddress {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::UnixDomain(unix) => match unix.as_pathname() {
Some(path) => path.display().fmt(fmt),
None => write!(fmt, "<unbound unix domain>"),
},
Self::V4(a) => a.fmt(fmt),
Self::V6(a) => a.fmt(fmt),
}
}
}

impl FromStr for HostAddress {
type Err = AddressParseError;
fn from_str(s: &str) -> Result<HostAddress, Self::Err> {
match IpAddr::from_str(s) {
Ok(a) => Ok(a.into()),
Err(net_err) => {
if s.starts_with('[') && s.ends_with(']') {
let alternative = &s[1..s.len() - 1];
if let Ok(a) = IpAddr::from_str(alternative) {
return Ok(a.into());
}
}

let path: &Path = s.as_ref();
if path.is_relative() {
Err(AddressParseError {
candidate: s.to_string(),
net_err,
unix_err: std::io::Error::new(
std::io::ErrorKind::Other,
"unix domain path must be absolute",
),
})
} else {
match UnixSocketAddr::from_pathname(path) {
Ok(unix) => Ok(HostAddress::UnixDomain(unix.into())),
Err(unix_err) => Err(AddressParseError {
candidate: s.to_string(),
net_err,
unix_err,
}),
}
}
}
}
}
}

impl TryFrom<String> for HostAddress {
type Error = AddressParseError;
fn try_from(s: String) -> Result<HostAddress, Self::Error> {
HostAddress::from_str(&s)
}
}

impl From<UnixSocketAddr> for HostAddress {
fn from(unix: UnixSocketAddr) -> HostAddress {
HostAddress::UnixDomain(unix.into())
}
}

impl From<Ipv4Addr> for HostAddress {
fn from(ip: Ipv4Addr) -> HostAddress {
HostAddress::V4(ip)
}
}

impl From<Ipv6Addr> for HostAddress {
fn from(ip: Ipv6Addr) -> HostAddress {
HostAddress::V6(ip)
}
}

impl From<IpAddr> for HostAddress {
fn from(ip: IpAddr) -> HostAddress {
match ip {
IpAddr::V4(a) => HostAddress::V4(a),
IpAddr::V6(a) => HostAddress::V6(a),
}
}
}

impl From<SocketAddr> for HostAddress {
fn from(a: SocketAddr) -> HostAddress {
a.ip().into()
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn parse() {
assert_eq!(
"10.0.0.1".parse::<HostAddress>(),
Ok(HostAddress::V4(Ipv4Addr::new(10, 0, 0, 1)))
);
assert_eq!(
"[10.0.0.1]".parse::<HostAddress>(),
Ok(HostAddress::V4(Ipv4Addr::new(10, 0, 0, 1)))
);
assert_eq!(
"::1".parse::<HostAddress>(),
Ok(HostAddress::V6(Ipv6Addr::LOCALHOST))
);
assert_eq!(
"[::1]".parse::<HostAddress>(),
Ok(HostAddress::V6(Ipv6Addr::LOCALHOST))
);
assert_eq!(
"/some/path".parse::<HostAddress>(),
Ok(HostAddress::UnixDomain(
UnixSocketAddr::from_pathname("/some/path").unwrap().into()
))
);
assert_eq!(
format!("{:#}", "[/some/path]".parse::<HostAddress>().unwrap_err()),
"Failed to parse '[/some/path]' as an address. \
Got 'invalid IP address syntax' when considering it as \
an IP address and 'unix domain path must be absolute' \
when considering it as a unix domain socket path."
);
assert_eq!(
format!("{:#}", "hello there".parse::<HostAddress>().unwrap_err()),
"Failed to parse 'hello there' as an address. \
Got 'invalid IP address syntax' when considering it as \
an IP address and 'unix domain path must be absolute' \
when considering it as a unix domain socket path."
);
}
}
2 changes: 2 additions & 0 deletions crates/kumo-address/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod host;
pub mod socket;
Loading

0 comments on commit 4b12c4c

Please sign in to comment.