use std::net::SocketAddr;
use super::*;
pub fn check_udp_port(s: &str) -> Result<SocketAddr> {
let s = s.trim();
let a: SocketAddr = s.parse().map_err(|_| {
Error::invalid(format!(
"--allow-udp {s:?}: expected IP:PORT, e.g. 203.0.113.7:10000"
))
})?;
if a.ip().is_unspecified() || a.ip().is_loopback() {
return Err(Error::invalid(format!(
"--allow-udp {s:?}: name the host address the port is published on (not a wildcard or loopback)"
)));
}
if a.port() == 0 {
return Err(Error::invalid(format!("--allow-udp {s:?}: port 0")));
}
Ok(a)
}
pub(super) fn parse_list(s: &str) -> Vec<SocketAddr> {
s.split_whitespace()
.filter_map(|e| check_udp_port(e).ok())
.collect()
}
pub(super) fn render(list: &[SocketAddr]) -> String {
let mut v: Vec<SocketAddr> = list.to_vec();
v.sort();
v.dedup();
v.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ")
}
pub fn allowed_udp(base: &Client, org: &OrgId) -> Result<Vec<SocketAddr>> {
let p = host(base).get_opt(&format!(
"/1.0/projects/{}",
encode_segment(&org.incus_project())
))?;
Ok(p.as_ref()
.and_then(|p| p["config"][KEY_UDP].as_str())
.map(parse_list)
.unwrap_or_default())
}
pub fn check_proxies<'a>(
name: &str,
org: Option<&OrgId>,
devices: impl IntoIterator<Item = (&'a String, &'a BTreeMap<String, String>)>,
stack_udp: bool,
) -> Result<()> {
let Some(org) = org else {
return Ok(());
};
for (d, p) in devices {
if p.get("type").map(String::as_str) != Some("proxy") {
continue;
}
let get = |k: &str| p.get(k).map(String::as_str).unwrap_or_default();
let udp_nat = get("bind") == "host"
&& get("nat") == "true"
&& get("listen").starts_with("udp:")
&& get("connect").starts_with("udp:0.0.0.0:");
if !(stack_udp && udp_nat) {
return Err(Error::invalid(format!(
"{name}: proxy device {d:?} is refused in org {org}: an org publishes host ports through its stacks (docs/concepts/orgs.md#udp-ports)"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entries_name_a_host_address() {
assert_eq!(
check_udp_port(" 203.0.113.7:10000 ").unwrap().to_string(),
"203.0.113.7:10000"
);
assert_eq!(
check_udp_port("[2001:db8::7]:59000").unwrap().to_string(),
"[2001:db8::7]:59000"
);
for bad in [
"0.0.0.0:10000",
"[::]:10000",
"127.0.0.1:10000",
"203.0.113.7:0",
"203.0.113.7",
"10000",
"example.com:10000",
] {
assert!(check_udp_port(bad).is_err(), "{bad}");
}
}
#[test]
fn only_a_stacks_udp_nat_proxy_is_admitted_in_an_org() {
let acme = OrgId::new("acme").unwrap();
let dev = |kv: &[(&str, &str)]| -> BTreeMap<String, String> {
kv.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
};
let udp = dev(&[
("type", "proxy"),
("bind", "host"),
("nat", "true"),
("listen", "udp:203.0.113.7:10000"),
("connect", "udp:0.0.0.0:10000"),
]);
let name = "port-host-udp-10000".to_string();
check_proxies("x", Some(&acme), [(&name, &udp)], true).unwrap();
assert!(check_proxies("x", Some(&acme), [(&name, &udp)], false).is_err());
let mut guest = udp.clone();
guest.insert("bind".into(), "guest".into());
guest.insert("connect".into(), "unix:/var/lib/incus/unix.socket".into());
let mut tcp = udp.clone();
tcp.insert("listen".into(), "tcp:203.0.113.7:10000".into());
let mut no_nat = udp.clone();
no_nat.remove("nat");
for bad in [guest, tcp, no_nat] {
assert!(check_proxies("x", Some(&acme), [(&name, &bad)], true).is_err());
}
let disk = dev(&[("type", "disk"), ("path", "/")]);
check_proxies("x", Some(&acme), [(&name, &disk)], false).unwrap();
check_proxies("x", None, [(&name, &udp)], false).unwrap();
}
#[test]
fn stored_list_round_trips_sorted() {
let l = parse_list("203.0.113.7:59000 203.0.113.7:10000 junk 203.0.113.7:10000");
assert_eq!(render(&l), "203.0.113.7:10000 203.0.113.7:59000");
assert_eq!(parse_list(""), Vec::<SocketAddr>::new());
}
}