Add container:<name id> network mode for WSLC containers (#40502)

beena352 committed May 15, 2026 at 15:57 UTC 104e05124ee7235e09b8e60955d4186305853168
3 files changed +248 -22
localization/strings/en-US/Resources.resw
+13
@@ -2111,6 +2111,19 @@ Usage:
2111 <value>Container '{}' is running.</value>
2112 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2113 </data>
2114 + <data name="MessageWslcContainerModeNoAdditionalNetworks" xml:space="preserve">
2115 + <value>Additional networks are not allowed with container network mode.</value>
2116 + </data>
2117 + <data name="MessageWslcContainerModeNoPorts" xml:space="preserve">
2118 + <value>Port mappings are not supported with container network mode; ports are owned by the target container.</value>
2119 + </data>
2120 + <data name="MessageWslcContainerModeRequiresTarget" xml:space="preserve">
2121 + <value>Target container name is required for container network mode.</value>
2122 + </data>
2123 + <data name="MessageWslcContainerModeTargetNotFound" xml:space="preserve">
2124 + <value>Target container '{}' not found.</value>
2125 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2126 + </data>
2127 <data name="MessageWslcContainerNotFound" xml:space="preserve">
2128 <value>Container '{}' not found.</value>
2129 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/wslcsession/WSLCContainer.cpp
+71 -21
@@ -32,6 +32,8 @@ using wsl::windows::common::io::OverlappedIOHandle;
32 using wsl::windows::common::io::ReadHandle;
33 using wsl::windows::common::io::RelayHandle;
34 using wsl::windows::service::wslc::ContainerPortMapping;
35 +using wsl::windows::service::wslc::DockerHTTPClient;
36 +using wsl::windows::service::wslc::DockerHTTPException;
37 using wsl::windows::service::wslc::IWSLCVolume;
38 using wsl::windows::service::wslc::NetworkEntry;
39 using wsl::windows::service::wslc::RelayedProcessIO;
@@ -147,13 +149,21 @@ uint16_t AllocateEphemeralPort(int family, const char* address)
149 return port;
150 }
151
152 +constexpr std::string_view c_containerNetworkPrefix = "container:";
153 +
154 +bool IsContainerNetworkMode(LPCSTR name)
155 +{
156 + return name != nullptr && std::string_view(name).starts_with(c_containerNetworkPrefix);
157 +}
158 +
159 // Builds port mapping list from container options and returns the network mode string.
160 std::pair<std::vector<ContainerPortMapping>, std::string> ProcessPortMappings(
161 std::vector<_WSLCPortMapping>& requestedPorts,
162 WSLCContainerNetworkType networkType,
163 WSLCVirtualMachine& virtualMachine,
164 const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
156 - LPCSTR containerNetworkName)
165 + LPCSTR containerNetworkName,
166 + DockerHTTPClient& dockerClient)
167 {
168 THROW_HR_IF_MSG(
169 E_INVALIDARG,
@@ -179,10 +189,32 @@ std::pair<std::vector<ContainerPortMapping>, std::string> ProcessPortMappings(
189 THROW_HR_WITH_USER_ERROR_IF(
190 E_INVALIDARG, Localization::MessageWslcContainerNetworkNameRequired(), !containerNetworkName || strlen(containerNetworkName) == 0);
191
182 - THROW_HR_WITH_USER_ERROR_IF(
183 - WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(containerNetworkName), !sessionNetworks.contains(containerNetworkName));
192 + if (IsContainerNetworkMode(containerNetworkName))
193 + {
194 + auto target = std::string_view(containerNetworkName).substr(c_containerNetworkPrefix.size());
195 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcContainerModeRequiresTarget(), target.empty());
196 +
197 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcContainerModeNoPorts(), !requestedPorts.empty());
198
185 - networkMode = containerNetworkName;
199 + try
200 + {
201 + auto targetInspect = dockerClient.InspectContainer(std::string(target));
202 + networkMode = std::format("container:{}", targetInspect.Id);
203 + }
204 + catch (const DockerHTTPException& e)
205 + {
206 + THROW_HR_WITH_USER_ERROR_IF(
207 + WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerModeTargetNotFound(std::string(target)), e.StatusCode() == 404);
208 + throw;
209 + }
210 + }
211 + else
212 + {
213 + THROW_HR_WITH_USER_ERROR_IF(
214 + WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(containerNetworkName), !sessionNetworks.contains(containerNetworkName));
215 +
216 + networkMode = containerNetworkName;
217 + }
218 }
219 else
220 {
@@ -208,7 +240,8 @@ std::pair<std::vector<ContainerPortMapping>, std::string> ProcessPortMappings(
240 auto& entry = ports.emplace_back(VMPortMapping::FromWSLCPortMapping(e), e.ContainerPort);
241
242 // Allocate VM ports for bridged and custom networks. Host mode ports are allocated when the container starts.
211 - if (networkType == WSLCContainerNetworkTypeBridged || networkType == WSLCContainerNetworkTypeCustom)
243 + if (networkType == WSLCContainerNetworkTypeBridged ||
244 + (networkType == WSLCContainerNetworkTypeCustom && !IsContainerNetworkMode(containerNetworkName)))
245 {
246 entry.VmMapping.AssignVmPort(virtualMachine.AllocatePort(e.Family, e.Protocol));
247 }
@@ -275,32 +308,45 @@ WSLCContainerState DockerStateToWSLCState(ContainerState state)
308 }
309 }
310
278 -WSLCContainerNetworkType DockerNetworkModeToWSLCNetworkType(const std::string& mode)
311 +struct DockerNetworkMode
312 +{
313 + WSLCContainerNetworkType Type;
314 + std::optional<std::string> TargetId;
315 +};
316 +
317 +DockerNetworkMode ParseDockerNetworkMode(const std::string& mode)
318 {
319 if (mode == "bridge")
320 {
282 - return WSLCContainerNetworkTypeBridged;
321 + return {WSLCContainerNetworkTypeBridged, {}};
322 }
323 else if (mode == "host")
324 {
286 - return WSLCContainerNetworkTypeHost;
325 + return {WSLCContainerNetworkTypeHost, {}};
326 }
327 else if (mode == "none")
328 {
290 - return WSLCContainerNetworkTypeNone;
329 + return {WSLCContainerNetworkTypeNone, {}};
330 }
331
332 // Docker treats empty NetworkMode as the default (bridged).
333 if (mode.empty())
334 {
296 - return WSLCContainerNetworkTypeBridged;
335 + return {WSLCContainerNetworkTypeBridged, {}};
336 + }
337 +
338 + if (mode.starts_with(c_containerNetworkPrefix))
339 + {
340 + auto target = mode.substr(c_containerNetworkPrefix.size());
341 + THROW_HR_IF_MSG(E_INVALIDARG, target.empty(), "Invalid Docker network mode: missing container id/name in '%hs'", mode.c_str());
342 + return {WSLCContainerNetworkTypeCustom, std::move(target)};
343 }
344
299 - // Reject Docker special syntaxes (container:<id>, service:<name>, etc.);
345 + // Reject other Docker special syntaxes (service:<name>, etc.);
346 // any plain name is treated as a user-defined custom network.
347 THROW_HR_IF_MSG(E_INVALIDARG, mode.find(':') != std::string::npos, "Unsupported Docker network mode: %hs", mode.c_str());
348
303 - return WSLCContainerNetworkTypeCustom;
349 + return {WSLCContainerNetworkTypeCustom, {}};
350 }
351
352 std::uint64_t ParseDockerTimestamp(const std::string& timestamp)
@@ -383,14 +429,13 @@ void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::wind
429
430 constexpr ULONG GetAdditionalStartIndex(WSLCContainerNetworkType type)
431 {
386 - // For Custom, Networks[0] is the primary user network; additionals start at 1.
387 - // For Bridged/Host/None, the primary mode is implicit, so all entries are additional.
388 - return type == WSLCContainerNetworkTypeCustom ? 1 : 0;
432 + return (type == WSLCContainerNetworkTypeCustom) ? 1 : 0;
433 }
434
391 -LPCSTR GetPrimaryCustomNetworkName(const WSLCContainerNetwork& network)
435 +LPCSTR GetPrimaryNetworkName(const WSLCContainerNetwork& network)
436 {
393 - if (network.ContainerNetworkType != WSLCContainerNetworkTypeCustom || network.NetworksCount == 0)
437 + const auto type = network.ContainerNetworkType;
438 + if (type != WSLCContainerNetworkTypeCustom || network.NetworksCount == 0)
439 {
440 return nullptr;
441 }
@@ -422,6 +467,9 @@ void ProcessAdditionalNetworks(
467 return;
468 }
469
470 + THROW_HR_WITH_USER_ERROR_IF(
471 + E_INVALIDARG, Localization::MessageWslcContainerModeNoAdditionalNetworks(), IsContainerNetworkMode(GetPrimaryNetworkName(network)));
472 +
473 THROW_HR_WITH_USER_ERROR_IF(
474 E_INVALIDARG,
475 Localization::MessageWslcAdditionalNetworksRequirePrimary(),
@@ -1574,7 +1622,8 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1622 containerOptions.ContainerNetwork.ContainerNetworkType,
1623 virtualMachine,
1624 sessionNetworks,
1577 - GetPrimaryCustomNetworkName(containerOptions.ContainerNetwork));
1625 + GetPrimaryNetworkName(containerOptions.ContainerNetwork),
1626 + DockerClient);
1627
1628 request.HostConfig.NetworkMode = networkMode;
1629
@@ -1717,14 +1766,15 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1766 auto metadata = ParseContainerMetadata(metadataIt->second.c_str());
1767 labels.erase(metadataIt);
1768
1720 - auto networkingMode = DockerNetworkModeToWSLCNetworkType(dockerContainer.HostConfig.NetworkMode);
1769 + auto networkMode = ParseDockerNetworkMode(dockerContainer.HostConfig.NetworkMode);
1770 // Re-register recovered VM ports in the allocation pool to prevent conflicts.
1771 std::vector<ContainerPortMapping> ports;
1772 for (const auto& e : metadata.Ports)
1773 {
1774 auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort});
1775
1727 - if (networkingMode == WSLCContainerNetworkTypeBridged || networkingMode == WSLCContainerNetworkTypeCustom)
1776 + if (networkMode.Type == WSLCContainerNetworkTypeBridged ||
1777 + (networkMode.Type == WSLCContainerNetworkTypeCustom && !networkMode.TargetId.has_value()))
1778 {
1779 auto allocation = virtualMachine.TryAllocatePort(e.VmPort, e.Family, e.Protocol);
1780
@@ -1745,7 +1795,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1795 std::string(dockerContainer.Id),
1796 std::move(name),
1797 std::string(dockerContainer.Image),
1748 - networkingMode,
1798 + networkMode.Type,
1799 std::move(metadata.Volumes),
1800 std::move(ports),
1801 std::move(labels),
test/windows/WSLCTests.cpp
+164 -1
@@ -5863,7 +5863,7 @@ class WSLCTests
5863 wil::com_ptr<IWSLCContainer> container;
5864 auto hr = m_defaultSession->CreateContainer(&options, &container);
5865 VERIFY_ARE_EQUAL(E_INVALIDARG, hr);
5866 - ValidateCOMErrorMessageContains(L"Container network name is required");
5866 + ValidateCOMErrorMessage(L"Container network name is required for custom network type.");
5867 }
5868
5869 WSLC_TEST_METHOD(ContainerCustomNetworkMultipleContainersTest)
@@ -6248,6 +6248,169 @@ class WSLCTests
6248 ValidateCOMErrorMessage(L"Container network name is required for custom network type.");
6249 }
6250
6251 + WSLC_TEST_METHOD(ContainerNetworkModeHappyPathTest)
6252 + {
6253 + // Start container A on the default (bridged) network, then start container B sharing A's
6254 + // network namespace. Verify the inspect round-trip returns the canonical container mode.
6255 + const std::string containerAName = "test-container-mode-a";
6256 + const std::string containerBName = "test-container-mode-b";
6257 +
6258 + WSLCContainerLauncher launcherA("debian:latest", containerAName, {"sleep", "99999"}, {});
6259 + auto containerA = launcherA.Launch(*m_defaultSession);
6260 + VERIFY_ARE_EQUAL(containerA.State(), WslcContainerStateRunning);
6261 +
6262 + const std::string containerAId = containerA.Id();
6263 +
6264 + WSLCContainerLauncher launcherB("debian:latest", containerBName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
6265 + launcherB.SetContainerNetworkName("container:" + containerAName);
6266 +
6267 + auto containerB = launcherB.Launch(*m_defaultSession);
6268 + VERIFY_ARE_EQUAL(containerB.State(), WslcContainerStateRunning);
6269 +
6270 + // Inspect B: NetworkMode must be "container:<A's canonical 64-char id>".
6271 + const std::string expectedNetworkMode = "container:" + containerAId;
6272 + VERIFY_ARE_EQUAL(containerB.Inspect().HostConfig.NetworkMode, expectedNetworkMode);
6273 + }
6274 +
6275 + WSLC_TEST_METHOD(ContainerNetworkModeMissingTargetRejectedTest)
6276 + {
6277 + // Container mode with an empty target name must be rejected before any Docker call.
6278 + LPCSTR args[] = {"sleep", "99999"};
6279 +
6280 + WSLCContainerOptions options{};
6281 + options.Image = "debian:latest";
6282 + options.Name = "test-container-mode-no-target";
6283 + options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
6284 + options.ContainerNetwork.ContainerNetworkType = WSLCContainerNetworkTypeCustom;
6285 + WSLCNetworkAttachment emptyTarget{};
6286 + emptyTarget.NetworkName = "container:";
6287 + options.ContainerNetwork.Networks = &emptyTarget;
6288 + options.ContainerNetwork.NetworksCount = 1;
6289 +
6290 + wil::com_ptr<IWSLCContainer> container;
6291 + auto hr = m_defaultSession->CreateContainer(&options, &container);
6292 + VERIFY_ARE_EQUAL(E_INVALIDARG, hr);
6293 + ValidateCOMErrorMessage(L"Target container name is required for container network mode.");
6294 + }
6295 +
6296 + WSLC_TEST_METHOD(ContainerNetworkModeTargetNotFoundTest)
6297 + {
6298 + // Container mode with a nonexistent target must return WSLC_E_CONTAINER_NOT_FOUND
6299 + // with a localized message naming the target.
6300 + const std::string targetName = "does-not-exist-container-target";
6301 +
6302 + WSLCContainerLauncher launcher(
6303 + "debian:latest", "test-container-mode-notfound", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
6304 + launcher.SetContainerNetworkName("container:" + targetName);
6305 +
6306 + auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
6307 + VERIFY_ARE_EQUAL(WSLC_E_CONTAINER_NOT_FOUND, retVal.first);
6308 + ValidateCOMErrorMessage(std::format(L"Target container '{}' not found.", targetName));
6309 + }
6310 +
6311 + WSLC_TEST_METHOD(ContainerNetworkModePortsRejectedTest)
6312 + {
6313 + // Container mode does not support port mappings — ports belong to the target container.
6314 + const std::string containerAName = "test-container-mode-ports-a";
6315 +
6316 + WSLCContainerLauncher launcherA("debian:latest", containerAName, {"sleep", "99999"}, {});
6317 + auto containerA = launcherA.Launch(*m_defaultSession);
6318 + VERIFY_ARE_EQUAL(containerA.State(), WslcContainerStateRunning);
6319 +
6320 + WSLCContainerLauncher launcherB(
6321 + "debian:latest", "test-container-mode-ports-b", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
6322 + launcherB.SetContainerNetworkName("container:" + containerAName);
6323 + launcherB.AddPort(8080, 80, AF_INET);
6324 +
6325 + auto retVal = launcherB.LaunchNoThrow(*m_defaultSession);
6326 + VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
6327 + ValidateCOMErrorMessage(
6328 + L"Port mappings are not supported with container network mode; ports are owned by the target container.");
6329 + }
6330 +
6331 + WSLC_TEST_METHOD(ContainerNetworkModeAdditionalNetworkRejectedTest)
6332 + {
6333 + // Container mode does not support additional networks — the target owns the netns.
6334 + const std::string containerAName = "test-container-mode-addnet-a";
6335 +
6336 + WSLCContainerLauncher launcherA("debian:latest", containerAName, {"sleep", "99999"}, {});
6337 + auto containerA = launcherA.Launch(*m_defaultSession);
6338 + VERIFY_ARE_EQUAL(containerA.State(), WslcContainerStateRunning);
6339 +
6340 + WSLCContainerLauncher launcherB(
6341 + "debian:latest", "test-container-mode-addnet-b", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
6342 + launcherB.SetContainerNetworkName("container:" + containerAName);
6343 + launcherB.AddAdditionalNetwork("bridge");
6344 +
6345 + auto retVal = launcherB.LaunchNoThrow(*m_defaultSession);
6346 + VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
6347 + ValidateCOMErrorMessage(L"Additional networks are not allowed with container network mode.");
6348 + }
6349 +
6350 + WSLC_TEST_METHOD(ContainerNetworkModeInspectRoundTripTest)
6351 + {
6352 + // Verify that after a session reset (service restart), Inspect() on a recovered
6353 + // container-mode container still returns the correct "container:<id>" NetworkMode.
6354 + const std::string containerAName = "test-container-mode-rt-a";
6355 + const std::string containerBName = "test-container-mode-rt-b";
6356 +
6357 + std::string containerAId;
6358 +
6359 + {
6360 + WSLCContainerLauncher launcherA("debian:latest", containerAName, {"sleep", "99999"}, {});
6361 + auto containerA = launcherA.Launch(*m_defaultSession);
6362 + VERIFY_ARE_EQUAL(containerA.State(), WslcContainerStateRunning);
6363 + containerAId = containerA.Id();
6364 + containerA.SetDeleteOnClose(false);
6365 +
6366 + WSLCContainerLauncher launcherB(
6367 + "debian:latest", containerBName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
6368 + launcherB.SetContainerNetworkName("container:" + containerAName);
6369 + auto containerB = launcherB.Create(*m_defaultSession);
6370 + VERIFY_ARE_EQUAL(containerB.State(), WslcContainerStateCreated);
6371 + containerB.SetDeleteOnClose(false);
6372 + }
6373 +
6374 + // Simulate service restart — Open() path reconstructs container from Docker state.
6375 + ResetTestSession();
6376 +
6377 + auto recoveredContainerA = OpenContainer(m_defaultSession.get(), containerAName);
6378 + auto recoveredContainerB = OpenContainer(m_defaultSession.get(), containerBName);
6379 + VERIFY_ARE_EQUAL(recoveredContainerB.State(), WslcContainerStateCreated);
6380 +
6381 + const std::string expectedNetworkMode = "container:" + containerAId;
6382 + VERIFY_ARE_EQUAL(recoveredContainerB.Inspect().HostConfig.NetworkMode, expectedNetworkMode);
6383 + }
6384 +
6385 + WSLC_TEST_METHOD(ContainerNetworkModeIpAddressRejectedTest)
6386 + {
6387 + // ContainerIpAddress must be rejected for container network mode just as for other modes.
6388 + const std::string containerAName = "test-container-mode-ip-a";
6389 +
6390 + WSLCContainerLauncher launcherA("debian:latest", containerAName, {"sleep", "99999"}, {});
6391 + auto containerA = launcherA.Launch(*m_defaultSession);
6392 + VERIFY_ARE_EQUAL(containerA.State(), WslcContainerStateRunning);
6393 +
6394 + LPCSTR args[] = {"sleep", "99999"};
6395 +
6396 + WSLCContainerOptions options{};
6397 + options.Image = "debian:latest";
6398 + options.Name = "test-container-mode-ip-b";
6399 + options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
6400 + options.ContainerNetwork.ContainerNetworkType = WSLCContainerNetworkTypeCustom;
6401 + const std::string containerNetName = "container:" + containerAName;
6402 + WSLCNetworkAttachment netWithIp{};
6403 + netWithIp.NetworkName = containerNetName.c_str();
6404 + netWithIp.ContainerIpAddress = "10.0.0.5";
6405 + options.ContainerNetwork.Networks = &netWithIp;
6406 + options.ContainerNetwork.NetworksCount = 1;
6407 +
6408 + wil::com_ptr<IWSLCContainer> container;
6409 + auto hr = m_defaultSession->CreateContainer(&options, &container);
6410 + VERIFY_ARE_EQUAL(E_NOTIMPL, hr);
6411 + ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported.");
6412 + }
6413 +
6414 WSLC_TEST_METHOD(ContainerInspect)
6415 {
6416 // Helper to verify port mappings.