Add WSLCContainerNetworkTypeCustom support (#40311)

beena352 committed May 4, 2026 at 13:13 UTC 3f1007245a1fc292360f49b8ead8ba61431d4479
8 files changed +259 -9
localization/strings/en-US/Resources.resw
+3
@@ -2026,6 +2026,9 @@ Usage:
2026 <value>Container '{}' not found.</value>
2027 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2028 </data>
2029 + <data name="MessageWslcContainerNetworkNameRequired" xml:space="preserve">
2030 + <value>Container network name is required for custom network type.</value>
2031 + </data>
2032 <data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2033 <value>Unrecognized command: '{}'</value>
2034 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/common/WSLCContainerLauncher.cpp
+6
@@ -139,6 +139,11 @@ void WSLCContainerLauncher::SetContainerFlags(WSLCContainerFlags Flags)
139 m_containerFlags = Flags;
140 }
141
142 +void WSLCContainerLauncher::SetContainerNetworkName(std::string&& Name)
143 +{
144 + m_containerNetworkName = std::move(Name);
145 +}
146 +
147 void WSLCContainerLauncher::SetHostname(std::string&& Hostname)
148 {
149 m_hostname = std::move(Hostname);
@@ -250,6 +255,7 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
255 auto [processOptions, commandLinePtrs, environmentPtrs] = CreateProcessOptions();
256 options.InitProcessOptions = processOptions;
257 options.ContainerNetwork.ContainerNetworkType = m_containerNetworkType;
258 + options.ContainerNetwork.ContainerNetworkName = m_containerNetworkName.empty() ? nullptr : m_containerNetworkName.c_str();
259 options.Ports = m_ports.data();
260 options.PortsCount = static_cast<ULONG>(m_ports.size());
261 options.StopSignal = m_stopSignal;
src/windows/common/WSLCContainerLauncher.h
+2
@@ -73,6 +73,7 @@ public:
73 void SetEntrypoint(std::vector<std::string>&& entrypoint);
74 void SetDefaultStopSignal(WSLCSignal Signal);
75 void SetContainerFlags(WSLCContainerFlags Flags);
76 + void SetContainerNetworkName(std::string&& Name);
77 void SetHostname(std::string&& Hostname);
78 void SetDomainname(std::string&& Domainame);
79 void SetDnsServers(std::vector<std::string>&& DnsServers);
@@ -93,6 +94,7 @@ private:
94 std::deque<std::string> m_volumeNames;
95 std::deque<std::string> m_containerPaths;
96 WSLCContainerNetworkType m_containerNetworkType;
97 + std::string m_containerNetworkName;
98 std::vector<std::string> m_entrypoint;
99 WSLCSignal m_stopSignal = WSLCSignalNone;
100 WSLCContainerFlags m_containerFlags = WSLCContainerFlagsNone;
src/windows/service/inc/wslc.idl
+1 -1
@@ -231,7 +231,7 @@ typedef enum _WSLCContainerNetworkType
231 WSLCContainerNetworkTypeNone = 0,
232 WSLCContainerNetworkTypeHost = 1,
233 WSLCContainerNetworkTypeBridged = 2,
234 - // WSLCContainerNetworkTypeCustom = 3 // TODO: Implement when implementing custom networks
234 + WSLCContainerNetworkTypeCustom = 3
235 } WSLCContainerNetworkType;
236
237 typedef struct _WSLCContainerNetwork
src/windows/wslcsession/WSLCContainer.cpp
+42 -6
@@ -32,6 +32,7 @@ using wsl::windows::common::relay::ReadHandle;
32 using wsl::windows::common::relay::RelayHandle;
33 using wsl::windows::service::wslc::ContainerPortMapping;
34 using wsl::windows::service::wslc::IWSLCVolume;
35 +using wsl::windows::service::wslc::NetworkEntry;
36 using wsl::windows::service::wslc::RelayedProcessIO;
37 using wsl::windows::service::wslc::TypedHandle;
38 using wsl::windows::service::wslc::unique_com_disconnect;
@@ -147,8 +148,17 @@ uint16_t AllocateEphemeralPort(int family, const char* address)
148
149 // Builds port mapping list from container options and returns the network mode string.
150 std::pair<std::vector<ContainerPortMapping>, std::string> ProcessPortMappings(
150 - std::vector<_WSLCPortMapping>& requestedPorts, WSLCContainerNetworkType networkType, WSLCVirtualMachine& virtualMachine)
151 + std::vector<_WSLCPortMapping>& requestedPorts,
152 + WSLCContainerNetworkType networkType,
153 + WSLCVirtualMachine& virtualMachine,
154 + const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
155 + LPCSTR containerNetworkName)
156 {
157 + THROW_HR_IF_MSG(
158 + E_INVALIDARG,
159 + containerNetworkName != nullptr && networkType != WSLCContainerNetworkTypeCustom,
160 + "ContainerNetworkName must be null when network type is not Custom");
161 +
162 // Determine network mode string.
163 std::string networkMode;
164 if (networkType == WSLCContainerNetworkTypeBridged)
@@ -163,6 +173,16 @@ std::pair<std::vector<ContainerPortMapping>, std::string> ProcessPortMappings(
173 {
174 networkMode = "none";
175 }
176 + else if (networkType == WSLCContainerNetworkTypeCustom)
177 + {
178 + THROW_HR_WITH_USER_ERROR_IF(
179 + E_INVALIDARG, Localization::MessageWslcContainerNetworkNameRequired(), !containerNetworkName || strlen(containerNetworkName) == 0);
180 +
181 + THROW_HR_WITH_USER_ERROR_IF(
182 + WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(containerNetworkName), !sessionNetworks.contains(containerNetworkName));
183 +
184 + networkMode = containerNetworkName;
185 + }
186 else
187 {
188 THROW_HR_MSG(E_INVALIDARG, "Invalid networking mode: %i", networkType);
@@ -186,8 +206,8 @@ std::pair<std::vector<ContainerPortMapping>, std::string> ProcessPortMappings(
206
207 auto& entry = ports.emplace_back(VMPortMapping::FromWSLCPortMapping(e), e.ContainerPort);
208
189 - // Only allocate port for bridged network. Host mode ports are allocated when the container starts.
190 - if (networkType == WSLCContainerNetworkTypeBridged)
209 + // Allocate VM ports for bridged and custom networks. Host mode ports are allocated when the container starts.
210 + if (networkType == WSLCContainerNetworkTypeBridged || networkType == WSLCContainerNetworkTypeCustom)
211 {
212 entry.VmMapping.AssignVmPort(virtualMachine.AllocatePort(e.Family, e.Protocol));
213 }
@@ -269,7 +289,17 @@ WSLCContainerNetworkType DockerNetworkModeToWSLCNetworkType(const std::string& m
289 return WSLCContainerNetworkTypeNone;
290 }
291
272 - THROW_HR_MSG(E_INVALIDARG, "Invalid networking mode: %hs", mode.c_str());
292 + // Docker treats empty NetworkMode as the default (bridged).
293 + if (mode.empty())
294 + {
295 + return WSLCContainerNetworkTypeBridged;
296 + }
297 +
298 + // Reject Docker special syntaxes (container:<id>, service:<name>, etc.);
299 + // any plain name is treated as a user-defined custom network.
300 + THROW_HR_IF_MSG(E_INVALIDARG, mode.find(':') != std::string::npos, "Unsupported Docker network mode: %hs", mode.c_str());
301 +
302 + return WSLCContainerNetworkTypeCustom;
303 }
304
305 std::uint64_t ParseDockerTimestamp(const std::string& timestamp)
@@ -1170,6 +1200,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1200 WSLCSession& wslcSession,
1201 WSLCVirtualMachine& virtualMachine,
1202 const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
1203 + const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
1204 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1205 ContainerEventTracker& EventTracker,
1206 DockerHTTPClient& DockerClient,
@@ -1411,7 +1442,12 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1442 }
1443
1444 // Process port mappings from container options.
1414 - auto [mappedPorts, networkMode] = ProcessPortMappings(ports, containerOptions.ContainerNetwork.ContainerNetworkType, virtualMachine);
1445 + auto [mappedPorts, networkMode] = ProcessPortMappings(
1446 + ports,
1447 + containerOptions.ContainerNetwork.ContainerNetworkType,
1448 + virtualMachine,
1449 + sessionNetworks,
1450 + containerOptions.ContainerNetwork.ContainerNetworkName);
1451
1452 request.HostConfig.NetworkMode = networkMode;
1453
@@ -1519,7 +1555,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1555 {
1556 auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort});
1557
1522 - if (networkingMode == WSLCContainerNetworkTypeBridged)
1558 + if (networkingMode == WSLCContainerNetworkTypeBridged || networkingMode == WSLCContainerNetworkTypeCustom)
1559 {
1560 auto allocation = virtualMachine.TryAllocatePort(e.VmPort, e.Family, e.Protocol);
1561
src/windows/wslcsession/WSLCContainer.h
+2
@@ -23,6 +23,7 @@ Abstract:
23 #include "COMImplClass.h"
24 #include "wslc_schema.h"
25 #include "WSLCContainerMetadata.h"
26 +#include "WSLCNetworkMetadata.h"
27 #include "WSLCVhdVolume.h"
28 #include <unordered_map>
29
@@ -127,6 +128,7 @@ public:
128 WSLCSession& wslcSession,
129 WSLCVirtualMachine& virtualMachine,
130 const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& SessionVolumes,
131 + const std::unordered_map<std::string, NetworkEntry>& SessionNetworks,
132 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
133 ContainerEventTracker& EventTracker,
134 DockerHTTPClient& DockerClient,
src/windows/wslcsession/WSLCSession.cpp
+4 -2
@@ -1624,13 +1624,14 @@ try
1624
1625 try
1626 {
1627 - std::scoped_lock lock(m_containersLock, m_volumesLock);
1627 + std::scoped_lock lock(m_containersLock, m_volumesLock, m_networksLock);
1628
1629 auto& it = m_containers.emplace_back(WSLCContainerImpl::Create(
1630 *containerOptions,
1631 *this,
1632 m_virtualMachine.value(),
1633 m_volumes,
1634 + m_networks,
1635 std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
1636 m_eventTracker.value(),
1637 m_dockerClient.value(),
@@ -2219,8 +2220,9 @@ try
2220 }
2221 catch (const DockerHTTPException& e)
2222 {
2223 + // Docker returns 403 when the network has active endpoints.
2224 THROW_HR_WITH_USER_ERROR_IF(
2223 - HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcNetworkInUse(name), e.StatusCode() == 409);
2225 + HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcNetworkInUse(name), e.StatusCode() == 403);
2226 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), e.StatusCode() == 404);
2227 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to delete network '%hs'", name.c_str());
2228 }
test/windows/WSLCTests.cpp
+199
@@ -5428,6 +5428,205 @@ class WSLCTests
5428 }
5429 }
5430
5431 + WSLC_TEST_METHOD(ContainerCustomNetworkTest)
5432 + {
5433 + const std::string networkName = "custom-net-test";
5434 +
5435 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5436 +
5437 + WSLCDriverOption opts[] = {{"Subnet", "172.35.0.0/16"}};
5438 +
5439 + WSLCNetworkOptions networkOptions{};
5440 + networkOptions.Name = networkName.c_str();
5441 + networkOptions.Driver = "bridge";
5442 + networkOptions.DriverOpts = opts;
5443 + networkOptions.DriverOptsCount = ARRAYSIZE(opts);
5444 +
5445 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&networkOptions));
5446 +
5447 + auto networkCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
5448 +
5449 + WSLCContainerLauncher launcher(
5450 + "debian:latest", "test-custom-network", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5451 + launcher.SetContainerNetworkName(std::string(networkName));
5452 +
5453 + auto container = launcher.Launch(*m_defaultSession);
5454 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
5455 + VERIFY_ARE_EQUAL(container.Inspect().HostConfig.NetworkMode, networkName);
5456 + }
5457 +
5458 + WSLC_TEST_METHOD(ContainerCustomNetworkNotFoundTest)
5459 + {
5460 + WSLCContainerLauncher launcher(
5461 + "debian:latest", "test-custom-network-notfound", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5462 + launcher.SetContainerNetworkName(std::string("nonexistent-net"));
5463 +
5464 + auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
5465 + VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, retVal.first);
5466 + ValidateCOMErrorMessageContains(L"nonexistent-net");
5467 + }
5468 +
5469 + WSLC_TEST_METHOD(ContainerCustomNetworkMissingNameTest)
5470 + {
5471 + WSLCContainerLauncher launcher(
5472 + "debian:latest", "test-custom-network-noname", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5473 +
5474 + auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
5475 + VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
5476 + ValidateCOMErrorMessageContains(L"Container network name is required");
5477 + }
5478 +
5479 + WSLC_TEST_METHOD(ContainerCustomNetworkEmptyNameTest)
5480 + {
5481 + LPCSTR args[] = {"sleep", "99999"};
5482 +
5483 + WSLCContainerOptions options{};
5484 + options.Image = "debian:latest";
5485 + options.Name = "test-custom-network-empty";
5486 + options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
5487 + options.ContainerNetwork.ContainerNetworkType = WSLCContainerNetworkTypeCustom;
5488 + options.ContainerNetwork.ContainerNetworkName = "";
5489 +
5490 + wil::com_ptr<IWSLCContainer> container;
5491 + auto hr = m_defaultSession->CreateContainer(&options, &container);
5492 + VERIFY_ARE_EQUAL(E_INVALIDARG, hr);
5493 + ValidateCOMErrorMessageContains(L"Container network name is required");
5494 + }
5495 +
5496 + WSLC_TEST_METHOD(ContainerCustomNetworkMultipleContainersTest)
5497 + {
5498 + const std::string networkName = "custom-net-multi";
5499 +
5500 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5501 +
5502 + WSLCDriverOption opts[] = {{"Subnet", "172.36.0.0/16"}};
5503 +
5504 + WSLCNetworkOptions networkOptions{};
5505 + networkOptions.Name = networkName.c_str();
5506 + networkOptions.Driver = "bridge";
5507 + networkOptions.DriverOpts = opts;
5508 + networkOptions.DriverOptsCount = ARRAYSIZE(opts);
5509 +
5510 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&networkOptions));
5511 +
5512 + auto networkCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
5513 +
5514 + WSLCContainerLauncher launcher1(
5515 + "debian:latest", "test-custom-multi-1", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5516 + launcher1.SetContainerNetworkName(std::string(networkName));
5517 +
5518 + WSLCContainerLauncher launcher2(
5519 + "debian:latest", "test-custom-multi-2", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5520 + launcher2.SetContainerNetworkName(std::string(networkName));
5521 +
5522 + auto container1 = launcher1.Launch(*m_defaultSession);
5523 + VERIFY_ARE_EQUAL(container1.State(), WslcContainerStateRunning);
5524 + VERIFY_ARE_EQUAL(container1.Inspect().HostConfig.NetworkMode, networkName);
5525 +
5526 + auto container2 = launcher2.Launch(*m_defaultSession);
5527 + VERIFY_ARE_EQUAL(container2.State(), WslcContainerStateRunning);
5528 + VERIFY_ARE_EQUAL(container2.Inspect().HostConfig.NetworkMode, networkName);
5529 + }
5530 +
5531 + WSLC_TEST_METHOD(ContainerCustomNetworkDeleteWhileInUseTest)
5532 + {
5533 + const std::string networkName = "custom-net-inuse";
5534 +
5535 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5536 +
5537 + WSLCDriverOption opts[] = {{"Subnet", "172.37.0.0/16"}};
5538 +
5539 + WSLCNetworkOptions networkOptions{};
5540 + networkOptions.Name = networkName.c_str();
5541 + networkOptions.Driver = "bridge";
5542 + networkOptions.DriverOpts = opts;
5543 + networkOptions.DriverOptsCount = ARRAYSIZE(opts);
5544 +
5545 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&networkOptions));
5546 +
5547 + auto networkCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
5548 +
5549 + WSLCContainerLauncher launcher(
5550 + "debian:latest", "test-custom-net-inuse", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5551 + launcher.SetContainerNetworkName(std::string(networkName));
5552 +
5553 + auto container = launcher.Launch(*m_defaultSession);
5554 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
5555 +
5556 + VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), m_defaultSession->DeleteNetwork(networkName.c_str()));
5557 + }
5558 +
5559 + WSLC_TEST_METHOD(ContainerCustomNetworkPortMappingTest)
5560 + {
5561 + const std::string networkName = "custom-net-ports";
5562 +
5563 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5564 +
5565 + WSLCDriverOption opts[] = {{"Subnet", "172.38.0.0/16"}};
5566 +
5567 + WSLCNetworkOptions networkOptions{};
5568 + networkOptions.Name = networkName.c_str();
5569 + networkOptions.Driver = "bridge";
5570 + networkOptions.DriverOpts = opts;
5571 + networkOptions.DriverOptsCount = ARRAYSIZE(opts);
5572 +
5573 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&networkOptions));
5574 +
5575 + auto networkCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
5576 +
5577 + WSLCContainerLauncher launcher(
5578 + "python:3.12-alpine", "test-custom-net-ports", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5579 + launcher.SetContainerNetworkName(std::string(networkName));
5580 + launcher.AddPort(1251, 8000, AF_INET);
5581 +
5582 + auto container = launcher.Launch(*m_defaultSession);
5583 + auto initProcess = container.GetInitProcess();
5584 + WaitForOutput(initProcess.GetStdHandle(1), "Serving HTTP on");
5585 +
5586 + ExpectHttpResponse(L"http://127.0.0.1:1251", 200);
5587 + }
5588 +
5589 + WSLC_TEST_METHOD(ContainerCustomNetworkRecoveryTest)
5590 + {
5591 + const std::string networkName = "custom-net-recovery";
5592 + const std::string containerName = "test-custom-net-recovery";
5593 +
5594 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5595 +
5596 + WSLCDriverOption opts[] = {{"Subnet", "172.39.0.0/16"}};
5597 +
5598 + WSLCNetworkOptions networkOptions{};
5599 + networkOptions.Name = networkName.c_str();
5600 + networkOptions.Driver = "bridge";
5601 + networkOptions.DriverOpts = opts;
5602 + networkOptions.DriverOptsCount = ARRAYSIZE(opts);
5603 +
5604 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&networkOptions));
5605 +
5606 + auto networkCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
5607 +
5608 + WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5609 + launcher.SetContainerNetworkName(std::string(networkName));
5610 +
5611 + {
5612 + auto container = launcher.Create(*m_defaultSession);
5613 + container.SetDeleteOnClose(false);
5614 +
5615 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated);
5616 + }
5617 +
5618 + // Restart the session and verify the container is recovered with its custom network.
5619 + ResetTestSession();
5620 +
5621 + auto recoveredContainer = OpenContainer(m_defaultSession.get(), containerName);
5622 +
5623 + VERIFY_ARE_EQUAL(recoveredContainer.State(), WslcContainerStateCreated);
5624 + VERIFY_SUCCEEDED(recoveredContainer.Get().Start(WSLCContainerStartFlagsAttach, nullptr));
5625 + VERIFY_ARE_EQUAL(recoveredContainer.State(), WslcContainerStateRunning);
5626 +
5627 + VERIFY_ARE_EQUAL(recoveredContainer.Inspect().HostConfig.NetworkMode, networkName);
5628 + }
5629 +
5630 WSLC_TEST_METHOD(ContainerInspect)
5631 {
5632 // Helper to verify port mappings.