Add support for starting a container in multiple networks (#40434)
beena352 committed
May 13, 2026 at 14:28 UTC
83e9cfcd89b9b7adabe5164da8f46a4daa238fb2
8 files changed
+449
-8
localization/strings/en-US/Resources.resw
+15
@@ -2103,6 +2103,10 @@ Usage:
2103
<value>Container '{}' is not running.</value>
2104
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2105
</data>
2106
+ <data name="MessageWslcContainerIpAddressNotSupported" xml:space="preserve">
2107
+ <value>ContainerIpAddress is not yet supported.</value>
2108
+ <comment>{Locked="ContainerIpAddress"}Command line arguments, file names and string inserts should not be translated</comment>
2109
+ </data>
2110
<data name="MessageWslcContainerIsRunning" xml:space="preserve">
2111
<value>Container '{}' is running.</value>
2112
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
@@ -2114,6 +2118,9 @@ Usage:
2118
<data name="MessageWslcContainerNetworkNameRequired" xml:space="preserve">
2119
<value>Container network name is required for custom network type.</value>
2120
</data>
2121
+ <data name="MessageWslcNetworkNameRequired" xml:space="preserve">
2122
+ <value>Network name cannot be empty.</value>
2123
+ </data>
2124
<data name="WSLCCLI_UnrecognizedCommandError" xml:space="preserve">
2125
<value>Unrecognized command: '{}'</value>
2126
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
@@ -2300,6 +2307,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2307
<value>Network '{}' already exists.</value>
2308
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2309
</data>
2310
+ <data name="MessageWslcAdditionalNetworksRequirePrimary" xml:space="preserve">
2311
+ <value>Additional networks are not allowed when the primary network mode is 'host' or 'none'.</value>
2312
+ <comment>{Locked="host"}{Locked="none"}Command line arguments, file names and string inserts should not be translated</comment>
2313
+ </data>
2314
+ <data name="MessageWslcDuplicateNetwork" xml:space="preserve">
2315
+ <value>Duplicate network: '{}'</value>
2316
+ <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2317
+ </data>
2318
<data name = "MessageWslcFailedToMountVolume" xml:space = "preserve" >
2319
<value>Failed to create volume '{}': {}</value>
2320
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/common/WSLCContainerLauncher.cpp
+21
-1
@@ -250,6 +250,11 @@ void wsl::windows::common::WSLCContainerLauncher::AddTmpfs(const std::string& Co
250
m_tmpfsMounts.push_back(tmpfs);
251
}
252
253
+void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name)
254
+{
255
+ m_additionalNetworks.push_back(Name);
256
+}
257
+
258
std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::LaunchNoThrow(IWSLCSession& Session, WSLCContainerStartFlags Flags)
259
{
260
auto [result, container] = CreateNoThrow(Session);
@@ -283,7 +288,6 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
288
auto [processOptions, commandLinePtrs, environmentPtrs] = CreateProcessOptions();
289
options.InitProcessOptions = processOptions;
290
options.ContainerNetwork.ContainerNetworkType = m_containerNetworkType;
286
- options.ContainerNetwork.ContainerNetworkName = m_containerNetworkName.empty() ? nullptr : m_containerNetworkName.c_str();
291
options.Ports = m_ports.data();
292
options.PortsCount = static_cast<ULONG>(m_ports.size());
293
options.StopSignal = m_stopSignal;
@@ -355,6 +359,22 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
359
options.TmpfsCount = static_cast<ULONG>(m_tmpfsMounts.size());
360
options.Tmpfs = m_tmpfsMounts.size() > 0 ? m_tmpfsMounts.data() : nullptr;
361
362
+ std::vector<WSLCNetworkAttachment> networkAttachments;
363
+ if (m_containerNetworkType == WSLCContainerNetworkTypeCustom)
364
+ {
365
+ networkAttachments.push_back({m_containerNetworkName.c_str(), nullptr});
366
+ }
367
+ for (const auto& e : m_additionalNetworks)
368
+ {
369
+ networkAttachments.push_back({e.c_str(), nullptr});
370
+ }
371
+
372
+ if (!networkAttachments.empty())
373
+ {
374
+ options.ContainerNetwork.Networks = networkAttachments.data();
375
+ options.ContainerNetwork.NetworksCount = static_cast<ULONG>(networkAttachments.size());
376
+ }
377
+
378
options.MemoryBytes = m_memoryBytes;
379
options.NanoCpus = m_nanoCpus;
380
options.UlimitsCount = static_cast<ULONG>(m_ulimits.size());
src/windows/common/WSLCContainerLauncher.h
+2
@@ -62,6 +62,7 @@ public:
62
void AddPort(uint16_t WindowsPort, uint16_t ContainerPort, int Family, int Protocol = IPPROTO_TCP, const std::optional<std::string>& BindingAddress = {});
63
void AddLabel(const std::string& Key, const std::string& Value);
64
void AddTmpfs(const std::string& ContainerPath, const std::string& Options);
65
+ void AddAdditionalNetwork(const std::string& Name);
66
67
std::pair<HRESULT, std::optional<RunningWSLCContainer>> CreateNoThrow(IWSLCSession& Session);
68
RunningWSLCContainer Create(IWSLCSession& Session);
@@ -108,6 +109,7 @@ private:
109
std::vector<std::string> m_dnsServers;
110
std::vector<std::string> m_dnsSearchDomains;
111
std::vector<std::string> m_dnsOptions;
112
+ std::vector<std::string> m_additionalNetworks;
113
std::vector<WSLCLabel> m_labels;
114
std::deque<std::string> m_labelKeys;
115
std::deque<std::string> m_labelValues;
src/windows/inc/docker_schema.h
+30
-3
@@ -235,6 +235,30 @@ struct HostConfig
235
HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs, Devices, ShmSize, Memory, NanoCpus, Ulimits);
236
};
237
238
+struct EndpointSettings
239
+{
240
+ std::string IPAddress;
241
+ std::string Gateway;
242
+ std::string MacAddress;
243
+ int IPPrefixLen{};
244
+
245
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen);
246
+};
247
+
248
+struct NetworkingConfig
249
+{
250
+ std::map<std::string, EmptyObject> EndpointsConfig;
251
+
252
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(NetworkingConfig, EndpointsConfig);
253
+};
254
+
255
+struct NetworkSettings
256
+{
257
+ std::map<std::string, EndpointSettings> Networks;
258
+
259
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkSettings, Networks);
260
+};
261
+
262
struct CreateContainer
263
{
264
using TResponse = CreatedContainer;
@@ -257,9 +281,10 @@ struct CreateContainer
281
std::map<std::string, EmptyObject> ExposedPorts;
282
std::map<std::string, std::string> Labels;
283
HostConfig HostConfig;
284
+ NetworkingConfig NetworkingConfig;
285
286
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(
262
- CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, WorkingDir, User, Hostname, Domainname, Labels);
287
+ CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, WorkingDir, User, Hostname, Domainname, Labels, NetworkingConfig);
288
};
289
290
struct ContainerInspectState
@@ -304,8 +329,9 @@ struct InspectContainer
329
ContainerInspectState State;
330
ContainerConfig Config;
331
HostConfig HostConfig;
332
+ NetworkSettings NetworkSettings;
333
308
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, Config, HostConfig);
334
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, Config, HostConfig, NetworkSettings);
335
};
336
337
struct InspectExec
@@ -492,8 +518,9 @@ struct ContainerInfo
518
ContainerState State{ContainerState::Unknown};
519
int64_t Created{};
520
HostConfig HostConfig;
521
+ NetworkSettings NetworkSettings;
522
496
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInfo, Id, Names, Image, Labels, Ports, Mounts, State, Created, HostConfig);
523
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInfo, Id, Names, Image, Labels, Ports, Mounts, State, Created, HostConfig, NetworkSettings);
524
};
525
526
struct BuildKitVertex
src/windows/inc/wslc_schema.h
+19
-1
@@ -79,6 +79,23 @@ struct ContainerConfig
79
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir);
80
};
81
82
+struct InspectEndpointSettings
83
+{
84
+ std::string IPAddress;
85
+ std::string Gateway;
86
+ std::string MacAddress;
87
+ int IPPrefixLen{};
88
+
89
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen);
90
+};
91
+
92
+struct InspectNetworkSettings
93
+{
94
+ std::map<std::string, InspectEndpointSettings> Networks;
95
+
96
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectNetworkSettings, Networks);
97
+};
98
+
99
struct InspectContainer
100
{
101
std::string Id;
@@ -91,8 +108,9 @@ struct InspectContainer
108
std::map<std::string, std::vector<InspectPortBinding>> Ports;
109
std::vector<InspectMount> Mounts;
110
std::map<std::string, std::string> Labels;
111
+ InspectNetworkSettings NetworkSettings;
112
95
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, HostConfig, Config, Ports, Mounts, Labels);
113
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, HostConfig, Config, Ports, Mounts, Labels, NetworkSettings);
114
};
115
116
struct ImageConfig
src/windows/service/inc/wslc.idl
+8
-1
@@ -241,10 +241,17 @@ typedef enum _WSLCContainerNetworkType
241
WSLCContainerNetworkTypeCustom = 3
242
} WSLCContainerNetworkType;
243
244
+typedef struct _WSLCNetworkAttachment
245
+{
246
+ [unique] LPCSTR NetworkName;
247
+ [unique] LPCSTR ContainerIpAddress; // Reserved for future --ip support; must be NULL today.
248
+} WSLCNetworkAttachment;
249
+
250
typedef struct _WSLCContainerNetwork
251
{
252
WSLCContainerNetworkType ContainerNetworkType;
247
- LPCSTR ContainerNetworkName;
253
+ [unique, size_is(NetworksCount)] const WSLCNetworkAttachment* Networks;
254
+ ULONG NetworksCount;
255
} WSLCContainerNetwork;
256
257
typedef enum _WSLCContainerFlags
src/windows/wslcsession/WSLCContainer.cpp
+102
-1
@@ -381,6 +381,71 @@ void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::wind
381
}
382
}
383
384
+constexpr ULONG GetAdditionalStartIndex(WSLCContainerNetworkType type)
385
+{
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;
389
+}
390
+
391
+LPCSTR GetPrimaryCustomNetworkName(const WSLCContainerNetwork& network)
392
+{
393
+ if (network.ContainerNetworkType != WSLCContainerNetworkTypeCustom || network.NetworksCount == 0)
394
+ {
395
+ return nullptr;
396
+ }
397
+ THROW_HR_IF_MSG(E_INVALIDARG, network.Networks == nullptr, "Networks pointer is null but NetworksCount=%lu", network.NetworksCount);
398
+ return network.Networks[0].NetworkName;
399
+}
400
+
401
+void ProcessAdditionalNetworks(
402
+ const WSLCContainerNetwork& network,
403
+ const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
404
+ const std::string& primaryNetworkMode,
405
+ wsl::windows::common::docker_schema::CreateContainer& request)
406
+{
407
+ // Validate pointer/count consistency and reject unsupported fields across all entries.
408
+ if (network.NetworksCount > 0)
409
+ {
410
+ THROW_HR_IF_MSG(E_INVALIDARG, network.Networks == nullptr, "Networks pointer is null but NetworksCount=%lu", network.NetworksCount);
411
+
412
+ for (ULONG i = 0; i < network.NetworksCount; i++)
413
+ {
414
+ THROW_HR_WITH_USER_ERROR_IF(
415
+ E_NOTIMPL, Localization::MessageWslcContainerIpAddressNotSupported(), network.Networks[i].ContainerIpAddress != nullptr);
416
+ }
417
+ }
418
+
419
+ const ULONG startIndex = GetAdditionalStartIndex(network.ContainerNetworkType);
420
+ if (network.NetworksCount <= startIndex)
421
+ {
422
+ return;
423
+ }
424
+
425
+ THROW_HR_WITH_USER_ERROR_IF(
426
+ E_INVALIDARG,
427
+ Localization::MessageWslcAdditionalNetworksRequirePrimary(),
428
+ primaryNetworkMode == "host" || primaryNetworkMode == "none");
429
+
430
+ // Add the primary and all additional networks to EndpointsConfig so Docker attaches them all at create time.
431
+ auto& endpointsConfig = request.NetworkingConfig.EndpointsConfig;
432
+ endpointsConfig[primaryNetworkMode] = {};
433
+
434
+ for (ULONG i = startIndex; i < network.NetworksCount; i++)
435
+ {
436
+ const char* rawName = network.Networks[i].NetworkName;
437
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !rawName || strlen(rawName) == 0);
438
+
439
+ const std::string networkName = rawName;
440
+
441
+ auto [_, inserted] = endpointsConfig.insert({networkName, EmptyObject{}});
442
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDuplicateNetwork(networkName), !inserted);
443
+
444
+ THROW_HR_WITH_USER_ERROR_IF(
445
+ WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(networkName), !sessionNetworks.contains(networkName));
446
+ }
447
+}
448
+
449
void ConfigureLdPathForGpu(std::vector<std::string>& Env)
450
{
451
static constexpr std::string_view ldLibraryPathPrefix = "LD_LIBRARY_PATH=";
@@ -1214,6 +1279,17 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1279
// Map labels. m_labels should already exclude internal metadata labels.
1280
wslcInspect.Labels = m_labels;
1281
1282
+ // Map per-endpoint network settings from Docker inspect data.
1283
+ for (const auto& [name, endpoint] : dockerInspect.NetworkSettings.Networks)
1284
+ {
1285
+ wslc_schema::InspectEndpointSettings wslcEndpoint{};
1286
+ wslcEndpoint.IPAddress = endpoint.IPAddress;
1287
+ wslcEndpoint.Gateway = endpoint.Gateway;
1288
+ wslcEndpoint.MacAddress = endpoint.MacAddress;
1289
+ wslcEndpoint.IPPrefixLen = endpoint.IPPrefixLen;
1290
+ wslcInspect.NetworkSettings.Networks[name] = std::move(wslcEndpoint);
1291
+ }
1292
+
1293
return wslcInspect;
1294
}
1295
@@ -1495,10 +1571,12 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1571
containerOptions.ContainerNetwork.ContainerNetworkType,
1572
virtualMachine,
1573
sessionNetworks,
1498
- containerOptions.ContainerNetwork.ContainerNetworkName);
1574
+ GetPrimaryCustomNetworkName(containerOptions.ContainerNetwork));
1575
1576
request.HostConfig.NetworkMode = networkMode;
1577
1578
+ ProcessAdditionalNetworks(containerOptions.ContainerNetwork, sessionNetworks, networkMode, request);
1579
+
1580
for (const auto& e : mappedPorts)
1581
{
1582
auto portKey = std::format("{}/{}", e.ContainerPort, e.ProtocolString());
@@ -1542,6 +1620,29 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1620
// Inspect the container to fetch its generated name (if needed) and Docker's authoritative Created timestamp.
1621
auto inspectData = DockerClient.InspectContainer(result.Id);
1622
1623
+ // Post-create verification: confirm every requested network is actually attached.
1624
+ // If Docker rejected any endpoint, throw here so deleteOnFailure cleans up the orphan container.
1625
+ const ULONG verifyStartIndex = GetAdditionalStartIndex(containerOptions.ContainerNetwork.ContainerNetworkType);
1626
+ if (containerOptions.ContainerNetwork.NetworksCount > verifyStartIndex)
1627
+ {
1628
+ THROW_HR_IF_MSG(
1629
+ E_UNEXPECTED,
1630
+ !inspectData.NetworkSettings.Networks.contains(networkMode),
1631
+ "Container was created but primary network '%hs' was not attached",
1632
+ networkMode.c_str());
1633
+ }
1634
+
1635
+ for (ULONG i = verifyStartIndex; i < containerOptions.ContainerNetwork.NetworksCount; i++)
1636
+ {
1637
+ const char* rawName = containerOptions.ContainerNetwork.Networks[i].NetworkName;
1638
+ const std::string networkName = rawName != nullptr ? rawName : "";
1639
+ THROW_HR_IF_MSG(
1640
+ E_UNEXPECTED,
1641
+ !inspectData.NetworkSettings.Networks.contains(networkName),
1642
+ "Container was created but network '%hs' was not attached",
1643
+ networkName.c_str());
1644
+ }
1645
+
1646
// Wait for the container create event to be delivered on the Docker event stream so that
1647
// any events for objects created for the container (e.g. volumes) are delivered before we return
1648
// from this function.
test/windows/WSLCTests.cpp
+252
-1
@@ -5736,7 +5736,10 @@ class WSLCTests
5736
options.Name = "test-custom-network-empty";
5737
options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
5738
options.ContainerNetwork.ContainerNetworkType = WSLCContainerNetworkTypeCustom;
5739
- options.ContainerNetwork.ContainerNetworkName = "";
5739
+ WSLCNetworkAttachment emptyNet{};
5740
+ emptyNet.NetworkName = "";
5741
+ options.ContainerNetwork.Networks = &emptyNet;
5742
+ options.ContainerNetwork.NetworksCount = 1;
5743
5744
wil::com_ptr<IWSLCContainer> container;
5745
auto hr = m_defaultSession->CreateContainer(&options, &container);
@@ -5878,6 +5881,254 @@ class WSLCTests
5881
VERIFY_ARE_EQUAL(recoveredContainer.Inspect().HostConfig.NetworkMode, networkName);
5882
}
5883
5884
+ WSLC_TEST_METHOD(ContainerMultipleNetworksTest)
5885
+ {
5886
+ const std::string primaryNetwork = "multi-net-primary";
5887
+ const std::string additionalNetwork = "multi-net-additional";
5888
+
5889
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str()));
5890
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(additionalNetwork.c_str()));
5891
+
5892
+ WSLCDriverOption primaryOpts[] = {{"Subnet", "172.40.0.0/16"}};
5893
+ WSLCNetworkOptions primaryNetOpts{};
5894
+ primaryNetOpts.Name = primaryNetwork.c_str();
5895
+ primaryNetOpts.Driver = "bridge";
5896
+ primaryNetOpts.DriverOpts = primaryOpts;
5897
+ primaryNetOpts.DriverOptsCount = ARRAYSIZE(primaryOpts);
5898
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&primaryNetOpts));
5899
+ auto primaryCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str())); });
5900
+
5901
+ WSLCDriverOption additionalOpts[] = {{"Subnet", "172.41.0.0/16"}};
5902
+ WSLCNetworkOptions additionalNetOpts{};
5903
+ additionalNetOpts.Name = additionalNetwork.c_str();
5904
+ additionalNetOpts.Driver = "bridge";
5905
+ additionalNetOpts.DriverOpts = additionalOpts;
5906
+ additionalNetOpts.DriverOptsCount = ARRAYSIZE(additionalOpts);
5907
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&additionalNetOpts));
5908
+ auto additionalCleanup =
5909
+ wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(additionalNetwork.c_str())); });
5910
+
5911
+ WSLCContainerLauncher launcher(
5912
+ "debian:latest", "test-multi-net", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5913
+ launcher.SetContainerNetworkName(std::string(primaryNetwork));
5914
+ launcher.AddAdditionalNetwork(additionalNetwork);
5915
+
5916
+ auto container = launcher.Launch(*m_defaultSession);
5917
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
5918
+
5919
+ auto inspect = container.Inspect();
5920
+ VERIFY_ARE_EQUAL(inspect.HostConfig.NetworkMode, primaryNetwork);
5921
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(primaryNetwork));
5922
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(additionalNetwork));
5923
+ VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.at(primaryNetwork).IPAddress.empty());
5924
+ VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.at(additionalNetwork).IPAddress.empty());
5925
+ }
5926
+
5927
+ WSLC_TEST_METHOD(ContainerAdditionalNetworksRejectedForHostAndNoneTest)
5928
+ {
5929
+ // This check runs before network existence is validated, so no real network needs to exist.
5930
+ const std::string additionalNetwork = "any-additional-net";
5931
+
5932
+ {
5933
+ WSLCContainerLauncher launcher(
5934
+ "debian:latest", "test-multi-net-host-reject", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
5935
+ launcher.AddAdditionalNetwork(additionalNetwork);
5936
+
5937
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
5938
+ VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
5939
+ ValidateCOMErrorMessage(L"Additional networks are not allowed when the primary network mode is 'host' or 'none'.");
5940
+ }
5941
+
5942
+ {
5943
+ WSLCContainerLauncher launcher(
5944
+ "debian:latest", "test-multi-net-none-reject", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeNone);
5945
+ launcher.AddAdditionalNetwork(additionalNetwork);
5946
+
5947
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
5948
+ VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
5949
+ ValidateCOMErrorMessage(L"Additional networks are not allowed when the primary network mode is 'host' or 'none'.");
5950
+ }
5951
+ }
5952
+
5953
+ WSLC_TEST_METHOD(ContainerDuplicateNetworkRejectedTest)
5954
+ {
5955
+ const std::string primaryNetwork = "multi-net-dup-primary";
5956
+
5957
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str()));
5958
+
5959
+ WSLCDriverOption opts[] = {{"Subnet", "172.48.0.0/16"}};
5960
+ WSLCNetworkOptions netOpts{};
5961
+ netOpts.Name = primaryNetwork.c_str();
5962
+ netOpts.Driver = "bridge";
5963
+ netOpts.DriverOpts = opts;
5964
+ netOpts.DriverOptsCount = ARRAYSIZE(opts);
5965
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts));
5966
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str())); });
5967
+
5968
+ WSLCContainerLauncher launcher(
5969
+ "debian:latest", "test-multi-net-dup-reject", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5970
+ launcher.SetContainerNetworkName(std::string(primaryNetwork));
5971
+ launcher.AddAdditionalNetwork(primaryNetwork);
5972
+
5973
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
5974
+ VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
5975
+ ValidateCOMErrorMessage(
5976
+ std::format(L"Duplicate network: '{}'", std::wstring(primaryNetwork.begin(), primaryNetwork.end())).c_str());
5977
+ }
5978
+
5979
+ WSLC_TEST_METHOD(ContainerAdditionalNetworkNotFoundRejectedTest)
5980
+ {
5981
+ const std::string primaryNetwork = "multi-net-notfound-primary";
5982
+ const std::string missingNetwork = "multi-net-nonexistent";
5983
+
5984
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str()));
5985
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(missingNetwork.c_str()));
5986
+
5987
+ WSLCDriverOption opts[] = {{"Subnet", "172.49.0.0/16"}};
5988
+ WSLCNetworkOptions netOpts{};
5989
+ netOpts.Name = primaryNetwork.c_str();
5990
+ netOpts.Driver = "bridge";
5991
+ netOpts.DriverOpts = opts;
5992
+ netOpts.DriverOptsCount = ARRAYSIZE(opts);
5993
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts));
5994
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str())); });
5995
+
5996
+ WSLCContainerLauncher launcher(
5997
+ "debian:latest", "test-multi-net-notfound-reject", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
5998
+ launcher.SetContainerNetworkName(std::string(primaryNetwork));
5999
+ launcher.AddAdditionalNetwork(missingNetwork);
6000
+
6001
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
6002
+ VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, retVal.first);
6003
+ ValidateCOMErrorMessage(
6004
+ std::format(L"Network not found: '{}'", std::wstring(missingNetwork.begin(), missingNetwork.end())).c_str());
6005
+ }
6006
+
6007
+ WSLC_TEST_METHOD(ContainerBridgedPrimaryDuplicateNetworkRejectedTest)
6008
+ {
6009
+ // Verifies that passing the primary bridge network as an additional network is caught as a duplicate.
6010
+ WSLCContainerLauncher launcher(
6011
+ "debian:latest", "test-bridge-dup-reject", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged);
6012
+ launcher.AddAdditionalNetwork("bridge");
6013
+
6014
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
6015
+ VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
6016
+ ValidateCOMErrorMessage(L"Duplicate network: 'bridge'");
6017
+ }
6018
+
6019
+ WSLC_TEST_METHOD(ContainerAdditionalNetworkMalformedNameTest)
6020
+ {
6021
+ // Most malformed names fall through to the network lookup.
6022
+
6023
+ // Invalid character.
6024
+ {
6025
+ WSLCContainerLauncher launcher(
6026
+ "debian:latest", "test-invalid-name-char", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged);
6027
+ launcher.AddAdditionalNetwork("bad/name");
6028
+
6029
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
6030
+ VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, retVal.first);
6031
+ ValidateCOMErrorMessage(L"Network not found: 'bad/name'");
6032
+ }
6033
+
6034
+ // Empty name.
6035
+ {
6036
+ WSLCContainerLauncher launcher(
6037
+ "debian:latest", "test-invalid-name-empty", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged);
6038
+ launcher.AddAdditionalNetwork("");
6039
+
6040
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
6041
+ VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
6042
+ ValidateCOMErrorMessage(L"Network name cannot be empty.");
6043
+ }
6044
+
6045
+ // Name exceeds WSLC_MAX_NETWORK_NAME_LENGTH (255).
6046
+ {
6047
+ const std::string tooLongName(WSLC_MAX_NETWORK_NAME_LENGTH + 1, 'a');
6048
+
6049
+ WSLCContainerLauncher launcher(
6050
+ "debian:latest", "test-invalid-name-long", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged);
6051
+ launcher.AddAdditionalNetwork(tooLongName);
6052
+
6053
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
6054
+ VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, retVal.first);
6055
+ ValidateCOMErrorMessage(std::format(L"Network not found: '{}'", std::wstring(tooLongName.begin(), tooLongName.end())).c_str());
6056
+ }
6057
+ }
6058
+
6059
+ WSLC_TEST_METHOD(ContainerDeleteAdditionalNetworkWhileInUseTest)
6060
+ {
6061
+ const std::string primaryNetwork = "multi-net-del-primary";
6062
+ const std::string additionalNetwork = "multi-net-del-additional";
6063
+
6064
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str()));
6065
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(additionalNetwork.c_str()));
6066
+
6067
+ WSLCDriverOption primaryOpts[] = {{"Subnet", "172.50.0.0/16"}};
6068
+ WSLCNetworkOptions primaryNetOpts{};
6069
+ primaryNetOpts.Name = primaryNetwork.c_str();
6070
+ primaryNetOpts.Driver = "bridge";
6071
+ primaryNetOpts.DriverOpts = primaryOpts;
6072
+ primaryNetOpts.DriverOptsCount = ARRAYSIZE(primaryOpts);
6073
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&primaryNetOpts));
6074
+ auto primaryCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetwork.c_str())); });
6075
+
6076
+ WSLCDriverOption additionalOpts[] = {{"Subnet", "172.51.0.0/16"}};
6077
+ WSLCNetworkOptions additionalNetOpts{};
6078
+ additionalNetOpts.Name = additionalNetwork.c_str();
6079
+ additionalNetOpts.Driver = "bridge";
6080
+ additionalNetOpts.DriverOpts = additionalOpts;
6081
+ additionalNetOpts.DriverOptsCount = ARRAYSIZE(additionalOpts);
6082
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&additionalNetOpts));
6083
+ auto additionalCleanup =
6084
+ wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(additionalNetwork.c_str())); });
6085
+
6086
+ WSLCContainerLauncher launcher(
6087
+ "debian:latest", "test-multi-net-del", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
6088
+ launcher.SetContainerNetworkName(std::string(primaryNetwork));
6089
+ launcher.AddAdditionalNetwork(additionalNetwork);
6090
+
6091
+ auto container = launcher.Launch(*m_defaultSession);
6092
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
6093
+
6094
+ VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), m_defaultSession->DeleteNetwork(additionalNetwork.c_str()));
6095
+ }
6096
+
6097
+ WSLC_TEST_METHOD(ContainerNetworkAttachmentIpAddressRejectedTest)
6098
+ {
6099
+ // ContainerIpAddress is reserved and must be null today.
6100
+ LPCSTR args[] = {"sleep", "99999"};
6101
+
6102
+ WSLCContainerOptions options{};
6103
+ options.Image = "debian:latest";
6104
+ options.Name = "test-network-ip-reserved";
6105
+ options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
6106
+ options.ContainerNetwork.ContainerNetworkType = WSLCContainerNetworkTypeBridged;
6107
+ WSLCNetworkAttachment netWithIp{};
6108
+ netWithIp.NetworkName = "bridge";
6109
+ netWithIp.ContainerIpAddress = "10.0.0.5";
6110
+ options.ContainerNetwork.Networks = &netWithIp;
6111
+ options.ContainerNetwork.NetworksCount = 1;
6112
+
6113
+ wil::com_ptr<IWSLCContainer> container;
6114
+ auto hr = m_defaultSession->CreateContainer(&options, &container);
6115
+ VERIFY_ARE_EQUAL(E_NOTIMPL, hr);
6116
+ ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported.");
6117
+ }
6118
+
6119
+ WSLC_TEST_METHOD(ContainerCustomNetworkMissingPrimaryWithAdditionalRejectedTest)
6120
+ {
6121
+ // Custom + additional networks without a primary network name must be rejected,
6122
+ // not silently promote the first additional to primary.
6123
+ WSLCContainerLauncher launcher(
6124
+ "debian:latest", "test-custom-no-primary", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeCustom);
6125
+ launcher.AddAdditionalNetwork("any-net");
6126
+
6127
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
6128
+ VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
6129
+ ValidateCOMErrorMessage(L"Container network name is required for custom network type.");
6130
+ }
6131
+
6132
WSLC_TEST_METHOD(ContainerInspect)
6133
{
6134
// Helper to verify port mappings.