diff --git a/docs/backends/wslc/wsl-container-getting-started.md b/docs/backends/wslc/wsl-container-getting-started.md index 8112b477d7..21aae16320 100644 --- a/docs/backends/wslc/wsl-container-getting-started.md +++ b/docs/backends/wslc/wsl-container-getting-started.md @@ -14,7 +14,7 @@ MXC, which lets you run Linux containers on Windows using the WSLC SDK. | Requirement | Details | |---|---| | **Windows 10 1903 (build 18362.1049)+ on x64, or Windows 10 2004 (build 19041)+ on ARM64** | WSL 2's own [system requirements](https://learn.microsoft.com/windows/wsl/install-manual#step-2---check-requirements-for-running-wsl-2). MXC does not check the OS version itself — see [How an unsupported Windows version is reported](#how-an-unsupported-windows-version-is-reported) below. The WSL runtime package installs only on build 19041 and later, so in practice a 1903/1909 host still needs an upgrade to reach the WSL version below. | -| **WSL 3.0.3+** | The installed WSL runtime package must meet the WSLC minimum; see Step 1 below for installation | +| **WSL 3.0.5+** | The installed WSL runtime package must meet the WSLC minimum; see Step 1 below for installation | | **WSLC SDK** | `wslcsdk.dll` is a separate client SDK and must be in the same directory as the running executable (`wxc-exec.exe`, or your own binary when using the Rust SDK) | | **Container images** | Reachable from a registry, already cached, or supplied as a local tar | @@ -54,9 +54,9 @@ optional component is absent as well. Any SDK call that reaches the WSL service can return it, so an unexpected `0x80040327` from a WSLC operation means the host OS is below WSL 2's floor, not that WSL is merely uninstalled. -## Step 1 — Install WSL 3.0.3+ +## Step 1 — Install WSL 3.0.5+ -The WSLC SDK requires WSL version 3.0.3 or later. Update WSL to the latest +The WSLC SDK requires WSL version 3.0.5 or later. Update WSL to the latest version: ```powershell @@ -69,7 +69,7 @@ Verify your WSL version after updating: wsl --version ``` -The WSL version should be **3.0.3.0 or later**. If `wsl --update` +The WSL version should be **3.0.5.0 or later**. If `wsl --update` does not bring you to the required version, build WSL from the `master` branch: @@ -518,7 +518,7 @@ no separate `--setup-wslc` step is required. | Exact v0.9 policy | WSLC behavior | |---|---| -| Egress, ingress, and host-loopback defaults all `allow` | Bridged networking without independent directional filtering | +| Egress, ingress, and host-loopback defaults all `allow` | Bridged networking without independent directional filtering; reaches [host loopback](#host-loopback) | | All three defaults `deny` (also the omitted defaults) | No networking (isolated) | | Mixed allow/deny directions or egress rules | Rejected; WSLC cannot enforce that combination | @@ -527,6 +527,22 @@ no separate `--setup-wslc` step is required. > unenforced. The removed legacy `allowOutbound` setting and JSON host-list > fields are not accepted by any supported contract. +### Host loopback + +MXC enables WSL host loopback on every session it creates. A bridged container reaches a service bound to Windows `127.0.0.1` +through the hostname `host.wslc.internal`: + +```sh +wget -qO- http://host.wslc.internal:8080/ +``` + +- Every Windows loopback TCP port is reachable; there is no per-port filter. + +- **Behavior change:** existing all-`allow` configs gain outbound access to + Windows loopback services. + +- Inbound connections from Windows still need declared port mappings. + ### Network proxy (cooperative, unprivileged) WSLC supports a **cooperative HTTP/HTTPS proxy**: setting `runtimeConfig.networkProxy` @@ -553,8 +569,9 @@ variables that well-behaved clients honor. **The runtime field contains a URL string.** A WSLC container runs in its own network namespace (a separate WSL system VM). Its own `127.0.0.1` loopback is -valid for a proxy running inside that container, as in the test fixture below; -a proxy on the host's or WSL distro's loopback is **not reachable**. An external +valid for a proxy running inside that container, as in the test fixture below. +The host's or WSL distro's `127.0.0.1` is **not reachable** at that address; a +proxy on Windows loopback is reachable through `host.wslc.internal` (see [Host loopback](#host-loopback)). An external proxy must have an address routable from the container: ```json @@ -763,7 +780,7 @@ images — cannot be used. | `WSLC runtime unavailable` **on a host below WSL 2's minimum Windows version** | The WSL runtime package cannot install on this OS, so it reports as missing — MXC does not distinguish the two cases | Run `wsl --update`; WSL reports the version verdict ("Windows version {} does not support the packaged version of Windows Subsystem for Linux"). Upgrade Windows — see [How an unsupported Windows version is reported](#how-an-unsupported-windows-version-is-reported) | | HRESULT `0x80040327` (`WSL_E_OS_NOT_SUPPORTED`) from any WSLC call | The SDK reached WSL's service-connect guard on a host that is neither Windows 11 nor has the WSL support interface | Upgrade Windows. `0x80040321` (`WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED`) is the sibling code when the legacy `lxss` component is absent too | | `Failed to load wslcsdk.dll` | DLL not in same directory as `wxc-exec.exe` | Copy `wslcsdk.dll` next to the binary | -| `WSLC runtime unavailable` | WSL runtime package is missing, older than 3.0.3, or the Virtual Machine Platform optional component is disabled | Update WSL with `wsl --update`, verify the installed version with `wsl --version`, and enable the Virtual Machine Platform optional component if required. The WSLC SDK DLL is a separate dependency and does not replace the WSL runtime package. | +| `WSLC runtime unavailable` | WSL runtime package is missing, older than 3.0.5, or the Virtual Machine Platform optional component is disabled | Update WSL with `wsl --update`, verify the installed version with `wsl --version`, and enable the Virtual Machine Platform optional component if required. The WSLC SDK DLL is a separate dependency and does not replace the WSL runtime package. | | `WSLC runtime unavailable. Missing components: SdkNeedsUpdate` | The opposite direction: your installed WSL is **newer** than the WSLc SDK this MXC build ships (specified by `WSLC_SDK_VERSION` in `src/mxc-sdk/build/build_wslc_common.rs`) | Update MXC to a build that includes a newer SDK. Do **not** update WSL — it is already ahead, and updating it further will not clear this. | | `WSLC image '' is not cached, and this sandbox declares no egress` | An isolated config named an image the store does not have | Warm the cache with `--setup-wslc`, set `imageTarPath`, or allow egress. For a lifecycle operation the store is a daemon slot, so warm it with `setup-wslc.ps1 -DaemonSlots 1` | | `WSLc store unavailable: the image store at is already held by another live session` | Two processes reached the same store, which admits one live session | Wait for the holder to release it. A running `wxc-wslc-daemon.exe` releases its slot after `MXC_WSLC_DAEMON_IDLE_TIMEOUT_SECS` (default 300s); a create-and-run execution releases its store when the run ends | @@ -779,6 +796,7 @@ images — cannot be used. - [`tests/examples/wslc_hello_world.json`](../../../tests/examples/wslc_hello_world.json) — Hello world with Alpine - [`tests/configs/wslc_network_isolated.json`](../../../tests/configs/wslc_network_isolated.json) — Network isolation +- [`tests/configs/wslc_network_host_loopback.json`](../../../tests/configs/wslc_network_host_loopback.json) — Bridged container reaches a Windows `127.0.0.1` service through `host.wslc.internal` (driven by `run_wslc_host_loopback_test.ps1`) - [`tests/configs/wslc_network_proxy.json`](../../../tests/configs/wslc_network_proxy.json) — In-container cooperative HTTP proxy (`runtimeConfig.networkProxy` at the container's `127.0.0.1`) - [`tests/configs/wslc_custom_registry_ghcr.json`](../../../tests/configs/wslc_custom_registry_ghcr.json) — Pull from GitHub Container Registry - [`tests/configs/wslc_custom_registry_quay.json`](../../../tests/configs/wslc_custom_registry_quay.json) — Pull from Quay.io diff --git a/docs/backends/wslc/wslc-state-aware.md b/docs/backends/wslc/wslc-state-aware.md index 224041e4c0..bda1805f8e 100644 --- a/docs/backends/wslc/wslc-state-aware.md +++ b/docs/backends/wslc/wslc-state-aware.md @@ -20,7 +20,7 @@ Native builds still require the `wslc` feature (`build.bat --with-wslc`). ## Why a daemon -The WSLc SDK (`wslcsdk.dll`, 3.0.3) has **no cross-process session re-attach**: every operation +The WSLc SDK (`wslcsdk.dll`, 3.0.5) has **no cross-process session re-attach**: every operation (`WslcCreateContainer`, `WslcStartContainer`, `WslcCreateContainerProcess`, image pull, stop / delete) requires a live in-process `WslcSession` / `WslcContainer` handle, and there is no `WslcOpenSession`. Each lifecycle phase runs as a **separate** @@ -640,7 +640,9 @@ Omitted directional values default to deny, so an egress-only allow request is rejected rather than claiming its implicit ingress/loopback denies are enforced. Mixed postures and per-host rules are rejected. Allowing ingress/host-loopback does not create host port forwarding or promise reachability across NAT; it -acknowledges that WSLC cannot independently restrict those directions. +acknowledges that WSLC cannot independently restrict those directions. The +daemon's sessions enable host loopback, so bridged containers reach Windows `127.0.0.1` services through `host.wslc.internal`; see +[Host loopback](wsl-container-getting-started.md#host-loopback). | Field | provision | start / stop / deprovision | exec | |-------|-----------|----------------------------|------| diff --git a/docs/development/build-and-test/wslc-sdk-bindings.md b/docs/development/build-and-test/wslc-sdk-bindings.md index 2588168d01..825ae9e746 100644 --- a/docs/development/build-and-test/wslc-sdk-bindings.md +++ b/docs/development/build-and-test/wslc-sdk-bindings.md @@ -108,6 +108,8 @@ match the pinned SDK. cargo test -p mxc-sdk --features wslc --lib wslc::common # includes ABI size/enum asserts cargo fmt --all -- --check cargo clippy -p mxc-sdk --features wslc --all-targets -- -D warnings + cd .. + .\scripts\generate-wslc-bindings.ps1 -Check ``` 7. **Run the WSLC E2E suite on a live WSL host** (cannot run in GitHub Actions — diff --git a/scripts/generate-wslc-bindings.ps1 b/scripts/generate-wslc-bindings.ps1 index 8cb71638a6..df52028be8 100644 --- a/scripts/generate-wslc-bindings.ps1 +++ b/scripts/generate-wslc-bindings.ps1 @@ -169,7 +169,7 @@ try { # newtypes via --bitfield-enum; exclusive-value enums become opaque # newtypes via --newtype-enum. Both keep the C ABI while avoiding UB from # out-of-range discriminants that a plain Rust `enum` would allow. - $bitfieldEnums = 'Wslc(ContainerFlags|SessionFeatureFlags|ContainerStartFlags|DeleteContainerFlags|ComponentFlags|VhdRequirementsFlags|ProcessFlags)' + $bitfieldEnums = 'Wslc(ContainerFlags|SessionFeatureFlags|ContainerStartFlags|DeleteContainerFlags|ComponentFlags|VhdRequirementsFlags|ProcessFlags|NetworkFlags)' $newtypeEnums = 'Wslc(PortProtocol|Signal|ProcessIOHandle|ContainerNetworkingMode|VhdType|SessionTerminationReason|ContainerState|ProcessState|ImageProgressStatus)' $generatedFile = if ($Check) { Join-Path $staging "wslcsdk_sys.rs" } else { $outFile } diff --git a/src/mxc-sdk/build/build_wslc_common.rs b/src/mxc-sdk/build/build_wslc_common.rs index 7749aae265..3324bc0ee5 100644 --- a/src/mxc-sdk/build/build_wslc_common.rs +++ b/src/mxc-sdk/build/build_wslc_common.rs @@ -27,7 +27,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; /// Pinned WSLC SDK version. Override with the `WSLC_SDK_VERSION` env var. -const WSLC_SDK_VERSION: &str = "3.0.3"; +const WSLC_SDK_VERSION: &str = "3.0.5"; /// Direct anonymous content URL for a package version on the public /// MxcDependencies Azure Artifacts feed (mirrors nuget.org; reachable from the @@ -293,6 +293,9 @@ fn expected_sha256(version: &str) -> Option { // version. This is an exact-bytes content pin, independent of the NuGet // author signature. match version { + "3.0.5" => { + Some("23e905e0f0925834d9960d4d752f8bedcbd34cb59a20fb0578c6336745f8e88f".to_string()) + } "3.0.3" => { Some("d31f78cc4816ccccc0dc196ecf4d2b27911b8ed677b4b9cc697274e941b6511b".to_string()) } diff --git a/src/mxc-sdk/build/wslc_common/Microsoft.WSL.Containers.3.0.3.nupkg b/src/mxc-sdk/build/wslc_common/Microsoft.WSL.Containers.3.0.3.nupkg deleted file mode 100644 index 049ef7bfd5..0000000000 Binary files a/src/mxc-sdk/build/wslc_common/Microsoft.WSL.Containers.3.0.3.nupkg and /dev/null differ diff --git a/src/mxc-sdk/build/wslc_common/Microsoft.WSL.Containers.3.0.5.nupkg b/src/mxc-sdk/build/wslc_common/Microsoft.WSL.Containers.3.0.5.nupkg new file mode 100644 index 0000000000..7334986a93 Binary files /dev/null and b/src/mxc-sdk/build/wslc_common/Microsoft.WSL.Containers.3.0.5.nupkg differ diff --git a/src/mxc-sdk/build/wslc_common/README.md b/src/mxc-sdk/build/wslc_common/README.md index 70c7eb890e..a26e3989a3 100644 --- a/src/mxc-sdk/build/wslc_common/README.md +++ b/src/mxc-sdk/build/wslc_common/README.md @@ -4,7 +4,7 @@ **Package:** `Microsoft.WSL.Containers` **Version:** pinned in `src/mxc-sdk/build/build_wslc_common.rs` -(`WSLC_SDK_VERSION`, currently **3.0.3**) +(`WSLC_SDK_VERSION`, currently **3.0.5**) **Source:** the [`MxcDependencies`](https://dev.azure.com/shine-oss/mxc/_artifacts/feed/MxcDependencies) Azure Artifacts feed — a public, anonymous-read mirror of nuget.org. @@ -42,7 +42,7 @@ environment variable. ## Contents -- `Microsoft.WSL.Containers.3.0.3.nupkg` — vendored fallback copy of the SDK +- `Microsoft.WSL.Containers..nupkg` — vendored fallback copy of the SDK (header `wslcsdk.h`, import libs `wslcsdk.lib`, and runtime `wslcsdk.dll` for x64 and ARM64). Must match `WSLC_SDK_VERSION` in `build.rs`. diff --git a/src/mxc-sdk/src/backends/wslc/common/container_steps.rs b/src/mxc-sdk/src/backends/wslc/common/container_steps.rs index 7cc10cda50..08c1ec7ffa 100644 --- a/src/mxc-sdk/src/backends/wslc/common/container_steps.rs +++ b/src/mxc-sdk/src/backends/wslc/common/container_steps.rs @@ -33,7 +33,7 @@ //! move that happens *before* any `&raw` is taken), after which callers keep it //! as a stationary local. -use std::ffi::c_void; +use std::ffi::{c_void, CStr}; use std::fmt::Write; use std::ptr; use std::sync::atomic::{AtomicBool, Ordering}; @@ -845,15 +845,9 @@ pub unsafe fn create_daemon_session( } apply_session_sizing(sdk, &mut settings, sizing)?; - let mut session: WslcSession = ptr::null_mut(); - let mut err_msg = CoTaskMemPWSTR::null(); let started = Instant::now(); - let hr = sdk.WslcCreateSession(&mut settings, &mut session, err_msg.as_mut_ptr()); + let session = create_session(sdk, &mut settings)?; let created_in = started.elapsed(); - if hr != S_OK { - let msg = err_msg.to_string_lossy(); - return Err(sdk_error("WslcCreateSession failed", hr, &msg).into()); - } let guard = WslcSessionGuard::from_raw( session, @@ -898,6 +892,45 @@ pub unsafe fn create_daemon_session( Ok(guard) } +/// Hostname that resolves to the Windows host loopback inside a session. WSL's +/// own CLI uses the same default. +pub(crate) const HOST_LOOPBACK_NAME: &CStr = c"host.wslc.internal"; + +/// Create a session with host loopback enabled. A WSL runtime older than 3.0.5 +/// rejects the setting. +/// +/// # Safety +/// `sdk` must hold valid function pointers, `settings` must be initialised and +/// COM must be initialised on this thread. +pub(crate) unsafe fn create_session( + sdk: &WslcSdk, + settings: &mut WslcSessionSettings, +) -> Result { + let hr = sdk.WslcSetSessionSettingsHostLoopback(settings, HOST_LOOPBACK_NAME.as_ptr()); + if hr != S_OK { + return Err(sdk_error( + "WslcSetSessionSettingsHostLoopback failed", + hr, + "", + )); + } + + let mut session: WslcSession = ptr::null_mut(); + let mut err_msg = CoTaskMemPWSTR::null(); + let hr = sdk.WslcCreateSession(settings, &mut session, err_msg.as_mut_ptr()); + if hr == WSLC_E_RUNTIME_UPDATE_NEEDED { + return Err(WslcError::Unavailable(sdk_init::prerequisite_error( + WslcComponentFlags::WSLC_COMPONENT_FLAG_WSL_PACKAGE, + )) + .into_response()); + } + if hr != S_OK { + let msg = err_msg.to_string_lossy(); + return Err(sdk_error("WslcCreateSession failed", hr, &msg)); + } + Ok(session) +} + /// Apply a session's sizing and its boot deadline to `settings`. /// /// The boot timeout is applied even when the caller asked for no sizing, since diff --git a/src/mxc-sdk/src/backends/wslc/common/sdk_init.rs b/src/mxc-sdk/src/backends/wslc/common/sdk_init.rs index 4cab801312..f36d788f46 100644 --- a/src/mxc-sdk/src/backends/wslc/common/sdk_init.rs +++ b/src/mxc-sdk/src/backends/wslc/common/sdk_init.rs @@ -79,7 +79,7 @@ pub(crate) fn prerequisite_error(missing: WslcComponentFlags) -> String { ); } if needs_wsl_package { - guidance.push("install WSL 3.0.3 or newer by running `wsl --update`".to_string()); + guidance.push("install WSL 3.0.5 or newer by running `wsl --update`".to_string()); } if needs_sdk_update { // MXC's vendored SDK is behind the installed WSL, so the fix is on MXC's diff --git a/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs b/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs index 41092e9a93..53e3518abd 100644 --- a/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs +++ b/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs @@ -659,14 +659,7 @@ impl WSLContainerRunner { } } - // Create session while string data is still alive - let mut session: WslcSession = ptr::null_mut(); - let mut err_msg = CoTaskMemPWSTR::null(); - let hr = sdk.WslcCreateSession(&mut settings, &mut session, err_msg.as_mut_ptr()); - if hr != S_OK { - let msg = err_msg.to_string_lossy(); - return Err(sdk_error("WslcCreateSession failed", hr, &msg)); - } + let session = container_steps::create_session(sdk, &mut settings)?; let _ = writeln!(logger, "[WSLC] Session created: {session_name_str}"); Ok(WslcSessionGuard::from_raw( @@ -3274,7 +3267,7 @@ mod tests { sdk_init::prerequisite_error(WslcComponentFlags::WSLC_COMPONENT_FLAG_WSL_PACKAGE); assert!(message.contains("WslPackage")); - assert!(message.contains("3.0.3")); + assert!(message.contains("3.0.5")); assert!(message.contains("wsl --update")); assert!(!message.contains("--pre-release")); assert!(!message.contains("Virtual Machine Platform")); diff --git a/src/mxc-sdk/src/backends/wslc/common/wslc_bindings.rs b/src/mxc-sdk/src/backends/wslc/common/wslc_bindings.rs index 2a6f3c70fc..c4bdd0561a 100644 --- a/src/mxc-sdk/src/backends/wslc/common/wslc_bindings.rs +++ b/src/mxc-sdk/src/backends/wslc/common/wslc_bindings.rs @@ -43,6 +43,9 @@ pub const WSLC_E_IMAGE_NOT_FOUND: HRESULT = wslc_error(1); /// Administrative policy on this host forbids reaching the registry. pub const WSLC_E_REGISTRY_BLOCKED_BY_POLICY: HRESULT = wslc_error(13); +/// The installed WSL runtime is older than a requested setting needs. +pub const WSLC_E_RUNTIME_UPDATE_NEEDED: HRESULT = wslc_error(21); + // --------------------------------------------------------------------------- // Raw function-pointer types used by the RAII guards. // @@ -156,6 +159,7 @@ impl WslcSdk { WslcSetSessionSettingsMemory, WslcSetSessionSettingsTimeout, WslcSetSessionSettingsFeatureFlags, + WslcSetSessionSettingsHostLoopback, WslcTerminateSession, WslcReleaseSession, WslcInitContainerSettings, diff --git a/src/mxc-sdk/src/backends/wslc/common/wslcsdk_sys.rs b/src/mxc-sdk/src/backends/wslc/common/wslcsdk_sys.rs index 628532075b..93990c9ca4 100644 --- a/src/mxc-sdk/src/backends/wslc/common/wslcsdk_sys.rs +++ b/src/mxc-sdk/src/backends/wslc/common/wslcsdk_sys.rs @@ -16,16 +16,21 @@ #![allow(clippy::all)] pub const WSLC_E_BASE: u32 = 1536; -pub const WSLC_SESSION_OPTIONS_SIZE: u32 = 72; +pub const WSLC_SESSION_OPTIONS_SIZE: u32 = 80; pub const WSLC_SESSION_OPTIONS_ALIGNMENT: u32 = 8; -pub const WSLC_CONTAINER_OPTIONS_SIZE: u32 = 104; +pub const WSLC_CONTAINER_OPTIONS_SIZE: u32 = 152; pub const WSLC_CONTAINER_OPTIONS_ALIGNMENT: u32 = 8; pub const WSLC_CONTAINER_PROCESS_OPTIONS_SIZE: u32 = 72; pub const WSLC_CONTAINER_PROCESS_OPTIONS_ALIGNMENT: u32 = 8; pub const WSLC_CONTAINER_ID_BUFFER_SIZE: u32 = 65; pub const WSLC_IMAGE_NAME_LENGTH: u32 = 256; +pub const WSLC_NETWORK_ID_LENGTH: u32 = 65; +pub const WSLC_NETWORK_NAME_LENGTH: u32 = 256; +pub const WSLC_NETWORK_DRIVER_LENGTH: u32 = 256; pub type wchar_t = ::std::os::raw::c_ushort; +pub type ULONG = ::std::os::raw::c_ulong; pub type USHORT = ::std::os::raw::c_ushort; +pub type UCHAR = ::std::os::raw::c_uchar; pub type BOOL = ::std::os::raw::c_int; pub type BYTE = ::std::os::raw::c_uchar; pub type INT32 = ::std::os::raw::c_int; @@ -39,6 +44,77 @@ pub type PSTR = *mut CHAR; pub type PCSTR = *const CHAR; pub type HANDLE = *mut ::std::os::raw::c_void; pub type HRESULT = ::std::os::raw::c_long; +#[repr(C)] +#[derive(Copy, Clone)] +pub struct in_addr { + pub S_un: in_addr__bindgen_ty_1, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub union in_addr__bindgen_ty_1 { + pub S_un_b: in_addr__bindgen_ty_1__bindgen_ty_1, + pub S_un_w: in_addr__bindgen_ty_1__bindgen_ty_2, + pub S_addr: ULONG, +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct in_addr__bindgen_ty_1__bindgen_ty_1 { + pub s_b1: UCHAR, + pub s_b2: UCHAR, + pub s_b3: UCHAR, + pub s_b4: UCHAR, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of in_addr__bindgen_ty_1__bindgen_ty_1"] + [::std::mem::size_of::() - 4usize]; + ["Alignment of in_addr__bindgen_ty_1__bindgen_ty_1"] + [::std::mem::align_of::() - 1usize]; + ["Offset of field: in_addr__bindgen_ty_1__bindgen_ty_1::s_b1"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1__bindgen_ty_1, s_b1) - 0usize]; + ["Offset of field: in_addr__bindgen_ty_1__bindgen_ty_1::s_b2"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1__bindgen_ty_1, s_b2) - 1usize]; + ["Offset of field: in_addr__bindgen_ty_1__bindgen_ty_1::s_b3"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1__bindgen_ty_1, s_b3) - 2usize]; + ["Offset of field: in_addr__bindgen_ty_1__bindgen_ty_1::s_b4"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1__bindgen_ty_1, s_b4) - 3usize]; +}; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct in_addr__bindgen_ty_1__bindgen_ty_2 { + pub s_w1: USHORT, + pub s_w2: USHORT, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of in_addr__bindgen_ty_1__bindgen_ty_2"] + [::std::mem::size_of::() - 4usize]; + ["Alignment of in_addr__bindgen_ty_1__bindgen_ty_2"] + [::std::mem::align_of::() - 2usize]; + ["Offset of field: in_addr__bindgen_ty_1__bindgen_ty_2::s_w1"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1__bindgen_ty_2, s_w1) - 0usize]; + ["Offset of field: in_addr__bindgen_ty_1__bindgen_ty_2::s_w2"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1__bindgen_ty_2, s_w2) - 2usize]; +}; +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of in_addr__bindgen_ty_1"][::std::mem::size_of::() - 4usize]; + ["Alignment of in_addr__bindgen_ty_1"] + [::std::mem::align_of::() - 4usize]; + ["Offset of field: in_addr__bindgen_ty_1::S_un_b"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1, S_un_b) - 0usize]; + ["Offset of field: in_addr__bindgen_ty_1::S_un_w"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1, S_un_w) - 0usize]; + ["Offset of field: in_addr__bindgen_ty_1::S_addr"] + [::std::mem::offset_of!(in_addr__bindgen_ty_1, S_addr) - 0usize]; +}; +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of in_addr"][::std::mem::size_of::() - 4usize]; + ["Alignment of in_addr"][::std::mem::align_of::() - 4usize]; + ["Offset of field: in_addr::S_un"][::std::mem::offset_of!(in_addr, S_un) - 0usize]; +}; +pub type IN_ADDR = in_addr; pub type ADDRESS_FAMILY = USHORT; #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -62,14 +138,42 @@ const _: () = { [::std::mem::offset_of!(sockaddr_storage, __ss_pad2) - 16usize]; }; #[repr(C)] +#[derive(Copy, Clone)] +pub struct in6_addr { + pub u: in6_addr__bindgen_ty_1, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub union in6_addr__bindgen_ty_1 { + pub Byte: [UCHAR; 16usize], + pub Word: [USHORT; 8usize], +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of in6_addr__bindgen_ty_1"][::std::mem::size_of::() - 16usize]; + ["Alignment of in6_addr__bindgen_ty_1"] + [::std::mem::align_of::() - 2usize]; + ["Offset of field: in6_addr__bindgen_ty_1::Byte"] + [::std::mem::offset_of!(in6_addr__bindgen_ty_1, Byte) - 0usize]; + ["Offset of field: in6_addr__bindgen_ty_1::Word"] + [::std::mem::offset_of!(in6_addr__bindgen_ty_1, Word) - 0usize]; +}; +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of in6_addr"][::std::mem::size_of::() - 16usize]; + ["Alignment of in6_addr"][::std::mem::align_of::() - 2usize]; + ["Offset of field: in6_addr::u"][::std::mem::offset_of!(in6_addr, u) - 0usize]; +}; +pub type IN6_ADDR = in6_addr; +#[repr(C)] #[repr(align(8))] #[derive(Debug, Copy, Clone)] pub struct WslcSessionSettings { - pub _opaque: [BYTE; 72usize], + pub _opaque: [BYTE; 80usize], } #[allow(clippy::unnecessary_operation, clippy::identity_op)] const _: () = { - ["Size of WslcSessionSettings"][::std::mem::size_of::() - 72usize]; + ["Size of WslcSessionSettings"][::std::mem::size_of::() - 80usize]; ["Alignment of WslcSessionSettings"][::std::mem::align_of::() - 8usize]; ["Offset of field: WslcSessionSettings::_opaque"] [::std::mem::offset_of!(WslcSessionSettings, _opaque) - 0usize]; @@ -91,11 +195,11 @@ pub type WslcSession = *mut WslcSession__; #[repr(align(8))] #[derive(Debug, Copy, Clone)] pub struct WslcContainerSettings { - pub _opaque: [BYTE; 104usize], + pub _opaque: [BYTE; 152usize], } #[allow(clippy::unnecessary_operation, clippy::identity_op)] const _: () = { - ["Size of WslcContainerSettings"][::std::mem::size_of::() - 104usize]; + ["Size of WslcContainerSettings"][::std::mem::size_of::() - 152usize]; ["Alignment of WslcContainerSettings"] [::std::mem::align_of::() - 8usize]; ["Offset of field: WslcContainerSettings::_opaque"] @@ -373,6 +477,30 @@ const _: () = { ["Offset of field: WslcContainerNamedVolume::readOnly"] [::std::mem::offset_of!(WslcContainerNamedVolume, readOnly) - 16usize]; }; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WslcContainerNetwork { + pub name: PCSTR, + pub aliases: *const PCSTR, + pub aliasCount: u32, + pub ipv4Address: *const IN_ADDR, + pub ipv6Address: *const IN6_ADDR, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of WslcContainerNetwork"][::std::mem::size_of::() - 40usize]; + ["Alignment of WslcContainerNetwork"][::std::mem::align_of::() - 8usize]; + ["Offset of field: WslcContainerNetwork::name"] + [::std::mem::offset_of!(WslcContainerNetwork, name) - 0usize]; + ["Offset of field: WslcContainerNetwork::aliases"] + [::std::mem::offset_of!(WslcContainerNetwork, aliases) - 8usize]; + ["Offset of field: WslcContainerNetwork::aliasCount"] + [::std::mem::offset_of!(WslcContainerNetwork, aliasCount) - 16usize]; + ["Offset of field: WslcContainerNetwork::ipv4Address"] + [::std::mem::offset_of!(WslcContainerNetwork, ipv4Address) - 24usize]; + ["Offset of field: WslcContainerNetwork::ipv6Address"] + [::std::mem::offset_of!(WslcContainerNetwork, ipv6Address) - 32usize]; +}; impl WslcContainerFlags { pub const WSLC_CONTAINER_FLAG_NONE: WslcContainerFlags = WslcContainerFlags(0); pub const WSLC_CONTAINER_FLAG_AUTO_REMOVE: WslcContainerFlags = WslcContainerFlags(1); @@ -749,6 +877,115 @@ pub const WslcIdentityTokenType_WSLC_IDENTITY_TOKEN_TYPE_UNKNOWN: WslcIdentityTo pub const WslcIdentityTokenType_WSLC_IDENTITY_TOKEN_TYPE_TOKEN: WslcIdentityTokenType = 1; pub const WslcIdentityTokenType_WSLC_IDENTITY_TOKEN_TYPE_CREDENTIALS: WslcIdentityTokenType = 2; pub type WslcIdentityTokenType = ::std::os::raw::c_int; +impl WslcNetworkFlags { + pub const WSLC_NETWORK_FLAG_NONE: WslcNetworkFlags = WslcNetworkFlags(0); + pub const WSLC_NETWORK_FLAG_ENABLE_IPV4: WslcNetworkFlags = WslcNetworkFlags(1); + pub const WSLC_NETWORK_FLAG_ENABLE_IPV6: WslcNetworkFlags = WslcNetworkFlags(2); + pub const WSLC_NETWORK_FLAG_INTERNAL: WslcNetworkFlags = WslcNetworkFlags(4); +} +impl ::std::ops::BitOr for WslcNetworkFlags { + type Output = Self; + #[inline] + fn bitor(self, other: Self) -> Self { + WslcNetworkFlags(self.0 | other.0) + } +} +impl ::std::ops::BitOrAssign for WslcNetworkFlags { + #[inline] + fn bitor_assign(&mut self, rhs: WslcNetworkFlags) { + self.0 |= rhs.0; + } +} +impl ::std::ops::BitAnd for WslcNetworkFlags { + type Output = Self; + #[inline] + fn bitand(self, other: Self) -> Self { + WslcNetworkFlags(self.0 & other.0) + } +} +impl ::std::ops::BitAndAssign for WslcNetworkFlags { + #[inline] + fn bitand_assign(&mut self, rhs: WslcNetworkFlags) { + self.0 &= rhs.0; + } +} +#[repr(transparent)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub struct WslcNetworkFlags(pub ::std::os::raw::c_int); +#[repr(C)] +#[derive(Copy, Clone)] +pub struct WslcNetworkInfo { + pub id: [CHAR; 65usize], + pub name: [CHAR; 256usize], + pub driver: [CHAR; 256usize], + pub createdUnixTime: i64, + pub flags: WslcNetworkFlags, + pub ipv4PrefixLength: u8, + pub ipv6PrefixLength: u8, + pub ipv4Subnet: IN_ADDR, + pub ipv4Gateway: IN_ADDR, + pub ipv6Subnet: IN6_ADDR, + pub ipv6Gateway: IN6_ADDR, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of WslcNetworkInfo"][::std::mem::size_of::() - 640usize]; + ["Alignment of WslcNetworkInfo"][::std::mem::align_of::() - 8usize]; + ["Offset of field: WslcNetworkInfo::id"][::std::mem::offset_of!(WslcNetworkInfo, id) - 0usize]; + ["Offset of field: WslcNetworkInfo::name"] + [::std::mem::offset_of!(WslcNetworkInfo, name) - 65usize]; + ["Offset of field: WslcNetworkInfo::driver"] + [::std::mem::offset_of!(WslcNetworkInfo, driver) - 321usize]; + ["Offset of field: WslcNetworkInfo::createdUnixTime"] + [::std::mem::offset_of!(WslcNetworkInfo, createdUnixTime) - 584usize]; + ["Offset of field: WslcNetworkInfo::flags"] + [::std::mem::offset_of!(WslcNetworkInfo, flags) - 592usize]; + ["Offset of field: WslcNetworkInfo::ipv4PrefixLength"] + [::std::mem::offset_of!(WslcNetworkInfo, ipv4PrefixLength) - 596usize]; + ["Offset of field: WslcNetworkInfo::ipv6PrefixLength"] + [::std::mem::offset_of!(WslcNetworkInfo, ipv6PrefixLength) - 597usize]; + ["Offset of field: WslcNetworkInfo::ipv4Subnet"] + [::std::mem::offset_of!(WslcNetworkInfo, ipv4Subnet) - 600usize]; + ["Offset of field: WslcNetworkInfo::ipv4Gateway"] + [::std::mem::offset_of!(WslcNetworkInfo, ipv4Gateway) - 604usize]; + ["Offset of field: WslcNetworkInfo::ipv6Subnet"] + [::std::mem::offset_of!(WslcNetworkInfo, ipv6Subnet) - 608usize]; + ["Offset of field: WslcNetworkInfo::ipv6Gateway"] + [::std::mem::offset_of!(WslcNetworkInfo, ipv6Gateway) - 624usize]; +}; +#[repr(C)] +#[derive(Copy, Clone)] +pub struct WslcNetworkSettings { + pub name: PCSTR, + pub flags: WslcNetworkFlags, + pub ipv4PrefixLength: u8, + pub ipv6PrefixLength: u8, + pub ipv4Subnet: IN_ADDR, + pub ipv4Gateway: IN_ADDR, + pub ipv6Subnet: IN6_ADDR, + pub ipv6Gateway: IN6_ADDR, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of WslcNetworkSettings"][::std::mem::size_of::() - 56usize]; + ["Alignment of WslcNetworkSettings"][::std::mem::align_of::() - 8usize]; + ["Offset of field: WslcNetworkSettings::name"] + [::std::mem::offset_of!(WslcNetworkSettings, name) - 0usize]; + ["Offset of field: WslcNetworkSettings::flags"] + [::std::mem::offset_of!(WslcNetworkSettings, flags) - 8usize]; + ["Offset of field: WslcNetworkSettings::ipv4PrefixLength"] + [::std::mem::offset_of!(WslcNetworkSettings, ipv4PrefixLength) - 12usize]; + ["Offset of field: WslcNetworkSettings::ipv6PrefixLength"] + [::std::mem::offset_of!(WslcNetworkSettings, ipv6PrefixLength) - 13usize]; + ["Offset of field: WslcNetworkSettings::ipv4Subnet"] + [::std::mem::offset_of!(WslcNetworkSettings, ipv4Subnet) - 16usize]; + ["Offset of field: WslcNetworkSettings::ipv4Gateway"] + [::std::mem::offset_of!(WslcNetworkSettings, ipv4Gateway) - 20usize]; + ["Offset of field: WslcNetworkSettings::ipv6Subnet"] + [::std::mem::offset_of!(WslcNetworkSettings, ipv6Subnet) - 24usize]; + ["Offset of field: WslcNetworkSettings::ipv6Gateway"] + [::std::mem::offset_of!(WslcNetworkSettings, ipv6Gateway) - 40usize]; +}; impl WslcComponentFlags { pub const WSLC_COMPONENT_FLAG_NONE: WslcComponentFlags = WslcComponentFlags(0); pub const WSLC_COMPONENT_FLAG_VIRTUAL_MACHINE_PLATFORM: WslcComponentFlags = @@ -856,6 +1093,10 @@ pub struct WslcSdk { ) -> HRESULT, ::libloading::Error, >, + pub WslcSetSessionSettingsHostLoopback: Result< + unsafe extern "C" fn(sessionSettings: *mut WslcSessionSettings, hostName: PCSTR) -> HRESULT, + ::libloading::Error, + >, pub WslcGetSessionTerminationEvent: Result< unsafe extern "C" fn(session: WslcSession, terminationEvent: *mut HANDLE) -> HRESULT, ::libloading::Error, @@ -981,6 +1222,30 @@ pub struct WslcSdk { ) -> HRESULT, ::libloading::Error, >, + pub WslcSetContainerSettingsNetworks: Result< + unsafe extern "C" fn( + containerSettings: *mut WslcContainerSettings, + networks: *const WslcContainerNetwork, + networkCount: u32, + ) -> HRESULT, + ::libloading::Error, + >, + pub WslcSetContainerSettingsCapabilityAdditions: Result< + unsafe extern "C" fn( + containerSettings: *mut WslcContainerSettings, + capabilities: *const PCSTR, + capabilityCount: u32, + ) -> HRESULT, + ::libloading::Error, + >, + pub WslcSetContainerSettingsCapabilityDrops: Result< + unsafe extern "C" fn( + containerSettings: *mut WslcContainerSettings, + capabilities: *const PCSTR, + capabilityCount: u32, + ) -> HRESULT, + ::libloading::Error, + >, pub WslcCreateContainerProcess: Result< unsafe extern "C" fn( container: WslcContainer, @@ -1197,6 +1462,32 @@ pub struct WslcSdk { ) -> HRESULT, ::libloading::Error, >, + pub WslcListSessionNetworks: Result< + unsafe extern "C" fn( + session: WslcSession, + networks: *mut *mut WslcNetworkInfo, + count: *mut u32, + errorMessage: *mut PWSTR, + ) -> HRESULT, + ::libloading::Error, + >, + pub WslcCreateSessionNetwork: Result< + unsafe extern "C" fn( + session: WslcSession, + settings: *const WslcNetworkSettings, + networkInfo: *mut WslcNetworkInfo, + errorMessage: *mut PWSTR, + ) -> HRESULT, + ::libloading::Error, + >, + pub WslcDeleteSessionNetwork: Result< + unsafe extern "C" fn( + session: WslcSession, + nameOrId: PCSTR, + errorMessage: *mut PWSTR, + ) -> HRESULT, + ::libloading::Error, + >, pub WslcCreateSessionVhdVolume: Result< unsafe extern "C" fn( session: WslcSession, @@ -1259,6 +1550,9 @@ impl WslcSdk { let WslcSetSessionSettingsFeatureFlags = __library .get(b"WslcSetSessionSettingsFeatureFlags\0") .map(|sym| *sym); + let WslcSetSessionSettingsHostLoopback = __library + .get(b"WslcSetSessionSettingsHostLoopback\0") + .map(|sym| *sym); let WslcGetSessionTerminationEvent = __library .get(b"WslcGetSessionTerminationEvent\0") .map(|sym| *sym); @@ -1306,6 +1600,15 @@ impl WslcSdk { let WslcSetContainerSettingsNamedVolumes = __library .get(b"WslcSetContainerSettingsNamedVolumes\0") .map(|sym| *sym); + let WslcSetContainerSettingsNetworks = __library + .get(b"WslcSetContainerSettingsNetworks\0") + .map(|sym| *sym); + let WslcSetContainerSettingsCapabilityAdditions = __library + .get(b"WslcSetContainerSettingsCapabilityAdditions\0") + .map(|sym| *sym); + let WslcSetContainerSettingsCapabilityDrops = __library + .get(b"WslcSetContainerSettingsCapabilityDrops\0") + .map(|sym| *sym); let WslcCreateContainerProcess = __library .get(b"WslcCreateContainerProcess\0") .map(|sym| *sym); @@ -1358,6 +1661,9 @@ impl WslcSdk { let WslcPushSessionImage = __library.get(b"WslcPushSessionImage\0").map(|sym| *sym); let WslcSessionAuthenticate = __library.get(b"WslcSessionAuthenticate\0").map(|sym| *sym); let WslcListSessionImages = __library.get(b"WslcListSessionImages\0").map(|sym| *sym); + let WslcListSessionNetworks = __library.get(b"WslcListSessionNetworks\0").map(|sym| *sym); + let WslcCreateSessionNetwork = __library.get(b"WslcCreateSessionNetwork\0").map(|sym| *sym); + let WslcDeleteSessionNetwork = __library.get(b"WslcDeleteSessionNetwork\0").map(|sym| *sym); let WslcCreateSessionVhdVolume = __library .get(b"WslcCreateSessionVhdVolume\0") .map(|sym| *sym); @@ -1378,6 +1684,7 @@ impl WslcSdk { WslcSetSessionSettingsTimeout, WslcSetSessionSettingsVhd, WslcSetSessionSettingsFeatureFlags, + WslcSetSessionSettingsHostLoopback, WslcGetSessionTerminationEvent, WslcGetSessionTerminationReason, WslcTerminateSession, @@ -1397,6 +1704,9 @@ impl WslcSdk { WslcSetContainerSettingsPortMappings, WslcSetContainerSettingsVolumes, WslcSetContainerSettingsNamedVolumes, + WslcSetContainerSettingsNetworks, + WslcSetContainerSettingsCapabilityAdditions, + WslcSetContainerSettingsCapabilityDrops, WslcCreateContainerProcess, WslcReleaseContainer, WslcGetContainerID, @@ -1429,6 +1739,9 @@ impl WslcSdk { WslcPushSessionImage, WslcSessionAuthenticate, WslcListSessionImages, + WslcListSessionNetworks, + WslcCreateSessionNetwork, + WslcDeleteSessionNetwork, WslcCreateSessionVhdVolume, WslcDeleteSessionVhdVolume, WslcGetMissingComponents, @@ -1508,6 +1821,16 @@ impl WslcSdk { .as_ref() .expect("Expected function, got error."))(sessionSettings, flags) } + pub unsafe fn WslcSetSessionSettingsHostLoopback( + &self, + sessionSettings: *mut WslcSessionSettings, + hostName: PCSTR, + ) -> HRESULT { + (self + .WslcSetSessionSettingsHostLoopback + .as_ref() + .expect("Expected function, got error."))(sessionSettings, hostName) + } pub unsafe fn WslcGetSessionTerminationEvent( &self, session: WslcSession, @@ -1723,6 +2046,49 @@ impl WslcSdk { namedVolumeCount, ) } + pub unsafe fn WslcSetContainerSettingsNetworks( + &self, + containerSettings: *mut WslcContainerSettings, + networks: *const WslcContainerNetwork, + networkCount: u32, + ) -> HRESULT { + (self + .WslcSetContainerSettingsNetworks + .as_ref() + .expect("Expected function, got error."))( + containerSettings, networks, networkCount + ) + } + pub unsafe fn WslcSetContainerSettingsCapabilityAdditions( + &self, + containerSettings: *mut WslcContainerSettings, + capabilities: *const PCSTR, + capabilityCount: u32, + ) -> HRESULT { + (self + .WslcSetContainerSettingsCapabilityAdditions + .as_ref() + .expect("Expected function, got error."))( + containerSettings, + capabilities, + capabilityCount, + ) + } + pub unsafe fn WslcSetContainerSettingsCapabilityDrops( + &self, + containerSettings: *mut WslcContainerSettings, + capabilities: *const PCSTR, + capabilityCount: u32, + ) -> HRESULT { + (self + .WslcSetContainerSettingsCapabilityDrops + .as_ref() + .expect("Expected function, got error."))( + containerSettings, + capabilities, + capabilityCount, + ) + } pub unsafe fn WslcCreateContainerProcess( &self, container: WslcContainer, @@ -2091,6 +2457,43 @@ impl WslcSdk { .as_ref() .expect("Expected function, got error."))(session, images, count) } + pub unsafe fn WslcListSessionNetworks( + &self, + session: WslcSession, + networks: *mut *mut WslcNetworkInfo, + count: *mut u32, + errorMessage: *mut PWSTR, + ) -> HRESULT { + (self + .WslcListSessionNetworks + .as_ref() + .expect("Expected function, got error."))(session, networks, count, errorMessage) + } + pub unsafe fn WslcCreateSessionNetwork( + &self, + session: WslcSession, + settings: *const WslcNetworkSettings, + networkInfo: *mut WslcNetworkInfo, + errorMessage: *mut PWSTR, + ) -> HRESULT { + (self + .WslcCreateSessionNetwork + .as_ref() + .expect("Expected function, got error."))( + session, settings, networkInfo, errorMessage + ) + } + pub unsafe fn WslcDeleteSessionNetwork( + &self, + session: WslcSession, + nameOrId: PCSTR, + errorMessage: *mut PWSTR, + ) -> HRESULT { + (self + .WslcDeleteSessionNetwork + .as_ref() + .expect("Expected function, got error."))(session, nameOrId, errorMessage) + } pub unsafe fn WslcCreateSessionVhdVolume( &self, session: WslcSession, diff --git a/tests/configs/wslc_network_host_loopback.json b/tests/configs/wslc_network_host_loopback.json new file mode 100644 index 0000000000..3ddcba7590 --- /dev/null +++ b/tests/configs/wslc_network_host_loopback.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../schemas/stable/mxc-config.schema.1.0.0.json", + "version": "1.0.0", + "containerId": "wslc-network-host-loopback", + "containment": "wslc", + "process": { + "commandLine": "echo \"HOST_LOOPBACK_ADDR=[$(getent hosts host.wslc.internal | awk '{print $1}')]\"; BODY=$(wget -q -T 10 -O - http://host.wslc.internal:__HOST_LOOPBACK_PORT__/ 2>&1 || true); echo \"HOST_LOOPBACK_GOT=[$BODY]\"" + }, + "network": { + "egress": { "default": "allow" }, + "ingress": { "default": "allow", "hostLoopback": "allow" } + }, + "wslc": { + "image": "alpine:latest" + } +} diff --git a/tests/configs/wslc_network_host_loopback_isolated.json b/tests/configs/wslc_network_host_loopback_isolated.json new file mode 100644 index 0000000000..dcd25921ec --- /dev/null +++ b/tests/configs/wslc_network_host_loopback_isolated.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../schemas/stable/mxc-config.schema.1.0.0.json", + "version": "1.0.0", + "containerId": "wslc-network-host-loopback-isolated", + "containment": "wslc", + "process": { + "commandLine": "for TARGET in host.wslc.internal __HOST_LOOPBACK_ADDR__; do BODY=$(wget -q -T 5 -O - http://$TARGET:__HOST_LOOPBACK_PORT__/ 2>&1 || true); echo \"HOST_LOOPBACK_GOT $TARGET=[$BODY]\"; done; echo HOST_LOOPBACK_PROBE_DONE" + }, + "network": { + "egress": { "default": "deny" }, + "ingress": { "default": "deny", "hostLoopback": "deny" } + }, + "wslc": { + "image": "alpine:latest" + } +} diff --git a/tests/scripts/run_wslc_all_tests.ps1 b/tests/scripts/run_wslc_all_tests.ps1 index 9fc460b0c1..bcef9169f9 100644 --- a/tests/scripts/run_wslc_all_tests.ps1 +++ b/tests/scripts/run_wslc_all_tests.ps1 @@ -393,6 +393,18 @@ foreach ($proxyConfig in @("wslc_network_proxy.json", "wslc_network_proxy_inheri Reason = $(if ($proxyPass) { "" } else { "cooperative proxy test failed" }) }) } +# The listener runs on Windows with a per-run port and token, which a static +# fixture cannot assert, so the owning script drives it. +$hostLoopbackArgs = @{ WxcExecPath = $WxcExec } +if ($Debug) { $hostLoopbackArgs.Debug = $true } +& (Join-Path $PSScriptRoot "run_wslc_host_loopback_test.ps1") @hostLoopbackArgs +$hostLoopbackPass = ($LASTEXITCODE -eq 0) +$null = $results.Add(@{ + Name = "wslc_network_host_loopback{,_isolated}.json" + Pass = $hostLoopbackPass + Skipped = $false + Reason = $(if ($hostLoopbackPass) { "" } else { "host loopback test failed" }) +}) $null = $results.Add((Run-WslcTest "wslc_port_mapping_tcp.json" -OutputContains "PORT_MAPPING_TCP_OK")) $null = $results.Add((Run-WslcTest "wslc_port_mapping_multiple.json" -OutputContains "PORT_MAPPING_MULTI_OK")) diff --git a/tests/scripts/run_wslc_host_loopback_test.ps1 b/tests/scripts/run_wslc_host_loopback_test.ps1 new file mode 100644 index 0000000000..4c229ea469 --- /dev/null +++ b/tests/scripts/run_wslc_host_loopback_test.ps1 @@ -0,0 +1,96 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# WSLC host loopback functional test. +# +# MXC enables host.wslc.internal on every session. A bridged +# container reaches a service bound only to Windows 127.0.0.1 through it; an +# isolated container in such a session reaches it neither by name nor by the +# address the name resolves to. The listener answers with a per-run token, so +# the token in a container's output proves it reached this listener. +# +# Usage: +# .\run_wslc_host_loopback_test.ps1 -WxcExecPath [-Debug] + +param( + [switch]$Debug, + [string]$WxcExecPath +) + +$ErrorActionPreference = "Stop" +if (-not $WxcExecPath -or -not (Test-Path $WxcExecPath)) { + Write-Host "ERROR: pass -WxcExecPath with an existing wxc-exec.exe" -ForegroundColor Red + exit 1 +} +$RepoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +. (Join-Path $PSScriptRoot "wslc_loopback_token_server.ps1") + +function Invoke-HostLoopbackFixture { + param([string]$Name, [hashtable]$Values) + + Write-Host "Running $Name..." + $text = Get-Content (Join-Path $RepoRoot "tests\configs\$Name") -Raw + foreach ($key in $Values.Keys) { $text = $text.Replace($key, $Values[$key]) } + $configPath = Join-Path ([IO.Path]::GetTempPath()) "$([IO.Path]::GetFileNameWithoutExtension($Name))_$PID.json" + Set-Content -Path $configPath -Value $text -NoNewline + + $wxcArgs = @() + if ($Debug) { $wxcArgs += "--debug" } + $wxcArgs += $configPath + + $prev = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $output = & $WxcExecPath @wxcArgs 2>&1 | Out-String + $exitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $prev + Remove-Item $configPath -ErrorAction SilentlyContinue + } + Write-Host $output + @{ Output = $output; ExitCode = $exitCode } +} + +$failed = $false +$server = Start-LoopbackTokenServer +try { + $port = "$($server.Port)" + $tokenPattern = [regex]::Escape($server.Token) + Write-Host "Token listener on 127.0.0.1:$port answers with $($server.Token)" + + $bridged = Invoke-HostLoopbackFixture "wslc_network_host_loopback.json" @{ "__HOST_LOOPBACK_PORT__" = $port } + $address = if ($bridged.Output -match 'HOST_LOOPBACK_ADDR=\[([0-9.]+)\]') { $Matches[1] } + if ($bridged.ExitCode -ne 0) { + Write-Host "FAIL: bridged wxc-exec returned non-zero exit $($bridged.ExitCode)" -ForegroundColor Red + $failed = $true + } elseif ($bridged.Output -notmatch [regex]::Escape("HOST_LOOPBACK_GOT=[$($server.Token)]")) { + Write-Host "FAIL: bridged container did not reach the Windows 127.0.0.1:$port listener through host.wslc.internal" -ForegroundColor Red + $failed = $true + } elseif (-not $address) { + Write-Host "FAIL: bridged container did not report the address host.wslc.internal resolves to" -ForegroundColor Red + $failed = $true + } else { + Write-Host "PASS: bridged container reached a Windows loopback service through host.wslc.internal ($address)." -ForegroundColor Green + } + + if ($address) { + $isolated = Invoke-HostLoopbackFixture "wslc_network_host_loopback_isolated.json" @{ + "__HOST_LOOPBACK_PORT__" = $port + "__HOST_LOOPBACK_ADDR__" = $address + } + if ($isolated.ExitCode -ne 0 -or $isolated.Output -notmatch "HOST_LOOPBACK_PROBE_DONE") { + Write-Host "FAIL: isolated probe did not complete (exit $($isolated.ExitCode))" -ForegroundColor Red + $failed = $true + } elseif ($isolated.Output -match $tokenPattern) { + Write-Host "FAIL: isolated container reached the Windows 127.0.0.1:$port listener" -ForegroundColor Red + $failed = $true + } else { + Write-Host "PASS: isolated container reached Windows loopback neither by name nor by $address." -ForegroundColor Green + } + } +} finally { + Stop-LoopbackTokenServer $server +} + +if ($failed) { exit 1 } +exit 0 diff --git a/tests/scripts/run_wslc_state_aware_tests.ps1 b/tests/scripts/run_wslc_state_aware_tests.ps1 index 5940985cab..b52288a776 100644 --- a/tests/scripts/run_wslc_state_aware_tests.ps1 +++ b/tests/scripts/run_wslc_state_aware_tests.ps1 @@ -73,6 +73,7 @@ param( $ErrorActionPreference = "Stop" $RepoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +. (Join-Path $PSScriptRoot "wslc_loopback_token_server.ps1") # ---------------- Locate wxc-exec.exe + daemon ---------------- @@ -1177,9 +1178,12 @@ try { # exec injects HTTP(S)_PROXY from runtimeConfig.networkProxy without restating # the immutable network posture. Asserts the # proxy env reaches the container (the full functional proxy round-trip is -# covered by the one-shot run_wslc_proxy_test.ps1). +# covered by the one-shot run_wslc_proxy_test.ps1). An isolated peer started +# in the same session checks that host loopback stays bridged-only. $script:netSandboxId = $null +$script:peerSandboxId = $null $netDeprovisionedOk = $false +$peerDeprovisionedOk = $false try { $netProvisionedOk = Run-StateAwareTest "C: provision (bridged network)" { $r = Invoke-StateAware -ConfigFile 'wslc_state_aware_provision_bridged.json' @@ -1204,6 +1208,67 @@ try { Assert-True ($r.Stdout -match 'https_proxy=\[http://127\.0\.0\.1:8888\]') ` "https_proxy injected into the container" } | Out-Null + + $peerStartedOk = Run-StateAwareTest "C: start an isolated peer in the same session" { + $r = Invoke-StateAware -ConfigFile 'wslc_state_aware_provision.json' + $envObj = Assert-ResultEnvelope $r "isolated peer provision" + if ($null -eq $envObj) { return } + $script:peerSandboxId = [string]$envObj.result.sandboxId + $r = Invoke-StateAware -ConfigFile 'wslc_state_aware_start.json' -SandboxId $script:peerSandboxId + if ($null -eq (Assert-ResultEnvelope $r "isolated peer start")) { return } + $netBootId = Get-SandboxBootId -SandboxId $script:netSandboxId -What 'bridged sandbox' + $peerBootId = Get-SandboxBootId -SandboxId $script:peerSandboxId -What 'isolated peer' + Assert-True (-not [string]::IsNullOrEmpty($netBootId) -and $netBootId -ceq $peerBootId) ` + "both sandboxes run in the same session VM" + } + + # The session enables host loopback, so the bridged container reaches a + # service bound only to Windows 127.0.0.1. The isolated peer must not, + # by name or by the resolved address, while the bridged one still does. + Run-StateAwareTest "C: only the bridged sandbox reaches Windows loopback" { + $server = Start-LoopbackTokenServer + try { + $bridgedProbe = @{ + phase = 'exec' + sandboxId = $script:netSandboxId + process = @{ + commandLine = "sh -c 'set -- `$(getent hosts host.wslc.internal); echo HOST_LOOPBACK_ADDR=[`$1]; echo HOST_LOOPBACK_GOT=[`$(wget -q -T 10 -O - http://host.wslc.internal:$($server.Port)/ 2>&1)]'" + timeout = 60000 + } + } + $r = Invoke-StateAware -Request $bridgedProbe + Assert-True ($r.ExitCode -eq 0) "exit code = 0" + Assert-True ($r.Stdout.Contains("HOST_LOOPBACK_GOT=[$($server.Token)]")) ` + "bridged container received the Windows listener's token ($($r.Stdout.Trim()))" + $addr = if ($r.Stdout -match 'HOST_LOOPBACK_ADDR=\[([0-9.]+)\]') { $Matches[1] } + Assert-True ([bool]$addr) "host.wslc.internal resolved to an address" + if (-not $peerStartedOk -or -not $addr) { return } + + $r = Invoke-StateAware -Request @{ + phase = 'exec' + sandboxId = $script:peerSandboxId + process = @{ + commandLine = "sh -c 'for T in host.wslc.internal $addr; do echo HOST_LOOPBACK_GOT `$T=[`$(wget -q -T 5 -O - http://`$T:$($server.Port)/ 2>&1)]; done; echo HOST_LOOPBACK_PROBE_DONE'" + timeout = 60000 + } + } + Assert-True ($r.Stdout -match 'HOST_LOOPBACK_PROBE_DONE') "isolated probe completed ($($r.Stdout.Trim()))" + Assert-True (-not $r.Stdout.Contains($server.Token)) "isolated peer did not receive the token" + + $r = Invoke-StateAware -Request $bridgedProbe + Assert-True ($r.Stdout.Contains("HOST_LOOPBACK_GOT=[$($server.Token)]")) ` + "bridged container still receives the token ($($r.Stdout.Trim()))" + } finally { + Stop-LoopbackTokenServer $server + } + } | Out-Null + } + + if ($script:peerSandboxId) { + $peerDeprovisionedOk = Run-StateAwareTest "C: deprovision isolated peer" { + $r = Invoke-StateAware -ConfigFile 'wslc_state_aware_deprovision.json' -SandboxId $script:peerSandboxId + $null = Assert-ResultEnvelope $r "isolated peer deprovision" + } } if ($netProvisionedOk) { @@ -1218,6 +1283,11 @@ try { if ($netDeprovPassed) { $netDeprovisionedOk = $true } } } finally { + if ($script:peerSandboxId -and -not $peerDeprovisionedOk) { + Write-Host "" + Write-Host "[cleanup] best-effort deprovision of $script:peerSandboxId" -ForegroundColor DarkGray + try { $null = Invoke-StateAware -ConfigFile 'wslc_state_aware_deprovision.json' -SandboxId $script:peerSandboxId } catch { } + } if ($null -ne $script:netSandboxId -and -not $netDeprovisionedOk) { Write-Host "" Write-Host "[cleanup] best-effort deprovision of $script:netSandboxId" -ForegroundColor DarkGray diff --git a/tests/scripts/wslc_loopback_token_server.ps1 b/tests/scripts/wslc_loopback_token_server.ps1 new file mode 100644 index 0000000000..28baef554f --- /dev/null +++ b/tests/scripts/wslc_loopback_token_server.ps1 @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# HTTP listener bound only to Windows 127.0.0.1 that answers every request with +# a per-run token. Dot-source this file, then pair Start- with Stop-. + +function Start-LoopbackTokenServer { + $token = "HOST_LOOPBACK_" + [guid]::NewGuid().ToString("N") + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + $listener.Start() + + $shell = [powershell]::Create().AddScript({ + param($listener, $token) + $body = [Text.Encoding]::ASCII.GetBytes($token) + $header = [Text.Encoding]::ASCII.GetBytes( + "HTTP/1.0 200 OK`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n") + try { + while ($true) { + $client = $listener.AcceptTcpClient() + try { + $stream = $client.GetStream() + $stream.ReadTimeout = 5000 + $buffer = New-Object byte[] 4096 + $null = $stream.Read($buffer, 0, $buffer.Length) + $stream.Write($header, 0, $header.Length) + $stream.Write($body, 0, $body.Length) + } catch { + } finally { + $client.Close() + } + } + } catch { + # AcceptTcpClient throws once the listener stops. + } + }).AddArgument($listener).AddArgument($token) + + @{ + Port = $listener.LocalEndpoint.Port + Token = $token + Listener = $listener + Shell = $shell + Run = $shell.BeginInvoke() + } +} + +function Stop-LoopbackTokenServer { + param($Server) + if (-not $Server) { return } + $Server.Listener.Stop() + $null = $Server.Run.AsyncWaitHandle.WaitOne(5000) + $Server.Shell.Dispose() +}