use std::fmt;
use std::net::{Ipv4Addr, SocketAddr};
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolved {
Bound(SocketAddr),
NoAddress {
interface: String,
why: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindSpec {
Addr(SocketAddr),
Interface {
interface: String,
port: u16,
},
Role {
role: BindRole,
port: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindRole {
Utility,
}
impl BindRole {
pub fn word(self) -> &'static str {
match self {
BindRole::Utility => "utility",
}
}
}
pub fn utility_interface(
declared: &[String],
exists: impl Fn(&str) -> bool,
) -> Result<String, String> {
if declared.is_empty() {
return Err(
"`utility:<port>` names the box's utility NIC, and GUNNAR_NET declares no \
interface to find it among (e.g. GUNNAR_NET=eth0:dhcp,eth1:dhcp). gunnar will NOT \
fall back to 0.0.0.0."
.to_string(),
);
}
declared
.iter()
.rev()
.find(|i| exists(i))
.cloned()
.ok_or_else(|| {
format!(
"`utility:<port>` names the box's utility NIC, and none of the interfaces GUNNAR_NET \
declares ({}) is on this box. gunnar will NOT fall back to 0.0.0.0.",
declared.join(", ")
)
})
}
impl BindSpec {
#[must_use]
pub fn is_loopback_addr(&self) -> bool {
match self {
BindSpec::Addr(a) => a.ip().is_loopback(),
BindSpec::Interface { .. } | BindSpec::Role { .. } => false,
}
}
#[must_use]
pub fn port(&self) -> u16 {
match self {
BindSpec::Addr(a) => a.port(),
BindSpec::Interface { port, .. } | BindSpec::Role { port, .. } => *port,
}
}
pub fn for_this_box(
self,
declared: &[String],
exists: impl Fn(&str) -> bool,
) -> Result<BindSpec, String> {
match self {
BindSpec::Role {
role: BindRole::Utility,
port,
} => Ok(BindSpec::Interface {
interface: utility_interface(declared, exists)?,
port,
}),
other => Ok(other),
}
}
pub fn resolve_with<E: fmt::Display>(
&self,
lookup: impl FnOnce(&str) -> Result<Option<Ipv4Addr>, E>,
) -> Result<Resolved, String> {
let (interface, port) = match self {
BindSpec::Addr(addr) => return Ok(Resolved::Bound(*addr)),
BindSpec::Role { role, port } => {
return Err(format!(
"--grpc-bind {}:{port} names a role, which becomes this box's NIC first \
(`BindSpec::for_this_box`); it is never bound as it is, and gunnar will NOT \
fall back to 0.0.0.0.",
role.word()
))
}
BindSpec::Interface { interface, port } => (interface, port),
};
match lookup(interface) {
Ok(Some(ip)) => Ok(Resolved::Bound(SocketAddr::from((ip, *port)))),
Ok(None) => Ok(Resolved::NoAddress {
interface: interface.clone(),
why: format!(
"--grpc-bind {interface}:{port} names interface {interface:?}, which exists \
but holds NO IPv4 address, so there is nothing to bind on it. On an \
appliance that means the DHCP handshake on {interface} did not complete — \
check that GUNNAR_NET names it (e.g. \
GUNNAR_NET=eth0:dhcp,{interface}:dhcp) and that the network answered. \
gunnar will NOT fall back to 0.0.0.0: an unauthenticated control plane on \
every interface is the defect this spelling exists to close."
),
}),
Err(err) => Err(format!(
"--grpc-bind {interface}:{port} names interface {interface:?}, and this host \
could not be asked about it: {err}. A medium that names a NIC this machine \
does not have is a declaration that lies, and gunnar stops rather than \
falling back to 0.0.0.0."
)),
}
}
}
impl From<SocketAddr> for BindSpec {
fn from(addr: SocketAddr) -> Self {
BindSpec::Addr(addr)
}
}
impl fmt::Display for BindSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BindSpec::Addr(a) => write!(f, "{a}"),
BindSpec::Interface { interface, port } => write!(f, "{interface}:{port}"),
BindSpec::Role { role, port } => write!(f, "{}:{port}", role.word()),
}
}
}
fn is_interface_name(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 15
&& s != "."
&& s != ".."
&& !s
.bytes()
.any(|b| b == b'/' || b == b':' || b.is_ascii_whitespace() || b == 0)
}
impl FromStr for BindSpec {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(addr) = s.parse::<SocketAddr>() {
return Ok(BindSpec::Addr(addr));
}
let (host, port) = s.rsplit_once(':').ok_or_else(|| {
format!("{s:?} is not <addr>:<port> or <interface>:<port> — there is no `:` in it")
})?;
let port: u16 = port
.parse()
.map_err(|_| format!("{s:?}: {port:?} is not a port number"))?;
let address_shaped = host.starts_with('[')
|| (!host.is_empty() && host.bytes().all(|b| b.is_ascii_digit() || b == b'.'));
if address_shaped {
return Err(format!("{s:?}: {host:?} is not a valid IP address"));
}
if !is_interface_name(host) {
return Err(format!(
"{s:?}: {host:?} is neither an IP address nor a legal interface name (1..=15 \
bytes, no `/`, no `:`, no whitespace)"
));
}
if host == BindRole::Utility.word() {
return Ok(BindSpec::Role {
role: BindRole::Utility,
port,
});
}
Ok(BindSpec::Interface {
interface: host.to_owned(),
port,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_address_spelling_still_parses_to_an_address() {
for (text, want) in [
("127.0.0.1:50051", SocketAddr::from(([127, 0, 0, 1], 50051))),
("0.0.0.0:50051", SocketAddr::from(([0, 0, 0, 0], 50051))),
(
"203.0.113.152:50051",
SocketAddr::from(([203, 0, 113, 152], 50051)),
),
("[::1]:50051", "[::1]:50051".parse().unwrap()),
("[::]:50051", "[::]:50051".parse().unwrap()),
] {
assert_eq!(
text.parse::<BindSpec>().expect(text),
BindSpec::Addr(want),
"{text} must still parse as an ADDRESS"
);
assert_eq!(text.parse::<BindSpec>().unwrap().to_string(), text);
}
}
#[test]
fn an_interface_name_parses_as_one_and_round_trips() {
assert_eq!(
"eth1:50051".parse::<BindSpec>().expect("eth1:50051"),
BindSpec::Interface {
interface: "eth1".into(),
port: 50051
}
);
assert_eq!(
"eth1:50051".parse::<BindSpec>().unwrap().to_string(),
"eth1:50051"
);
}
#[test]
fn malformed_specs_are_refused_rather_than_guessed() {
for bad in [
"eth1", "eth1:", "eth1:notaport", "eth1:70000", "203.0.113.999:50051", "[::1:50051", "eth 1:50051", "a/b:50051", "abcdefghijklmnop:50051", ":50051", ] {
assert!(
bad.parse::<BindSpec>().is_err(),
"{bad:?} was ACCEPTED by BindSpec::from_str"
);
}
}
#[test]
fn an_address_resolves_to_itself_and_never_asks_the_kernel() {
let spec: BindSpec = "203.0.113.152:50051".parse().unwrap();
let got = spec
.resolve_with(|_| -> Result<Option<Ipv4Addr>, String> {
panic!("an address must not be looked up")
})
.expect("an address resolves");
assert_eq!(
got,
Resolved::Bound(SocketAddr::from(([203, 0, 113, 152], 50051)))
);
}
#[test]
fn an_interface_resolves_to_the_address_it_holds() {
let spec: BindSpec = "eth1:50051".parse().unwrap();
let got = spec
.resolve_with(|name| -> Result<Option<Ipv4Addr>, String> {
assert_eq!(name, "eth1");
Ok(Some(Ipv4Addr::new(203, 0, 113, 152)))
})
.expect("eth1 holds an address");
assert_eq!(
got,
Resolved::Bound(SocketAddr::from(([203, 0, 113, 152], 50051)))
);
}
#[test]
fn an_interface_with_no_address_yields_no_address_and_never_widens() {
let spec: BindSpec = "eth1:50051".parse().unwrap();
let got = spec
.resolve_with(|_| -> Result<Option<Ipv4Addr>, String> { Ok(None) })
.expect("an interface with no address is not a lying declaration");
let Resolved::NoAddress { interface, why } = got else {
panic!("an interface with no address must NOT resolve to a bind: {got:?}");
};
assert_eq!(interface, "eth1");
assert!(
why.contains("eth1"),
"the line must name the interface: {why}"
);
assert!(
why.contains("GUNNAR_NET"),
"the line must say how to fix it: {why}"
);
assert!(
why.contains("0.0.0.0"),
"the line must say out loud that it does NOT fall back: {why}"
);
}
#[test]
fn a_kernel_that_cannot_be_asked_is_an_error_and_not_merely_addressless() {
let spec: BindSpec = "eth1:50051".parse().unwrap();
let err = spec
.resolve_with(|_| -> Result<Option<Ipv4Addr>, String> {
Err("no interface named \"eth1\"".into())
})
.expect_err("a NIC that does not exist must stop the boot");
assert!(
err.contains("eth1"),
"the refusal must carry the cause: {err}"
);
assert!(
err.contains("0.0.0.0"),
"and must say it is not widening instead: {err}"
);
}
#[test]
fn loopback_is_only_claimed_for_an_address_that_is_one() {
assert!("127.0.0.1:50051"
.parse::<BindSpec>()
.unwrap()
.is_loopback_addr());
assert!("[::1]:50051"
.parse::<BindSpec>()
.unwrap()
.is_loopback_addr());
assert!(!"0.0.0.0:50051"
.parse::<BindSpec>()
.unwrap()
.is_loopback_addr());
assert!(
!"eth1:50051".parse::<BindSpec>().unwrap().is_loopback_addr(),
"an unresolved interface must not be claimed as loopback — its address is not known"
);
}
#[test]
fn the_utility_role_is_the_last_declared_nic_this_box_has() {
let spec: BindSpec = "utility:50051".parse().expect("the role spelling");
assert_eq!(
spec,
BindSpec::Role {
role: BindRole::Utility,
port: 50051
}
);
assert_eq!(spec.to_string(), "utility:50051");
assert_eq!(spec.port(), 50051);
assert!(
!spec.is_loopback_addr(),
"a role's address is not known yet"
);
let declared = ["eth0".to_string(), "eth1".to_string()];
let eth = |i: &str| BindSpec::Interface {
interface: i.into(),
port: 50051,
};
assert_eq!(
spec.clone().for_this_box(&declared, |_| true).unwrap(),
eth("eth1"),
"two NICs: the utility one is eth1"
);
assert_eq!(
spec.clone()
.for_this_box(&declared, |i| i == "eth0")
.unwrap(),
eth("eth0"),
"a lean box: its one NIC is the utility one"
);
assert_eq!(
spec.clone()
.for_this_box(&["eth0".to_string()], |_| true)
.unwrap(),
eth("eth0"),
"a one-NIC spec"
);
let none = spec.clone().for_this_box(&[], |_| true).unwrap_err();
assert!(
none.contains("GUNNAR_NET") && none.contains("0.0.0.0"),
"{none}"
);
let absent = spec.clone().for_this_box(&declared, |_| false).unwrap_err();
assert!(
absent.contains("eth0") && absent.contains("eth1"),
"{absent}"
);
for kept in ["eth1:50051", "127.0.0.1:50051", "10.6.0.2:50051"] {
let k: BindSpec = kept.parse().unwrap();
assert_eq!(
k.clone().for_this_box(&declared, |_| false).unwrap(),
k,
"{kept}: not a role, unchanged"
);
}
let unresolved = spec
.resolve_with(|_| -> Result<Option<Ipv4Addr>, String> { Ok(Some(Ipv4Addr::LOCALHOST)) })
.unwrap_err();
assert!(
unresolved.contains("utility") && unresolved.contains("0.0.0.0"),
"a role is never bound as it is: {unresolved}"
);
}
}