#![allow(clippy::missing_errors_doc)]
use windows::core::GUID;
use crate::endpoint::{self, Endpoint};
use crate::error::{HnsError, HnsResult};
use crate::namespace::Namespace;
use crate::schema::{Dns, EndpointPolicy, HostComputeEndpoint, IpConfig, Route, SchemaVersion};
#[derive(Debug)]
pub struct EndpointAttachment {
endpoint_id: GUID,
namespace_id: GUID,
ip: Option<String>,
}
impl EndpointAttachment {
#[allow(clippy::too_many_arguments)]
pub fn create_overlay(
network_id: GUID,
owner_tag: &str,
container_id: &str,
ip: std::net::IpAddr,
prefix_length: u8,
cluster_cidr: &str,
dns_server: Option<std::net::IpAddr>,
dns_domain: Option<&str>,
) -> HnsResult<Self> {
let endpoint_id = GUID::new().map_err(|e| HnsError::Other {
hresult: e.code().0,
message: format!("GUID::new(endpoint): {e}"),
})?;
let namespace = Namespace::create_host_default()?;
let namespace_id = namespace.id();
let ip_str = ip.to_string();
let gateway = gateway_for_slice(ip, prefix_length);
let dns = build_endpoint_dns(dns_server, dns_domain);
let settings = HostComputeEndpoint {
name: format!("{owner_tag}-{container_id}"),
host_compute_network: format!("{network_id:?}"),
schema_version: SchemaVersion::default(),
ip_configurations: vec![IpConfig {
ip_address: ip_str,
prefix_length,
}],
policies: vec![
EndpointPolicy::out_bound_nat(vec![cluster_cidr.to_string()]).into(),
EndpointPolicy::sdn_route(cluster_cidr, false).into(),
EndpointPolicy::acl_in_allow(cluster_cidr).into(),
],
routes: vec![Route {
next_hop: gateway.clone(),
destination_prefix: "0.0.0.0/0".to_string(),
metric: None,
}],
dns,
..HostComputeEndpoint::default()
};
let cleanup_namespace = |ns_id: GUID| {
if let Err(e) = Namespace::delete(ns_id) {
tracing::warn!(
ns = %format!("{ns_id:?}"),
error = %e,
"cleanup: failed to delete namespace after partial overlay attach",
);
}
};
let _endpoint = match Endpoint::create(network_id, endpoint_id, &settings) {
Ok(e) => e,
Err(err) => {
cleanup_namespace(namespace_id);
return Err(err);
}
};
if let Err(err) = namespace.add_endpoint(endpoint_id) {
if let Err(e2) = Endpoint::delete(endpoint_id) {
tracing::warn!(
ep = %format!("{endpoint_id:?}"),
error = %e2,
"cleanup: failed to delete overlay endpoint after namespace-attach failure",
);
}
cleanup_namespace(namespace_id);
return Err(err);
}
Ok(Self {
endpoint_id,
namespace_id,
ip: Some(ip.to_string()),
})
}
pub fn teardown(self) -> HnsResult<()> {
let ep_res = Endpoint::delete(self.endpoint_id);
if let Err(ref e) = ep_res {
tracing::warn!(
ep = %format!("{:?}", self.endpoint_id),
error = %e,
"teardown: HcnDeleteEndpoint failed",
);
}
let ns_res = Namespace::delete(self.namespace_id);
if let Err(ref e) = ns_res {
tracing::warn!(
ns = %format!("{:?}", self.namespace_id),
error = %e,
"teardown: HcnDeleteNamespace failed",
);
}
ep_res.or(ns_res)
}
#[must_use]
pub fn endpoint_id(&self) -> GUID {
self.endpoint_id
}
#[must_use]
pub fn namespace_id(&self) -> GUID {
self.namespace_id
}
#[must_use]
pub fn ip(&self) -> Option<&str> {
self.ip.as_deref()
}
}
fn build_endpoint_dns(
dns_server: Option<std::net::IpAddr>,
dns_domain: Option<&str>,
) -> Option<Dns> {
if dns_server.is_none() && dns_domain.is_none() {
return None;
}
let server_list = dns_server
.map(|ip| vec![ip.to_string()])
.unwrap_or_default();
let (domain, search) = match dns_domain {
Some(d) => (d.to_string(), vec![d.to_string()]),
None => (String::new(), Vec::new()),
};
Some(Dns {
domain,
search,
server_list,
options: Vec::new(),
})
}
fn gateway_for_slice(ip: std::net::IpAddr, prefix_length: u8) -> String {
match ip {
std::net::IpAddr::V4(v4) => {
let bits = u32::from(v4);
let mask = if prefix_length == 0 {
0u32
} else {
!0u32 << (32 - u32::from(prefix_length))
};
let network = bits & mask;
let gateway = network.wrapping_add(1);
std::net::Ipv4Addr::from(gateway).to_string()
}
std::net::IpAddr::V6(v6) => {
let bits = u128::from(v6);
let mask = if prefix_length == 0 {
0u128
} else {
!0u128 << (128 - u32::from(prefix_length))
};
let network = bits & mask;
let gateway = network.wrapping_add(1);
std::net::Ipv6Addr::from(gateway).to_string()
}
}
}
pub fn list_owned_endpoints(owner_tag: &str) -> HnsResult<Vec<(GUID, String)>> {
let guids = endpoint::list("{}")?;
let mut found = Vec::new();
for g in guids {
match Endpoint::open(g) {
Ok(ep) => match ep.query_properties("{}") {
Ok(props) => {
if props.name.starts_with(owner_tag) {
found.push((g, props.name));
}
}
Err(e) => {
tracing::warn!(
ep = %format!("{g:?}"),
error = %e,
"reconcile: query_properties failed; skipping",
);
}
},
Err(e) => {
tracing::warn!(
ep = %format!("{g:?}"),
error = %e,
"reconcile: open failed; skipping",
);
}
}
}
Ok(found)
}
pub fn delete_endpoint_and_namespace(endpoint_id: GUID, namespace_id: GUID) -> HnsResult<()> {
let ep_res = Endpoint::delete(endpoint_id);
if let Err(ref e) = ep_res {
tracing::warn!(
ep = %format!("{endpoint_id:?}"),
error = %e,
"reap: HcnDeleteEndpoint failed",
);
}
let ns_res = Namespace::delete(namespace_id);
if let Err(ref e) = ns_res {
tracing::warn!(
ns = %format!("{namespace_id:?}"),
error = %e,
"reap: HcnDeleteNamespace failed",
);
}
ep_res.or(ns_res)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn owner_tag_name_format_is_prefix_matchable() {
let owner = "zlayer";
let cid = "abc123";
let name = format!("{owner}-{cid}");
assert!(name.starts_with(owner), "name must start with owner_tag");
assert_eq!(name, "zlayer-abc123");
}
#[test]
fn endpoint_attachment_getters_return_stored_values() {
let ep = GUID::from_u128(0x1111_2222_3333_4444_5555_6666_7777_8888);
let ns = GUID::from_u128(0xaaaa_bbbb_cccc_dddd_eeee_ffff_0000_1111);
let att = EndpointAttachment {
endpoint_id: ep,
namespace_id: ns,
ip: Some("10.0.0.42".to_string()),
};
assert_eq!(att.endpoint_id(), ep);
assert_eq!(att.namespace_id(), ns);
assert_eq!(att.ip(), Some("10.0.0.42"));
}
#[test]
fn endpoint_attachment_ip_none_is_returned_as_none() {
let att = EndpointAttachment {
endpoint_id: GUID::zeroed(),
namespace_id: GUID::zeroed(),
ip: None,
};
assert!(att.ip().is_none());
}
#[test]
fn gateway_for_slice_v4_28() {
let ip: std::net::IpAddr = "10.200.42.5".parse().unwrap();
assert_eq!(super::gateway_for_slice(ip, 28), "10.200.42.1");
}
#[test]
fn gateway_for_slice_v4_24() {
let ip: std::net::IpAddr = "10.200.7.42".parse().unwrap();
assert_eq!(super::gateway_for_slice(ip, 24), "10.200.7.1");
}
#[test]
fn gateway_for_slice_v4_16() {
let ip: std::net::IpAddr = "10.200.42.5".parse().unwrap();
assert_eq!(super::gateway_for_slice(ip, 16), "10.200.0.1");
}
#[test]
fn gateway_for_slice_v4_endpoint_is_first_host() {
let ip: std::net::IpAddr = "10.200.42.0".parse().unwrap();
assert_eq!(super::gateway_for_slice(ip, 28), "10.200.42.1");
let ip: std::net::IpAddr = "10.200.42.15".parse().unwrap();
assert_eq!(super::gateway_for_slice(ip, 28), "10.200.42.1");
}
#[test]
fn gateway_for_slice_v6_64() {
let ip: std::net::IpAddr = "fd00:200:42::5".parse().unwrap();
assert_eq!(super::gateway_for_slice(ip, 64), "fd00:200:42::1");
}
}