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.