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.