#![allow(clippy::missing_errors_doc)]
use core::ffi::c_void;
use windows::core::{GUID, HSTRING, PWSTR};
use windows::Win32::Foundation::{LocalFree, HLOCAL};
use windows::Win32::System::HostComputeNetwork::{
HcnCreateNetwork, HcnDeleteNetwork, HcnEnumerateNetworks, HcnModifyNetwork, HcnOpenNetwork,
HcnQueryNetworkProperties,
};
use crate::error::{HnsError, HnsResult};
use crate::handle::{HcnNetworkHandle, OwnedNetwork};
use crate::schema::{HostComputeNetwork, Ipam, NetworkType, SchemaVersion, Subnet};
#[derive(Debug)]
pub struct Network {
id: GUID,
handle: OwnedNetwork,
}
impl Network {
pub fn create(id: GUID, settings: &HostComputeNetwork) -> HnsResult<Self> {
let settings_json = serde_json::to_string(settings)?;
let settings_hstring = HSTRING::from(settings_json);
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnCreateNetwork(&id, &settings_hstring, &mut raw, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnCreateNetwork"))?;
}
if raw.is_null() {
return Err(HnsError::Other {
hresult: 0,
message: "HcnCreateNetwork returned null handle".to_string(),
});
}
let handle = unsafe { OwnedNetwork::from_raw(raw as HcnNetworkHandle) };
Ok(Self { id, handle })
}
pub fn open(id: GUID) -> HnsResult<Self> {
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnOpenNetwork(&id, &mut raw, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnOpenNetwork({id:?})"))
})?;
}
if raw.is_null() {
return Err(HnsError::NotFound {
id: format!("{id:?}"),
});
}
let handle = unsafe { OwnedNetwork::from_raw(raw as HcnNetworkHandle) };
Ok(Self { id, handle })
}
pub fn delete(id: GUID) -> HnsResult<()> {
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnDeleteNetwork(&id, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnDeleteNetwork({id:?})"))
})?;
}
Ok(())
}
pub fn modify(&self, modification_json: &str) -> HnsResult<()> {
let mod_hstring = HSTRING::from(modification_json);
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnModifyNetwork(self.handle.as_raw(), &mod_hstring, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnModifyNetwork"))?;
}
Ok(())
}
pub fn query(&self, property_query_json: &str) -> HnsResult<HostComputeNetwork> {
let query_hstring = HSTRING::from(property_query_json);
let mut out_properties: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnQueryNetworkProperties(
self.handle.as_raw(),
&query_hstring,
&mut out_properties,
Some(&mut err_record),
)
.map_err(|e| classify_error(e.code(), err_record, "HcnQueryNetworkProperties"))?;
}
let json = decode_pwstr(out_properties);
let parsed: HostComputeNetwork = serde_json::from_str(&json)?;
Ok(parsed)
}
pub fn create_transparent(
id: GUID,
name: &str,
subnet: &str,
uplink_adapter_name: &str,
) -> HnsResult<Self> {
let settings = HostComputeNetwork {
id: None,
name: name.to_string(),
ty: NetworkType::Transparent,
policies: vec![net_adapter_name_policy(uplink_adapter_name)],
mac_pool: None,
dns: None,
ipams: vec![Ipam {
ty: "Static".to_string(),
subnets: vec![Subnet {
ip_address_prefix: subnet.to_string(),
routes: Vec::new(),
policies: Vec::new(),
}],
}],
flags: 0,
schema_version: SchemaVersion::default(),
};
Self::create(id, &settings)
}
#[must_use]
pub fn id(&self) -> GUID {
self.id
}
#[must_use]
pub fn handle(&self) -> &OwnedNetwork {
&self.handle
}
}
fn net_adapter_name_policy(adapter_name: &str) -> serde_json::Value {
serde_json::json!({
"Type": "NetAdapterName",
"Settings": { "NetworkAdapterName": adapter_name }
})
}
#[cfg(test)]
pub(crate) fn transparent_settings(
name: &str,
subnet: &str,
uplink_adapter_name: &str,
) -> HostComputeNetwork {
HostComputeNetwork {
id: None,
name: name.to_string(),
ty: NetworkType::Transparent,
policies: vec![net_adapter_name_policy(uplink_adapter_name)],
mac_pool: None,
dns: None,
ipams: vec![Ipam {
ty: "Static".to_string(),
subnets: vec![Subnet {
ip_address_prefix: subnet.to_string(),
routes: Vec::new(),
policies: Vec::new(),
}],
}],
flags: 0,
schema_version: SchemaVersion::default(),
}
}
pub fn list(query_json: &str) -> HnsResult<Vec<GUID>> {
let query_hstring = HSTRING::from(query_json);
let mut out_networks: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnEnumerateNetworks(&query_hstring, &mut out_networks, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnEnumerateNetworks"))?;
}
let json = decode_pwstr(out_networks);
if json.is_empty() {
return Ok(Vec::new());
}
let arr: Vec<String> = serde_json::from_str(&json).unwrap_or_default();
let mut guids = Vec::with_capacity(arr.len());
for s in arr {
let bare = s.trim_matches(|c: char| c == '{' || c == '}');
let guid = GUID::try_from(bare).map_err(|e| HnsError::Other {
hresult: 0,
message: format!("bad GUID from HcnEnumerateNetworks: {s}: {e}"),
})?;
guids.push(guid);
}
Ok(guids)
}
fn decode_pwstr(p: PWSTR) -> String {
if p.is_null() {
return String::new();
}
let s = unsafe { p.to_string().unwrap_or_default() };
unsafe {
let _ = LocalFree(Some(HLOCAL(p.0.cast())));
}
s
}
fn classify_error<S: Into<String>>(
hr: windows::core::HRESULT,
err_record: PWSTR,
context: S,
) -> HnsError {
let ctx: String = context.into();
let decoded = decode_pwstr(err_record);
let msg = if decoded.is_empty() {
ctx
} else {
format!("{ctx}: {decoded}")
};
HnsError::from_hresult(hr, msg)
}
#[cfg(test)]
mod tests {
use super::{net_adapter_name_policy, transparent_settings};
use serde_json::json;
#[test]
fn net_adapter_name_policy_shape_matches_hcsshim() {
let v = net_adapter_name_policy("Ethernet 2");
assert_eq!(
v,
json!({
"Type": "NetAdapterName",
"Settings": { "NetworkAdapterName": "Ethernet 2" }
})
);
}
#[test]
fn transparent_settings_wire_format() {
let settings = transparent_settings("zlayer-overlay", "10.200.42.0/28", "Ethernet");
let v = serde_json::to_value(&settings).unwrap();
assert_eq!(v["Name"], json!("zlayer-overlay"));
assert_eq!(v["Type"], json!("Transparent"));
assert_eq!(v["Ipams"][0]["Type"], json!("Static"));
assert_eq!(
v["Ipams"][0]["Subnets"][0]["IpAddressPrefix"],
json!("10.200.42.0/28")
);
assert_eq!(v["Policies"][0]["Type"], json!("NetAdapterName"));
assert_eq!(
v["Policies"][0]["Settings"]["NetworkAdapterName"],
json!("Ethernet")
);
assert_eq!(v["SchemaVersion"]["Major"], json!(2));
assert_eq!(v["SchemaVersion"]["Minor"], json!(0));
}
#[test]
fn transparent_settings_round_trip_preserves_shape() {
let settings = transparent_settings("zlayer-overlay", "10.200.0.0/28", "Ethernet 2");
let json_str = serde_json::to_string(&settings).unwrap();
let parsed: crate::schema::HostComputeNetwork = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed.name, "zlayer-overlay");
assert!(matches!(parsed.ty, crate::schema::NetworkType::Transparent));
assert_eq!(parsed.ipams.len(), 1);
assert_eq!(
parsed.ipams[0].subnets[0].ip_address_prefix,
"10.200.0.0/28"
);
assert_eq!(parsed.policies.len(), 1);
assert_eq!(parsed.policies[0]["Type"], "NetAdapterName");
}
}