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.