diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index aa0f5bf72c..b9a2aa6c03 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2986,6 +2986,14 @@ On first run, creates the file with all settings commented out at their defaults Invalid {} value: network name cannot be empty or whitespace {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Invalid {} value: network name can only be specified once + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + + Invalid {} value: unsupported network option '{}' + {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + host mode networking is not supported diff --git a/src/shared/inc/stringshared.h b/src/shared/inc/stringshared.h index 978bd9cbd7..48160c9426 100644 --- a/src/shared/inc/stringshared.h +++ b/src/shared/inc/stringshared.h @@ -13,14 +13,19 @@ Module Name: --*/ #pragma once +#include +#include +#include #include #include #include +#include #include #include #include #include #include +#include #ifndef WIN32 #include @@ -98,6 +103,27 @@ inline std::vector> Split(const std::basic_string& Strin return Output; } +template +inline std::vector> SplitPreserveEmpty(const std::basic_string_view String, T Separator) +{ + std::vector> Output; + size_t Start = 0; + while (Start <= String.size()) + { + const auto End = String.find(Separator, Start); + if (End == std::basic_string_view::npos) + { + Output.emplace_back(String.substr(Start)); + break; + } + + Output.emplace_back(String.substr(Start, End - Start)); + Start = End + 1; + } + + return Output; +} + template inline std::vector> SplitByMultipleSeparators(const std::basic_string& String, const std::basic_string& Separators) { @@ -311,6 +337,21 @@ inline bool IsEqual(const std::wstring_view String1, const std::wstring_view Str return (Compare(String1, String2, CaseInsensitive) == String1.size()); } +template +inline bool IsEmptyOrWhitespace(const std::basic_string_view String) +{ + return String.empty() || std::all_of(String.begin(), String.end(), [](T Ch) { + if constexpr (std::is_same_v) + { + return std::iswspace(static_cast(Ch)); + } + else + { + return std::isspace(static_cast(Ch)); + } + }); +} + template inline std::optional ParseBool(const T* String) { diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index 77b98cdd46..57a96e3d91 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -282,7 +282,12 @@ void wsl::windows::common::WSLCContainerLauncher::AddTmpfs(const std::string& Co void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name) { - m_additionalNetworks.push_back(Name); + AddAdditionalNetwork(Name, {}); +} + +void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name, const std::vector& Aliases) +{ + m_additionalNetworks.push_back({.Name = Name, .Aliases = Aliases}); } void wsl::windows::common::WSLCContainerLauncher::AddPrimaryNetworkAlias(const std::string& Alias) @@ -424,24 +429,37 @@ std::pair> WSLCContainerLauncher::C // Each additional network becomes an entry in NetworkingConfig.EndpointsConfig. std::vector connections; connections.reserve(m_additionalNetworks.size()); + std::vector> connectionSettings; + connectionSettings.reserve(m_additionalNetworks.size()); for (const auto& e : m_additionalNetworks) { - connections.push_back({.NetworkName = e.c_str()}); + auto& settings = connectionSettings.emplace_back(); + settings.reserve(e.Aliases.size()); + for (const auto& alias : e.Aliases) + { + settings.push_back({.Key = "Aliases", .Value = alias.c_str()}); + } + + connections.push_back({ + .NetworkName = e.Name.c_str(), + .Settings = settings.empty() ? nullptr : settings.data(), + .SettingsCount = static_cast(settings.size()), + }); } options.ContainerNetwork.Networks = connections.empty() ? nullptr : connections.data(); options.ContainerNetwork.NetworksCount = static_cast(connections.size()); // Aliases for the primary endpoint. - std::vector aliasKvps; - aliasKvps.reserve(m_primaryNetworkAliases.size()); + std::vector primarySettings; + primarySettings.reserve(m_primaryNetworkAliases.size()); for (const auto& alias : m_primaryNetworkAliases) { - aliasKvps.push_back({.Key = "Aliases", .Value = alias.c_str()}); + primarySettings.push_back({.Key = "Aliases", .Value = alias.c_str()}); } - options.ContainerNetwork.Settings = aliasKvps.empty() ? nullptr : aliasKvps.data(); - options.ContainerNetwork.SettingsCount = static_cast(aliasKvps.size()); + options.ContainerNetwork.Settings = primarySettings.empty() ? nullptr : primarySettings.data(); + options.ContainerNetwork.SettingsCount = static_cast(primarySettings.size()); options.MemoryBytes = m_memoryBytes; options.NanoCpus = m_nanoCpus; diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index 0e9a63f42b..0ad00956cd 100644 --- a/src/windows/common/WSLCContainerLauncher.h +++ b/src/windows/common/WSLCContainerLauncher.h @@ -63,6 +63,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher void AddLabel(const std::string& Key, const std::string& Value); void AddTmpfs(const std::string& ContainerPath, const std::string& Options); void AddAdditionalNetwork(const std::string& Name); + void AddAdditionalNetwork(const std::string& Name, const std::vector& Aliases); void AddPrimaryNetworkAlias(const std::string& Alias); std::pair> CreateNoThrow(IWSLCSession& Session, IWarningCallback* WarningCallback = nullptr); @@ -99,6 +100,12 @@ class WSLCContainerLauncher : private WSLCProcessLauncher using WSLCProcessLauncher::SetWorkingDirectory; private: + struct NetworkConnection + { + std::string Name; + std::vector Aliases; + }; + std::string m_image; std::string m_name; std::vector m_ports; @@ -123,7 +130,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher std::vector m_dnsServers; std::vector m_dnsSearchDomains; std::vector m_dnsOptions; - std::vector m_additionalNetworks; + std::vector m_additionalNetworks; std::vector m_primaryNetworkAliases; std::vector m_labels; std::deque m_labelKeys; diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 2c9abc1aa3..f12c5a265e 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -224,6 +224,9 @@ typedef struct _WSLCUlimit typedef struct _WSLCNetworkConnection { [string] LPCSTR NetworkName; + + // Settings for this endpoint. + // KVP-encoded; duplicate keys are allowed (e.g., multiple "Aliases" entries). [unique, size_is(SettingsCount)] const KeyValuePair* Settings; ULONG SettingsCount; } WSLCNetworkConnection; diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index 4b2995e8ea..1fca5926db 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -141,19 +141,7 @@ void Argument::Validate(const ArgMap& execArgs) const case ArgType::Network: { - for (const auto& value : execArgs.GetAll()) - { - if (value.empty() || - std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast(c)); })) - { - throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name)); - } - - if (IsEqual(value, L"host", true)) - { - throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError()); - } - } + validation::ValidateNetwork(execArgs.GetAll(), m_name); break; } @@ -219,6 +207,18 @@ void ValidateFilter(const std::vector& values) } } +void ValidateNetwork(const std::vector& values, const std::wstring& argName) +{ + for (const auto& value : values) + { + const auto parsed = ParseNetworkArgument(value, argName); + if (IsEqual(parsed.Name, "host", true)) + { + throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError()); + } + } +} + // Convert string to WSLCSignal enum - accepts either signal name (e.g., "SIGKILL") or number (e.g., "9") WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName) { @@ -659,6 +659,80 @@ std::tuple ParseUlimit(const std::wstring& input, return {WideToMultiByte(input.substr(0, equalsPos)), soft, hard}; } +ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::wstring& argName) +{ + ParsedNetworkArgument result; + + auto parseOptions = [&](std::wstring_view options, bool requireName) { + bool parsedName = false; + for (const auto part : SplitPreserveEmpty(options, L',')) + { + const auto separator = part.find(L'='); + if (separator == std::wstring_view::npos || separator == 0) + { + throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, std::wstring{part})); + } + + const auto key = part.substr(0, separator); + const auto optionValue = part.substr(separator + 1); + if (key == L"name") + { + if (IsEmptyOrWhitespace(optionValue)) + { + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName)); + } + + if (parsedName) + { + throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(argName)); + } + + parsedName = true; + result.Name = WideToMultiByte(std::wstring{optionValue}); + } + else if (key == L"alias") + { + if (IsEmptyOrWhitespace(optionValue)) + { + throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(argName)); + } + + result.Aliases.emplace_back(WideToMultiByte(std::wstring{optionValue})); + } + else + { + throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, std::wstring{key})); + } + } + + if (requireName && !parsedName) + { + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName)); + } + }; + + if (value.find(L'=') != std::wstring_view::npos) + { + parseOptions(value, true); + } + else + { + if (IsEmptyOrWhitespace(value)) + { + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName)); + } + + result.Name = WideToMultiByte(std::wstring{value}); + } + + if (result.Name.empty()) + { + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName)); + } + + return result; +} + std::pair ParseLabel(const std::wstring& value) { std::pair result{}; diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h index b25a807e03..f384d1afb0 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.h +++ b/src/windows/wslc/arguments/ArgumentValidation.h @@ -17,6 +17,7 @@ Module Name: #include "ContainerModel.h" #include "InspectModel.h" #include +#include #include #include #include @@ -85,9 +86,18 @@ InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstri void ValidateGpus(const std::vector& values, const std::wstring& argName); void ValidateVolumeMount(const std::vector& values); void ValidateFilter(const std::vector& values); +void ValidateNetwork(const std::vector& values, const std::wstring& argName); std::pair ParseLabel(const std::wstring& value); std::pair ParseDriverOption(const std::wstring& value); std::pair ParseFilter(const std::wstring& value); +struct ParsedNetworkArgument +{ + std::string Name; + std::vector Aliases; +}; + +ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::wstring& argName = {}); + } // namespace wsl::windows::wslc::validation diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index 4d8ff2cc59..3d57c07f21 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -16,7 +16,9 @@ Module Name: #include #include +#include #include +#include namespace wsl::windows::wslc::models { @@ -27,6 +29,12 @@ enum class FormatType Json, }; +struct ContainerNetwork +{ + std::string Name; + std::vector Aliases; +}; + struct ContainerOptions { std::vector Arguments; @@ -57,7 +65,7 @@ struct ContainerOptions std::vector DnsServers; std::vector DnsSearchDomains; std::vector DnsOptions; - std::vector Networks; + std::vector Networks; std::vector NetworkAliases; std::vector Tmpfs; std::vector> Labels; diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index b1854faa57..2fb869cf43 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -41,6 +41,12 @@ static void SetContainerArguments(WSLCProcessOptions& options, std::vector(argsStorage.size())}; } +static bool SupportsNetworkAliases(std::string_view network) +{ + // Aliases are only supported for user-defined networks, not built-in or container-sourced network modes. + return network != "bridge" && network != "host" && network != "none" && !network.starts_with("container:"); +} + static wsl::windows::common::RunningWSLCContainer CreateInternal(Reporter& reporter, Session& session, const std::string& image, const ContainerOptions& options) { WarningCallback warningCallback(reporter); @@ -54,14 +60,20 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Reporter& repor WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll); WI_SetFlagIf(containerFlags, WSLCContainerFlagsGpu, options.Gpu); - std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front(); + std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front().Name; wsl::windows::common::WSLCContainerLauncher containerLauncher( image, options.Name, options.Arguments, options.EnvironmentVariables, std::move(networkMode), processFlags); for (size_t i = 1; i < options.Networks.size(); ++i) { - containerLauncher.AddAdditionalNetwork(options.Networks[i]); + const auto& network = options.Networks[i]; + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, + Localization::MessageWslcAliasRequiresUserDefinedNetwork(), + !network.Aliases.empty() && !SupportsNetworkAliases(network.Name)); + + containerLauncher.AddAdditionalNetwork(network.Name, network.Aliases); } if (!options.NetworkAliases.empty()) @@ -70,13 +82,24 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Reporter& repor THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasAmbiguousWithMultipleNetworks(), options.Networks.size() > 1); + const auto& primary = options.Networks.front().Name; + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary)); + + for (const auto& alias : options.NetworkAliases) + { + containerLauncher.AddPrimaryNetworkAlias(alias); + } + } + + if (!options.Networks.empty()) + { const auto& primary = options.Networks.front(); THROW_HR_WITH_USER_ERROR_IF( E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), - primary == "bridge" || primary == "host" || primary == "none" || primary.starts_with("container:")); + !primary.Aliases.empty() && !SupportsNetworkAliases(primary.Name)); - for (const auto& alias : options.NetworkAliases) + for (const auto& alias : primary.Aliases) { containerLauncher.AddPrimaryNetworkAlias(alias); } diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 52cc96f28a..7e566bbe40 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -848,11 +848,15 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context) if (context.Args.Contains(ArgType::Network)) { + const auto networkArgName = Argument::Create(ArgType::Network).Name(); auto networks = context.Args.GetAll(); options.Networks.reserve(options.Networks.size() + networks.size()); for (const auto& value : networks) { - options.Networks.emplace_back(WideToMultiByte(value)); + auto parsed = validation::ParseNetworkArgument(value, networkArgName); + auto& network = options.Networks.emplace_back(); + network.Name = parsed.Name; + network.Aliases = std::move(parsed.Aliases); } } diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 24c7399932..2fb69a89e6 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -169,6 +169,11 @@ bool NetworkModeAllocatesVmPorts(std::string_view mode) noexcept return mode != "host" && mode != "none" && !mode.starts_with(c_containerNetworkPrefix); } +bool NetworkSupportsAliases(std::string_view mode) noexcept +{ + return mode != "bridge" && NetworkModeAllocatesVmPorts(mode); +} + // Reject `:` strings whose prefix isn't `container:`. Docker treats colon-prefixed // modes (`service:`, `ns:`, ...) as special, but WSLC only supports `container:`. Surface the // rejection here so both Create() and Open() recovery paths share the same gate. @@ -231,9 +236,9 @@ std::string ResolveNetworkMode(LPCSTR networkMode, bool hasRequestedPorts, const return std::string{mode}; } -// Parses the primary endpoint's Settings KVP. Today only "Aliases" is recognised; unknown -// keys are rejected to avoid silently dropping caller data (mirrors the per-connection guard). -EndpointConfig ResolvePrimaryEndpointConfig(const KeyValuePair* settings, ULONG count, std::string_view networkMode) +// Parses an endpoint's Settings KVP. Today only "Aliases" is recognised; unknown keys are rejected +// to avoid silently dropping caller data. +EndpointConfig ResolveEndpointConfig(const KeyValuePair* settings, ULONG count, std::string_view networkName) { EndpointConfig config{}; if (count == 0) @@ -248,7 +253,7 @@ EndpointConfig ResolvePrimaryEndpointConfig(const KeyValuePair* settings, ULONG for (const auto& [key, _] : parsed) { THROW_HR_WITH_USER_ERROR_IF( - E_NOTIMPL, Localization::MessageWslcEndpointSettingsNotSupported(std::string{networkMode}), key != "Aliases"); + E_NOTIMPL, Localization::MessageWslcEndpointSettingsNotSupported(std::string{networkName}), key != "Aliases"); } if (auto it = parsed.find("Aliases"); it != parsed.end()) @@ -256,10 +261,9 @@ EndpointConfig ResolvePrimaryEndpointConfig(const KeyValuePair* settings, ULONG for (const auto& alias : it->second) { THROW_HR_WITH_USER_ERROR_IF( - E_INVALIDARG, - Localization::MessageWslcAliasEmpty(), - alias.empty() || std::all_of(alias.begin(), alias.end(), [](unsigned char ch) { return std::isspace(ch); })); + E_INVALIDARG, Localization::MessageWslcAliasEmpty(), wsl::shared::string::IsEmptyOrWhitespace(std::string_view{alias})); } + config.Aliases = std::move(it->second); } @@ -288,15 +292,17 @@ std::map ResolveEndpoints( auto [it, inserted] = resolved.try_emplace(name); THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDuplicateNetwork(name), !inserted); + auto config = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name); + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), config.Aliases.has_value() && !NetworkSupportsAliases(name)); + if (name != "bridge") { THROW_HR_WITH_USER_ERROR_IF( WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), !sessionNetworks.contains(name)); } - // Per-endpoint Settings are reserved for future use (IPAddress, Aliases, ...). No keys - // are interpreted today, so reject any non-empty payload rather than silently dropping it. - THROW_HR_WITH_USER_ERROR_IF(E_NOTIMPL, Localization::MessageWslcEndpointSettingsNotSupported(name), connections[i].SettingsCount > 0); + it->second = std::move(config); } return resolved; } @@ -1846,13 +1852,13 @@ std::shared_ptr WSLCContainerImpl::Create( containerOptions.ContainerNetwork.Networks, containerOptions.ContainerNetwork.NetworksCount, networkMode, sessionNetworks); auto primaryConfig = - ResolvePrimaryEndpointConfig(containerOptions.ContainerNetwork.Settings, containerOptions.ContainerNetwork.SettingsCount, networkMode); + ResolveEndpointConfig(containerOptions.ContainerNetwork.Settings, containerOptions.ContainerNetwork.SettingsCount, networkMode); // Aliases require a user-defined endpoint. bridge/host/none/container: modes don't support them. THROW_HR_WITH_USER_ERROR_IF( E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), - primaryConfig.Aliases.has_value() && (networkMode == "bridge" || !NetworkModeAllocatesVmPorts(networkMode))); + primaryConfig.Aliases.has_value() && !NetworkSupportsAliases(networkMode)); auto mappedPorts = BuildPortMappings(ports, networkMode, virtualMachine); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index edc13644b6..3e435f6455 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -7161,9 +7161,8 @@ class WSLCTests WSLC_TEST_METHOD(ContainerNetworkEndpointSettingsNotImplementedTest) { - // Per-endpoint Settings (Aliases, IPAMConfig, etc.) are reserved for a future PR. - // Until that lands, any non-empty Settings payload must be rejected with E_NOTIMPL - // so callers don't silently lose data. + // Unsupported per-endpoint settings must be rejected with E_NOTIMPL so callers + // don't silently lose data. const std::string networkName = "custom-net-settings"; LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); @@ -7174,7 +7173,7 @@ class WSLCTests auto networkCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); }); LPCSTR args[] = {"sleep", "99999"}; - KeyValuePair settings[] = {{"Aliases", "my-alias"}}; + KeyValuePair settings[] = {{"IPAddress", "10.0.0.5"}}; WSLCNetworkConnection connection{}; connection.NetworkName = networkName.c_str(); connection.Settings = settings; @@ -7677,6 +7676,53 @@ class WSLCTests VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "backup") != endpoint.Aliases.end()); } + // Aliases on primary and additional user-defined networks — all present. + { + const std::string primaryNetworkName = "alias-net-primary"; + const std::string additionalNetworkName = "alias-net-additional"; + createNetwork(primaryNetworkName, "172.64.0.0/16"); + createNetwork(additionalNetworkName, "172.65.0.0/16"); + auto primaryNetCleanup = + wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetworkName.c_str())); }); + auto additionalNetCleanup = + wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(additionalNetworkName.c_str())); }); + + WSLCContainerLauncher launcher("debian:latest", "alias-ctr-additional", {"sleep", "99999"}, {}, primaryNetworkName); + launcher.AddPrimaryNetworkAlias("db"); + launcher.AddAdditionalNetwork(additionalNetworkName, {"cache", "replica"}); + auto container = launcher.Launch(*m_defaultSession); + + auto inspect = container.Inspect(); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(primaryNetworkName)); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(additionalNetworkName)); + const auto& primaryEndpoint = inspect.NetworkSettings.Networks.at(primaryNetworkName); + const auto& additionalEndpoint = inspect.NetworkSettings.Networks.at(additionalNetworkName); + VERIFY_IS_TRUE(std::ranges::find(primaryEndpoint.Aliases, "db") != primaryEndpoint.Aliases.end()); + VERIFY_IS_TRUE(std::ranges::find(additionalEndpoint.Aliases, "cache") != additionalEndpoint.Aliases.end()); + VERIFY_IS_TRUE(std::ranges::find(additionalEndpoint.Aliases, "replica") != additionalEndpoint.Aliases.end()); + } + + // Aliases on additional built-in/non-user-defined networks — rejected before network lookup. + { + const std::string primaryNetworkName = "alias-net-invalid-additional"; + createNetwork(primaryNetworkName, "172.66.0.0/16"); + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetworkName.c_str())); }); + + auto expectAdditionalNetworkAliasError = [&](const std::string& containerName, const std::string& additionalNetworkName) { + WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}, primaryNetworkName); + launcher.AddAdditionalNetwork(additionalNetworkName, {"db"}); + + auto result = wil::ResultFromException([&] { launcher.Launch(*m_defaultSession); }); + VERIFY_ARE_EQUAL(E_INVALIDARG, result); + ValidateCOMErrorMessage(L"Network aliases require a user-defined network. Use --network to specify one."); + }; + + expectAdditionalNetworkAliasError("alias-ctr-additional-bridge", "bridge"); + expectAdditionalNetworkAliasError("alias-ctr-additional-host", "host"); + expectAdditionalNetworkAliasError("alias-ctr-additional-none", "none"); + expectAdditionalNetworkAliasError("alias-ctr-additional-container", "container:alias-ctr-target"); + } + // Alias on 'host' mode — rejected at the IDL layer. { expectError( diff --git a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp index 9a5ca6218c..ddbe8bd4c0 100644 --- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp @@ -21,6 +21,7 @@ Module Name: #include "AsyncExecution.h" #include "Command.h" #include "RootCommand.h" +#include "ArgumentValidation.h" #include "ContainerCommand.h" #include "ContainerTasks.h" @@ -368,7 +369,7 @@ class WSLCCLIExecutionUnitTests const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(1u, options.Networks.size()); - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); } TEST_METHOD(RunCommand_ParseNetworkMultipleValues_PreservesOrder) @@ -384,8 +385,50 @@ class WSLCCLIExecutionUnitTests const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(2u, options.Networks.size()); - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]); - VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name); + } + + TEST_METHOD(RunCommand_ParseDockerNetworkAliases_SetsPerNetworkAliases) + { + auto invocation = + CreateInvocationFromCommandLine(L"wslc --network name=net1,alias=a,alias=b --network name=net2,alias=c ubuntu sh"); + + ContainerRunCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + command.ValidateArguments(context.Args); + + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); + + const auto& options = context.Data.Get(); + VERIFY_ARE_EQUAL(2u, options.Networks.size()); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size()); + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]); + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]); + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name); + VERIFY_ARE_EQUAL(1u, options.Networks[1].Aliases.size()); + VERIFY_ARE_EQUAL(std::string("c"), options.Networks[1].Aliases[0]); + } + + TEST_METHOD(RunCommand_ParseDockerNetworkAliasesWithNameAfterAlias_SetsPerNetworkAliases) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a,name=net1,alias=b ubuntu sh"); + + ContainerRunCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + command.ValidateArguments(context.Args); + + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); + + const auto& options = context.Data.Get(); + VERIFY_ARE_EQUAL(1u, options.Networks.size()); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size()); + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]); + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]); } TEST_METHOD(RunCommand_ParseNetworkEmptyValue_ThrowsArgumentException) @@ -413,7 +456,7 @@ class WSLCCLIExecutionUnitTests const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(1u, options.Networks.size()); - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); } TEST_METHOD(CreateCommand_ParseNetworkMultipleValues_PreservesOrder) @@ -429,8 +472,160 @@ class WSLCCLIExecutionUnitTests const auto& options = context.Data.Get(); VERIFY_ARE_EQUAL(2u, options.Networks.size()); - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]); - VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name); + } + + TEST_METHOD(CreateCommand_ParseDockerNetworkAliases_SetsPerNetworkAliases) + { + auto invocation = + CreateInvocationFromCommandLine(L"wslc --network name=net1,alias=a,alias=b --network name=net2,alias=c ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + command.ValidateArguments(context.Args); + + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); + + const auto& options = context.Data.Get(); + VERIFY_ARE_EQUAL(2u, options.Networks.size()); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size()); + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]); + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]); + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name); + VERIFY_ARE_EQUAL(1u, options.Networks[1].Aliases.size()); + VERIFY_ARE_EQUAL(std::string("c"), options.Networks[1].Aliases[0]); + } + + TEST_METHOD(CreateCommand_ParseDockerNetworkAliasesWithNameAfterAlias_SetsPerNetworkAliases) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a,name=net1,alias=b ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + command.ValidateArguments(context.Args); + + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context); + + const auto& options = context.Data.Get(); + VERIFY_ARE_EQUAL(1u, options.Networks.size()); + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name); + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size()); + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]); + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]); + } + + TEST_METHOD(CreateCommand_ParseNetworkDuplicateNameOption_ThrowsArgumentException) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,name=net2 ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkDuplicateNameError(L"network"); + return exception.Message() == expectedMessage; + }); + } + + TEST_METHOD(CreateCommand_ParseNetworkUnsupportedOption_ThrowsArgumentException) + { + auto invocation = CreateInvocationFromCommandLine( + L"wslc --network name=net1,driver-opt=com.docker.network.endpoint.sysctls=" + L"net.ipv4.conf.IFNAME.log_martians=1 ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = + wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"driver-opt"); + return exception.Message() == expectedMessage; + }); + } + + TEST_METHOD(CreateCommand_ParseNetworkUnknownOption_ThrowsArgumentException) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,aliases=a ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"aliases"); + return exception.Message() == expectedMessage; + }); + } + + TEST_METHOD(CreateCommand_ParseNetworkBackendAliasesOption_ThrowsArgumentException) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,Aliases=a ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"Aliases"); + return exception.Message() == expectedMessage; + }); + } + + TEST_METHOD(CreateCommand_ParseNetworkAliasWithoutName_ThrowsArgumentException) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network"); + return exception.Message() == expectedMessage; + }); + } + + TEST_METHOD(CreateCommand_ParseNetworkNameWhitespaceValue_ThrowsArgumentException) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"name= \" ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network"); + return exception.Message() == expectedMessage; + }); + } + + TEST_METHOD(ParseNetworkArgument_NameUnicodeWhitespaceValue_ThrowsArgumentException) + { + VERIFY_THROWS_SPECIFIC( + validation::ParseNetworkArgument(L"name=\u3000", L"network"), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network"); + return exception.Message() == expectedMessage; + }); + } + + TEST_METHOD(CreateCommand_ParseNetworkAliasEmptyValue_ThrowsArgumentException) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,alias= ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkAliasEmptyError(L"network"); + return exception.Message() == expectedMessage; + }); } TEST_METHOD(CreateCommand_ParseNetworkEmptyValue_ThrowsArgumentException) @@ -445,6 +640,21 @@ class WSLCCLIExecutionUnitTests command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; }); } + TEST_METHOD(CreateCommand_SetContainerOptionsInvalidNetwork_ThrowsArgumentExceptionWithArgumentName) + { + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,name=net2 ubuntu sh"); + + ContainerCreateCommand command{L""}; + CLIExecutionContext context; + command.ParseArguments(invocation, context.Args); + + VERIFY_THROWS_SPECIFIC( + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context), wsl::windows::wslc::ArgumentException, [](const auto& exception) { + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkDuplicateNameError(L"network"); + return exception.Message() == expectedMessage; + }); + } + // Test: Command Line test parsing all cases defined in CommandLineTestCases.h // This test verifies the command line parsing logic used by the CLI and executes the same // code as the CLI up to the point of command execution, including parsing and argument validtion. diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp index 39fd029997..78346c50d0 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp @@ -1079,6 +1079,39 @@ class WSLCE2EContainerCreateTests VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end()); } + WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_DockerStyleMultiNetwork_Success) + { + const auto secondNetworkName = TestNetworkName + L"-2"; + EnsureNetworkDoesNotExist(secondNetworkName); + + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); }); + + result = RunWslc(std::format(L"network create --driver bridge {}", secondNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto cleanupSecondNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(secondNetworkName); }); + + result = RunWslc(std::format( + L"container create --name {} --network name={},alias=db,alias=primary --network name={},alias=cache {} true", + WslcContainerName, + TestNetworkName, + secondNetworkName, + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName); + const auto secondNetwork = wsl::shared::string::WideToMultiByte(secondNetworkName); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName)); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(secondNetwork)); + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName); + const auto& secondEndpoint = inspect.NetworkSettings.Networks.at(secondNetwork); + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end()); + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "primary") != endpoint.Aliases.end()); + VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "cache") != secondEndpoint.Aliases.end()); + } + WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_NoNetwork_Rejected) { auto result = diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp index 74ac853a64..9903a5d032 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -927,6 +927,41 @@ class WSLCE2EContainerRunTests VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end()); } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_DockerStyleMultiNetwork_Success) + { + const auto secondNetworkName = TestNetworkName + L"-2"; + EnsureNetworkDoesNotExist(secondNetworkName); + + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); }); + + result = RunWslc(std::format(L"network create --driver bridge {}", secondNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto cleanupSecondNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(secondNetworkName); }); + + result = RunWslc(std::format( + L"container run --name {} --network name={},alias=db,alias=primary " + L"--network name={},alias=cache,alias=replica {} true", + WslcContainerName, + TestNetworkName, + secondNetworkName, + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName); + const auto secondNetwork = wsl::shared::string::WideToMultiByte(secondNetworkName); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName)); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(secondNetwork)); + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName); + const auto& secondEndpoint = inspect.NetworkSettings.Networks.at(secondNetwork); + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end()); + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "primary") != endpoint.Aliases.end()); + VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "cache") != secondEndpoint.Aliases.end()); + VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "replica") != secondEndpoint.Aliases.end()); + } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_NoNetwork_Rejected) { auto result =