#![allow(
clippy::too_many_lines,
clippy::used_underscore_binding,
clippy::doc_markdown,
clippy::single_match_else,
clippy::match_wild_err_arm
)]
#![cfg(target_os = "windows")]
use std::net::IpAddr;
use windows::core::GUID;
use zlayer_hns::adapter::find_primary_adapter;
use zlayer_hns::attach::EndpointAttachment;
use zlayer_hns::endpoint::Endpoint;
use zlayer_hns::network::Network;
use zlayer_hns::schema::{
AclAction, AclDirection, AclPolicySetting, OutBoundNatPolicySetting, SdnRoutePolicySetting,
};
const TEST_CIDR: &str = "10.220.99.0/28";
const TEST_IP: &str = "10.220.99.2";
const TEST_PREFIX_LEN: u8 = 28;
fn new_guid() -> GUID {
GUID::new().expect("GUID::new must succeed on a live Windows host")
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires a real Windows host with a default IPv4 gateway"]
async fn test_find_primary_adapter_nonempty() {
let name = tokio::task::spawn_blocking(find_primary_adapter)
.await
.expect("join error")
.expect("find_primary_adapter must succeed on a real host");
assert!(
!name.is_empty(),
"adapter friendly name must not be empty; got {name:?}",
);
eprintln!("primary adapter: {name}");
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "creates real HCN network + endpoint; requires admin on Windows"]
async fn test_create_transparent_network_and_endpoint() {
let adapter = tokio::task::spawn_blocking(find_primary_adapter)
.await
.expect("join error")
.expect("find_primary_adapter must succeed");
eprintln!("using uplink adapter: {adapter}");
let net_id = new_guid();
let adapter_clone = adapter.clone();
let net_result = tokio::task::spawn_blocking(move || {
Network::create_transparent(net_id, "zlayer-e2e-test", TEST_CIDR, &adapter_clone)
})
.await
.expect("join error");
let _net = match net_result {
Ok(n) => n,
Err(e) => {
eprintln!(
"FAILED: Network::create_transparent: {e:?}\n\
Common causes: not elevated, Hyper-V feature not enabled, \
HNS service not running, or uplink adapter {adapter:?} is not \
actually a physical NIC."
);
panic!("Network::create_transparent: {e}");
}
};
let ip: IpAddr = TEST_IP.parse().expect("TEST_IP is a valid IPv4 literal");
let attach_result = tokio::task::spawn_blocking(move || {
EndpointAttachment::create_overlay(
net_id,
"zlayer-test",
"test-container-1",
ip,
TEST_PREFIX_LEN,
TEST_CIDR,
None,
None,
)
})
.await
.expect("join error");
let attachment = match attach_result {
Ok(a) => a,
Err(e) => {
eprintln!("FAILED: EndpointAttachment::create_overlay: {e:?}");
let _ = tokio::task::spawn_blocking(move || Network::delete(net_id)).await;
panic!("EndpointAttachment::create_overlay: {e}");
}
};
let endpoint_id = attachment.endpoint_id();
let props_result = tokio::task::spawn_blocking(move || {
let ep = Endpoint::open(endpoint_id)?;
ep.query_properties("{}")
})
.await
.expect("join error");
let assertion_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let props = props_result.expect("query_properties must succeed on a live endpoint");
assert_eq!(
props.ip_configurations.len(),
1,
"expected exactly one IP configuration, got {:?}",
props.ip_configurations
);
assert_eq!(
props.ip_configurations[0].ip_address, TEST_IP,
"IPConfigurations[0].IpAddress wire mismatch",
);
assert_eq!(
props.ip_configurations[0].prefix_length, TEST_PREFIX_LEN,
"IPConfigurations[0].PrefixLength wire mismatch",
);
let mut saw_outbound_nat = false;
let mut saw_sdn_route = false;
let mut saw_acl = false;
for p in &props.policies {
match p.get("Type").and_then(|v| v.as_str()) {
Some("OutBoundNAT") => saw_outbound_nat = true,
Some("SDNRoute") => saw_sdn_route = true,
Some("ACL") => saw_acl = true,
_ => {}
}
}
assert!(
saw_outbound_nat,
"expected OutBoundNAT policy, got {:?}",
props.policies
);
assert!(
saw_sdn_route,
"expected SDNRoute policy, got {:?}",
props.policies
);
assert!(saw_acl, "expected ACL policy, got {:?}", props.policies);
}));
let teardown_ep = tokio::task::spawn_blocking(move || attachment.teardown()).await;
if let Ok(Err(e)) = &teardown_ep {
eprintln!("teardown: EndpointAttachment::teardown failed: {e:?}");
} else if let Err(e) = &teardown_ep {
eprintln!("teardown: spawn_blocking join error: {e:?}");
}
drop(_net);
let teardown_net = tokio::task::spawn_blocking(move || Network::delete(net_id)).await;
if let Ok(Err(e)) = &teardown_net {
eprintln!("teardown: Network::delete failed: {e:?}");
} else if let Err(e) = &teardown_net {
eprintln!("teardown: spawn_blocking join error: {e:?}");
}
if let Err(p) = assertion_outcome {
std::panic::resume_unwind(p);
}
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "creates real HCN network + endpoint; requires admin on Windows"]
async fn test_endpoint_policy_json_matches_wire() {
let adapter = tokio::task::spawn_blocking(find_primary_adapter)
.await
.expect("join error")
.expect("find_primary_adapter must succeed");
let net_id = new_guid();
let adapter_clone = adapter.clone();
let _net = tokio::task::spawn_blocking(move || {
Network::create_transparent(net_id, "zlayer-e2e-policy", TEST_CIDR, &adapter_clone)
})
.await
.expect("join error")
.expect("Network::create_transparent");
let ip: IpAddr = TEST_IP.parse().expect("valid IPv4");
let attachment = tokio::task::spawn_blocking(move || {
EndpointAttachment::create_overlay(
net_id,
"zlayer-test",
"test-container-1",
ip,
TEST_PREFIX_LEN,
TEST_CIDR,
None,
None,
)
})
.await
.expect("join error");
let attachment = match attachment {
Ok(a) => a,
Err(e) => {
let _ = tokio::task::spawn_blocking(move || Network::delete(net_id)).await;
panic!("EndpointAttachment::create_overlay: {e}");
}
};
let endpoint_id = attachment.endpoint_id();
let props_result = tokio::task::spawn_blocking(move || {
let ep = Endpoint::open(endpoint_id)?;
ep.query_properties("{}")
})
.await
.expect("join error");
let assertion_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let props = props_result.expect("query_properties");
for p in &props.policies {
let ty = p
.get("Type")
.and_then(|v| v.as_str())
.expect("every policy must have a Type field");
let settings = p
.get("Settings")
.cloned()
.expect("every policy must have a Settings object");
match ty {
"OutBoundNAT" => {
let decoded: OutBoundNatPolicySetting = serde_json::from_value(settings)
.expect(
"OutBoundNAT Settings must deserialize into OutBoundNatPolicySetting",
);
assert_eq!(
decoded.exceptions,
vec![TEST_CIDR.to_string()],
"OutBoundNAT.Exceptions wire mismatch",
);
}
"SDNRoute" => {
let decoded: SdnRoutePolicySetting = serde_json::from_value(settings)
.expect("SDNRoute Settings must deserialize into SdnRoutePolicySetting");
assert_eq!(
decoded.destination_prefix, TEST_CIDR,
"SDNRoute.DestinationPrefix wire mismatch",
);
assert!(
!decoded.need_encap,
"SDNRoute.NeedEncap must be false for overlay-on-WireGuard",
);
}
"ACL" => {
let decoded: AclPolicySetting = serde_json::from_value(settings)
.expect("ACL Settings must deserialize into AclPolicySetting");
assert_eq!(decoded.action, AclAction::Allow, "ACL.Action wire mismatch",);
assert_eq!(
decoded.direction,
AclDirection::In,
"ACL.Direction wire mismatch",
);
assert_eq!(
decoded.remote_addresses, TEST_CIDR,
"ACL.RemoteAddresses wire mismatch",
);
}
_ => {
eprintln!("ignoring unknown policy type {ty:?}");
}
}
}
}));
let teardown_ep = tokio::task::spawn_blocking(move || attachment.teardown()).await;
if let Ok(Err(e)) = &teardown_ep {
eprintln!("teardown: EndpointAttachment::teardown failed: {e:?}");
}
drop(_net);
let teardown_net = tokio::task::spawn_blocking(move || Network::delete(net_id)).await;
if let Ok(Err(e)) = &teardown_net {
eprintln!("teardown: Network::delete failed: {e:?}");
}
if let Err(p) = assertion_outcome {
std::panic::resume_unwind(p);
}
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires a live Wintun adapter (zlayer-overlay or equivalent)"]
async fn test_host_route_install_via_wintun() {
let Some(adapter_alias) = find_wintun_adapter_alias().await else {
eprintln!(
"SKIP: no Wintun adapter (looked for 'zlayer-overlay' or any \
InterfaceDescription matching 'Wintun'); start the overlay \
transport before re-running this test."
);
return;
};
eprintln!("using wintun adapter: {adapter_alias}");
let install = run_powershell(&format!(
"New-NetRoute -DestinationPrefix '{TEST_CIDR}' \
-InterfaceAlias '{adapter_alias}' -PolicyStore ActiveStore \
-ErrorAction Stop | Out-Null"
))
.await;
if let Err(e) = install {
eprintln!("FAILED to install test route: {e}");
panic!("New-NetRoute failed: {e}");
}
let verify = run_powershell(&format!(
"Get-NetRoute -DestinationPrefix '{TEST_CIDR}' \
-ErrorAction SilentlyContinue | Format-List"
))
.await;
let assertion_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let stdout = verify.expect("Get-NetRoute invocation");
assert!(
stdout.contains("InterfaceAlias"),
"Get-NetRoute stdout missing 'InterfaceAlias' field: {stdout}",
);
assert!(
stdout.contains(&adapter_alias),
"Get-NetRoute stdout does not reference our adapter {adapter_alias:?}: {stdout}",
);
}));
let _ = run_powershell(&format!(
"Remove-NetRoute -DestinationPrefix '{TEST_CIDR}' \
-InterfaceAlias '{adapter_alias}' -Confirm:$false \
-ErrorAction SilentlyContinue"
))
.await;
if let Err(p) = assertion_outcome {
std::panic::resume_unwind(p);
}
}
async fn find_wintun_adapter_alias() -> Option<String> {
let exact = run_powershell(
"(Get-NetAdapter -Name 'zlayer-overlay' -ErrorAction SilentlyContinue).Name",
)
.await
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
if let Some(a) = exact {
return Some(a);
}
let wintun = run_powershell(
"(Get-NetAdapter | Where-Object { $_.InterfaceDescription -like '*Wintun*' } | \
Select-Object -First 1).Name",
)
.await
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
wintun
}
async fn run_powershell(command: &str) -> Result<String, String> {
let command = command.to_string();
tokio::task::spawn_blocking(move || {
let output = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &command])
.output()
.map_err(|e| format!("failed to spawn powershell: {e}"))?;
if !output.status.success() {
return Err(format!(
"powershell exit {:?}\nstdout: {}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
})
.await
.map_err(|e| format!("join error: {e}"))?
}