From ab2ac59c9f7597cfac4ee73882d78e632cbab5d8 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Tue, 30 Jun 2026 17:42:22 -0700 Subject: [PATCH 01/10] Support Docker-style network aliases Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/windows/common/WSLCContainerLauncher.cpp | 23 ++- src/windows/common/WSLCContainerLauncher.h | 9 +- src/windows/service/inc/wslc.idl | 3 + .../wslc/arguments/ArgumentValidation.cpp | 19 ++- src/windows/wslc/services/ContainerModel.h | 145 +++++++++++++++++- .../wslc/services/ContainerService.cpp | 31 +++- src/windows/wslc/tasks/ContainerTasks.cpp | 9 +- src/windows/wslcsession/WSLCContainer.cpp | 21 ++- test/windows/WSLCTests.cpp | 35 ++++- .../wslc/WSLCCLIExecutionUnitTests.cpp | 70 ++++++++- .../wslc/e2e/WSLCE2EContainerCreateTests.cpp | 33 ++++ .../wslc/e2e/WSLCE2EContainerRunTests.cpp | 35 +++++ 12 files changed, 403 insertions(+), 30 deletions(-) diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index cdbb50a0c8..d88eaebc9a 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -252,7 +252,13 @@ 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) @@ -379,9 +385,22 @@ 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(); diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index 0cde0d926f..942d24ad0a 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); @@ -93,6 +94,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; @@ -112,7 +119,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 9fc0fa4e53..04d9a03891 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -207,6 +207,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 a2e949e741..eb081b9b19 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -116,13 +116,26 @@ void Argument::Validate(const ArgMap& execArgs) const { 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)); })) + const auto parsed = models::ParseNetworkArgument(value); + if (parsed.Error == models::NetworkArgumentParseError::EmptyNetworkName) { throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name)); } + else if (parsed.Error == models::NetworkArgumentParseError::EmptyAlias) + { + throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(m_name)); + } + else if (parsed.Error == models::NetworkArgumentParseError::DuplicateNetworkName) + { + throw ArgumentException(std::format(L"Invalid {} value: network name can only be specified once", m_name)); + } + else if (parsed.Error == models::NetworkArgumentParseError::UnsupportedOption) + { + throw ArgumentException( + std::format(L"Invalid {} value: unsupported network option '{}'", m_name, parsed.ErrorValue)); + } - if (IsEqual(value, L"host", true)) + if (IsEqual(parsed.Name, L"host", true)) { throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError()); } diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index eb7bd2f1a9..f263904326 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -16,10 +16,42 @@ Module Name: #include #include +#include +#include +#include #include +#include +#include namespace wsl::windows::wslc::models { +namespace details { +inline std::wstring ToWideString(std::wstring_view value) +{ + return {value.data(), value.size()}; +} + +inline std::vector SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter) +{ + std::vector parts; + size_t start = 0; + while (start <= value.size()) + { + const auto end = value.find(delimiter, start); + if (end == std::wstring_view::npos) + { + parts.emplace_back(value.substr(start)); + break; + } + + parts.emplace_back(value.substr(start, end - start)); + start = end + 1; + } + + return parts; +} +} // namespace details + // Valid formats for container list output. enum class FormatType { @@ -27,6 +59,117 @@ enum class FormatType Json, }; +enum class NetworkArgumentParseError +{ + None, + EmptyNetworkName, + EmptyAlias, + DuplicateNetworkName, + UnsupportedOption, +}; + +struct ParsedNetworkArgument +{ + std::wstring Name; + std::vector Aliases; + NetworkArgumentParseError Error = NetworkArgumentParseError::None; + std::wstring ErrorValue; +}; + +struct ContainerNetwork +{ + std::string Name; + std::vector Aliases; +}; + +inline ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) +{ + ParsedNetworkArgument result; + + auto parseOptions = [&](std::wstring_view options, bool requireName) { + bool parsedName = false; + for (const auto part : details::SplitPreserveEmpty(options, L',')) + { + const auto separator = part.find(L'='); + if (separator == std::wstring_view::npos) + { + result.Error = NetworkArgumentParseError::UnsupportedOption; + result.ErrorValue = details::ToWideString(part); + return; + } + + const auto key = part.substr(0, separator); + const auto optionValue = part.substr(separator + 1); + if (key == L"name") + { + if (parsedName) + { + result.Error = NetworkArgumentParseError::DuplicateNetworkName; + result.ErrorValue = details::ToWideString(key); + return; + } + + parsedName = true; + result.Name = details::ToWideString(optionValue); + } + else if (key == L"alias") + { + result.Aliases.emplace_back(details::ToWideString(optionValue)); + } + else + { + result.Error = NetworkArgumentParseError::UnsupportedOption; + result.ErrorValue = details::ToWideString(key); + return; + } + } + + if (requireName && !parsedName) + { + result.Error = NetworkArgumentParseError::EmptyNetworkName; + } + }; + + if (value.starts_with(L"name=")) + { + parseOptions(value, true); + } + else + { + result.Name = details::ToWideString(value); + } + + if (result.Error == NetworkArgumentParseError::None) + { + const auto nameIsEmpty = + result.Name.empty() || + std::all_of(result.Name.begin(), result.Name.end(), [](wchar_t c) { + return std::iswspace(static_cast(c)); + }); + if (nameIsEmpty) + { + result.Error = NetworkArgumentParseError::EmptyNetworkName; + return result; + } + + for (const auto& alias : result.Aliases) + { + const auto aliasIsEmpty = + alias.empty() || + std::all_of(alias.begin(), alias.end(), [](wchar_t c) { + return std::iswspace(static_cast(c)); + }); + if (aliasIsEmpty) + { + result.Error = NetworkArgumentParseError::EmptyAlias; + return result; + } + } + } + + return result; +} + struct ContainerOptions { std::vector Arguments; @@ -51,7 +194,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 9a85ebf988..c7b8b89804 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -42,6 +42,11 @@ static void SetContainerArguments(WSLCProcessOptions& options, std::vector(argsStorage.size())}; } +static bool SupportsNetworkAliases(std::string_view network) +{ + return network != "bridge" && network != "host" && network != "none" && !network.starts_with("container:"); +} + static wsl::windows::common::RunningWSLCContainer CreateInternal( Session& session, const std::string& image, const ContainerOptions& options, IWarningCallback* warningCallback = nullptr) { @@ -54,14 +59,20 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal( 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 +81,25 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal( 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 ad7d112d87..168957b838 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -553,7 +553,14 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context) options.Networks.reserve(options.Networks.size() + networks.size()); for (const auto& value : networks) { - options.Networks.emplace_back(WideToMultiByte(value)); + const auto parsed = ParseNetworkArgument(value); + auto& network = options.Networks.emplace_back(); + network.Name = WideToMultiByte(parsed.Name); + network.Aliases.reserve(parsed.Aliases.size()); + for (const auto& alias : parsed.Aliases) + { + network.Aliases.emplace_back(WideToMultiByte(alias)); + } } } diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 674b0b0cd9..80aaade4ce 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -231,9 +231,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 +248,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()) @@ -294,9 +294,13 @@ std::map ResolveEndpoints( 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); + auto config = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name); + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, + Localization::MessageWslcAliasRequiresUserDefinedNetwork(), + config.Aliases.has_value() && name == "bridge"); + + it->second = std::move(config); } return resolved; } @@ -1659,7 +1663,8 @@ std::unique_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( diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 04b07f9fa1..6e9e2941d7 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6743,9 +6743,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())); @@ -6756,7 +6755,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; @@ -7287,6 +7286,34 @@ 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()); + } + // 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..0f2550f441 100644 --- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp @@ -368,7 +368,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 +384,31 @@ 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_ParseNetworkEmptyValue_ThrowsArgumentException) @@ -413,7 +436,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 +452,43 @@ 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_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&) { return true; }); } TEST_METHOD(CreateCommand_ParseNetworkEmptyValue_ThrowsArgumentException) diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp index 33e6c89abf..084f195bd6 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp @@ -916,6 +916,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 1f74a95e61..14e5487fda 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -917,6 +917,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 = From 72e7b7621d50ad290068d573311f15afe3f84065 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 1 Jul 2026 15:59:30 -0700 Subject: [PATCH 02/10] Support network option ordering Treat any --network value containing key/value separators as Docker-style advanced network syntax so aliases can appear before name=. Add run/create parser coverage for alias-before-name ordering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/services/ContainerModel.h | 54 +++++++++---------- .../wslc/WSLCCLIExecutionUnitTests.cpp | 38 +++++++++++++ 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index f263904326..cd8d9701ad 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -26,30 +26,30 @@ Module Name: namespace wsl::windows::wslc::models { namespace details { -inline std::wstring ToWideString(std::wstring_view value) -{ - return {value.data(), value.size()}; -} + inline std::wstring ToWideString(std::wstring_view value) + { + return {value.data(), value.size()}; + } -inline std::vector SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter) -{ - std::vector parts; - size_t start = 0; - while (start <= value.size()) + inline std::vector SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter) { - const auto end = value.find(delimiter, start); - if (end == std::wstring_view::npos) + std::vector parts; + size_t start = 0; + while (start <= value.size()) { - parts.emplace_back(value.substr(start)); - break; + const auto end = value.find(delimiter, start); + if (end == std::wstring_view::npos) + { + parts.emplace_back(value.substr(start)); + break; + } + + parts.emplace_back(value.substr(start, end - start)); + start = end + 1; } - parts.emplace_back(value.substr(start, end - start)); - start = end + 1; + return parts; } - - return parts; -} } // namespace details // Valid formats for container list output. @@ -130,7 +130,7 @@ inline ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) } }; - if (value.starts_with(L"name=")) + if (value.find(L'=') != std::wstring_view::npos) { parseOptions(value, true); } @@ -141,11 +141,9 @@ inline ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) if (result.Error == NetworkArgumentParseError::None) { - const auto nameIsEmpty = - result.Name.empty() || - std::all_of(result.Name.begin(), result.Name.end(), [](wchar_t c) { - return std::iswspace(static_cast(c)); - }); + const auto nameIsEmpty = result.Name.empty() || std::all_of(result.Name.begin(), result.Name.end(), [](wchar_t c) { + return std::iswspace(static_cast(c)); + }); if (nameIsEmpty) { result.Error = NetworkArgumentParseError::EmptyNetworkName; @@ -154,11 +152,9 @@ inline ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) for (const auto& alias : result.Aliases) { - const auto aliasIsEmpty = - alias.empty() || - std::all_of(alias.begin(), alias.end(), [](wchar_t c) { - return std::iswspace(static_cast(c)); - }); + const auto aliasIsEmpty = alias.empty() || std::all_of(alias.begin(), alias.end(), [](wchar_t c) { + return std::iswspace(static_cast(c)); + }); if (aliasIsEmpty) { result.Error = NetworkArgumentParseError::EmptyAlias; diff --git a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp index 0f2550f441..451a433ab9 100644 --- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp @@ -411,6 +411,25 @@ class WSLCCLIExecutionUnitTests 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) { auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"\" ubuntu sh"); @@ -479,6 +498,25 @@ class WSLCCLIExecutionUnitTests 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_ParseNetworkAliasEmptyValue_ThrowsArgumentException) { auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,alias= ubuntu sh"); From 8ddfe2b6c476d1ae96caef93ca9026fcb9716302 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 1 Jul 2026 16:08:07 -0700 Subject: [PATCH 03/10] Localize network validation errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 8 ++++++++ src/windows/wslc/arguments/ArgumentValidation.cpp | 9 ++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index fccec5f14d..648159c2e8 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2884,6 +2884,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="{}"}Command line arguments, file names and string inserts should not be translated + host mode networking is not supported diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index eb081b9b19..97d41aa6a2 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -127,12 +127,11 @@ void Argument::Validate(const ArgMap& execArgs) const } else if (parsed.Error == models::NetworkArgumentParseError::DuplicateNetworkName) { - throw ArgumentException(std::format(L"Invalid {} value: network name can only be specified once", m_name)); + throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(m_name)); } else if (parsed.Error == models::NetworkArgumentParseError::UnsupportedOption) { - throw ArgumentException( - std::format(L"Invalid {} value: unsupported network option '{}'", m_name, parsed.ErrorValue)); + throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(m_name, parsed.ErrorValue)); } if (IsEqual(parsed.Name, L"host", true)) @@ -370,8 +369,8 @@ FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring } else { - throw ArgumentException(std::format( - L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); + throw ArgumentException( + std::format(L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); } } From a1a2b8033df43d58ff752e0cfe5ac0a66e9d6cca Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 1 Jul 2026 16:15:20 -0700 Subject: [PATCH 04/10] Fix network option localization placeholder comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 648159c2e8..77aa05f753 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2890,7 +2890,7 @@ On first run, creates the file with all settings commented out at their defaults Invalid {} value: unsupported network option '{}' - {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated host mode networking is not supported From 7a908df746eafbdaec2701fd6ec65d4ff9b3737f Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 2 Jul 2026 17:03:48 -0700 Subject: [PATCH 05/10] Move network argument parsing to validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../wslc/arguments/ArgumentValidation.cpp | 166 +++++++++++++++--- .../wslc/arguments/ArgumentValidation.h | 21 +++ src/windows/wslc/services/ContainerModel.h | 131 -------------- src/windows/wslc/tasks/ContainerTasks.cpp | 2 +- 4 files changed, 163 insertions(+), 157 deletions(-) diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index 97d41aa6a2..ccdec9a2d3 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -114,31 +114,7 @@ void Argument::Validate(const ArgMap& execArgs) const case ArgType::Network: { - for (const auto& value : execArgs.GetAll()) - { - const auto parsed = models::ParseNetworkArgument(value); - if (parsed.Error == models::NetworkArgumentParseError::EmptyNetworkName) - { - throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name)); - } - else if (parsed.Error == models::NetworkArgumentParseError::EmptyAlias) - { - throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(m_name)); - } - else if (parsed.Error == models::NetworkArgumentParseError::DuplicateNetworkName) - { - throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(m_name)); - } - else if (parsed.Error == models::NetworkArgumentParseError::UnsupportedOption) - { - throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(m_name, parsed.ErrorValue)); - } - - if (IsEqual(parsed.Name, L"host", true)) - { - throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError()); - } - } + validation::ValidateNetwork(execArgs.GetAll(), m_name); break; } @@ -178,6 +154,39 @@ static const std::unordered_map SignalMap = { {L"SIGPOLL", WSLCSignalSIGPOLL}, {L"SIGPWR", WSLCSignalSIGPWR}, {L"SIGSYS", WSLCSignalSIGSYS}, }; +namespace { + std::wstring ToWideString(std::wstring_view value) + { + return {value.data(), value.size()}; + } + + std::vector SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter) + { + std::vector parts; + size_t start = 0; + while (start <= value.size()) + { + const auto end = value.find(delimiter, start); + if (end == std::wstring_view::npos) + { + parts.emplace_back(value.substr(start)); + break; + } + + parts.emplace_back(value.substr(start, end - start)); + start = end + 1; + } + + return parts; + } + + bool IsEmptyOrWhitespace(std::wstring_view value) + { + return value.empty() || + std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast(c)); }); + } +} // namespace + void ValidateWSLCSignalFromString(const std::vector& values, const std::wstring& argName) { for (const auto& value : values) @@ -204,6 +213,113 @@ 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); + if (parsed.Error == NetworkArgumentParseError::EmptyNetworkName) + { + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName)); + } + else if (parsed.Error == NetworkArgumentParseError::EmptyAlias) + { + throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(argName)); + } + else if (parsed.Error == NetworkArgumentParseError::DuplicateNetworkName) + { + throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(argName)); + } + else if (parsed.Error == NetworkArgumentParseError::UnsupportedOption) + { + throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, parsed.ErrorValue)); + } + + if (IsEqual(parsed.Name, L"host", true)) + { + throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError()); + } + } +} + +ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) +{ + 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) + { + result.Error = NetworkArgumentParseError::UnsupportedOption; + result.ErrorValue = ToWideString(part); + return; + } + + const auto key = part.substr(0, separator); + const auto optionValue = part.substr(separator + 1); + if (key == L"name") + { + if (parsedName) + { + result.Error = NetworkArgumentParseError::DuplicateNetworkName; + result.ErrorValue = ToWideString(key); + return; + } + + parsedName = true; + result.Name = ToWideString(optionValue); + } + else if (key == L"alias") + { + result.Aliases.emplace_back(ToWideString(optionValue)); + } + else + { + result.Error = NetworkArgumentParseError::UnsupportedOption; + result.ErrorValue = ToWideString(key); + return; + } + } + + if (requireName && !parsedName) + { + result.Error = NetworkArgumentParseError::EmptyNetworkName; + } + }; + + if (value.find(L'=') != std::wstring_view::npos) + { + parseOptions(value, true); + } + else + { + result.Name = ToWideString(value); + } + + if (result.Error == NetworkArgumentParseError::None) + { + if (IsEmptyOrWhitespace(result.Name)) + { + result.Error = NetworkArgumentParseError::EmptyNetworkName; + return result; + } + + for (const auto& alias : result.Aliases) + { + if (IsEmptyOrWhitespace(alias)) + { + result.Error = NetworkArgumentParseError::EmptyAlias; + return result; + } + } + } + + return result; +} + // 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) { diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h index 83e0f4eb46..bb54990a60 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 @@ -82,9 +83,29 @@ 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); +enum class NetworkArgumentParseError +{ + None, + EmptyNetworkName, + EmptyAlias, + DuplicateNetworkName, + UnsupportedOption, +}; + +struct ParsedNetworkArgument +{ + std::wstring Name; + std::vector Aliases; + NetworkArgumentParseError Error = NetworkArgumentParseError::None; + std::wstring ErrorValue; +}; + +ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value); + } // namespace wsl::windows::wslc::validation diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index cd8d9701ad..4ddf05fc1b 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -16,42 +16,12 @@ Module Name: #include #include -#include -#include #include #include -#include #include namespace wsl::windows::wslc::models { -namespace details { - inline std::wstring ToWideString(std::wstring_view value) - { - return {value.data(), value.size()}; - } - - inline std::vector SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter) - { - std::vector parts; - size_t start = 0; - while (start <= value.size()) - { - const auto end = value.find(delimiter, start); - if (end == std::wstring_view::npos) - { - parts.emplace_back(value.substr(start)); - break; - } - - parts.emplace_back(value.substr(start, end - start)); - start = end + 1; - } - - return parts; - } -} // namespace details - // Valid formats for container list output. enum class FormatType { @@ -59,113 +29,12 @@ enum class FormatType Json, }; -enum class NetworkArgumentParseError -{ - None, - EmptyNetworkName, - EmptyAlias, - DuplicateNetworkName, - UnsupportedOption, -}; - -struct ParsedNetworkArgument -{ - std::wstring Name; - std::vector Aliases; - NetworkArgumentParseError Error = NetworkArgumentParseError::None; - std::wstring ErrorValue; -}; - struct ContainerNetwork { std::string Name; std::vector Aliases; }; -inline ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) -{ - ParsedNetworkArgument result; - - auto parseOptions = [&](std::wstring_view options, bool requireName) { - bool parsedName = false; - for (const auto part : details::SplitPreserveEmpty(options, L',')) - { - const auto separator = part.find(L'='); - if (separator == std::wstring_view::npos) - { - result.Error = NetworkArgumentParseError::UnsupportedOption; - result.ErrorValue = details::ToWideString(part); - return; - } - - const auto key = part.substr(0, separator); - const auto optionValue = part.substr(separator + 1); - if (key == L"name") - { - if (parsedName) - { - result.Error = NetworkArgumentParseError::DuplicateNetworkName; - result.ErrorValue = details::ToWideString(key); - return; - } - - parsedName = true; - result.Name = details::ToWideString(optionValue); - } - else if (key == L"alias") - { - result.Aliases.emplace_back(details::ToWideString(optionValue)); - } - else - { - result.Error = NetworkArgumentParseError::UnsupportedOption; - result.ErrorValue = details::ToWideString(key); - return; - } - } - - if (requireName && !parsedName) - { - result.Error = NetworkArgumentParseError::EmptyNetworkName; - } - }; - - if (value.find(L'=') != std::wstring_view::npos) - { - parseOptions(value, true); - } - else - { - result.Name = details::ToWideString(value); - } - - if (result.Error == NetworkArgumentParseError::None) - { - const auto nameIsEmpty = result.Name.empty() || std::all_of(result.Name.begin(), result.Name.end(), [](wchar_t c) { - return std::iswspace(static_cast(c)); - }); - if (nameIsEmpty) - { - result.Error = NetworkArgumentParseError::EmptyNetworkName; - return result; - } - - for (const auto& alias : result.Aliases) - { - const auto aliasIsEmpty = alias.empty() || std::all_of(alias.begin(), alias.end(), [](wchar_t c) { - return std::iswspace(static_cast(c)); - }); - if (aliasIsEmpty) - { - result.Error = NetworkArgumentParseError::EmptyAlias; - return result; - } - } - } - - return result; -} - struct ContainerOptions { std::vector Arguments; diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 168957b838..b7510e4d2d 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -553,7 +553,7 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context) options.Networks.reserve(options.Networks.size() + networks.size()); for (const auto& value : networks) { - const auto parsed = ParseNetworkArgument(value); + const auto parsed = validation::ParseNetworkArgument(value); auto& network = options.Networks.emplace_back(); network.Name = WideToMultiByte(parsed.Name); network.Aliases.reserve(parsed.Aliases.size()); From 6fa18f5e58efebbcd529286d7ffc45bab1a258ef Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 2 Jul 2026 17:17:19 -0700 Subject: [PATCH 06/10] Remove network parser string helper Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../wslc/arguments/ArgumentValidation.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index ccdec9a2d3..ee704e53e8 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -155,11 +155,6 @@ static const std::unordered_map SignalMap = { }; namespace { - std::wstring ToWideString(std::wstring_view value) - { - return {value.data(), value.size()}; - } - std::vector SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter) { std::vector parts; @@ -254,7 +249,7 @@ ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) if (separator == std::wstring_view::npos) { result.Error = NetworkArgumentParseError::UnsupportedOption; - result.ErrorValue = ToWideString(part); + result.ErrorValue = std::wstring{part}; return; } @@ -265,21 +260,21 @@ ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) if (parsedName) { result.Error = NetworkArgumentParseError::DuplicateNetworkName; - result.ErrorValue = ToWideString(key); + result.ErrorValue = std::wstring{key}; return; } parsedName = true; - result.Name = ToWideString(optionValue); + result.Name = std::wstring{optionValue}; } else if (key == L"alias") { - result.Aliases.emplace_back(ToWideString(optionValue)); + result.Aliases.emplace_back(optionValue); } else { result.Error = NetworkArgumentParseError::UnsupportedOption; - result.ErrorValue = ToWideString(key); + result.ErrorValue = std::wstring{key}; return; } } @@ -296,7 +291,7 @@ ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) } else { - result.Name = ToWideString(value); + result.Name = std::wstring{value}; } if (result.Error == NetworkArgumentParseError::None) From 76900d0848ea08a240112f48914bdbb817b74ee6 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 2 Jul 2026 17:33:25 -0700 Subject: [PATCH 07/10] Group network argument parser with parse helpers Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../wslc/arguments/ArgumentValidation.cpp | 160 +++++++++--------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index ee704e53e8..cc3cb274e3 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -237,84 +237,6 @@ void ValidateNetwork(const std::vector& values, const std::wstring } } -ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) -{ - 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) - { - result.Error = NetworkArgumentParseError::UnsupportedOption; - result.ErrorValue = std::wstring{part}; - return; - } - - const auto key = part.substr(0, separator); - const auto optionValue = part.substr(separator + 1); - if (key == L"name") - { - if (parsedName) - { - result.Error = NetworkArgumentParseError::DuplicateNetworkName; - result.ErrorValue = std::wstring{key}; - return; - } - - parsedName = true; - result.Name = std::wstring{optionValue}; - } - else if (key == L"alias") - { - result.Aliases.emplace_back(optionValue); - } - else - { - result.Error = NetworkArgumentParseError::UnsupportedOption; - result.ErrorValue = std::wstring{key}; - return; - } - } - - if (requireName && !parsedName) - { - result.Error = NetworkArgumentParseError::EmptyNetworkName; - } - }; - - if (value.find(L'=') != std::wstring_view::npos) - { - parseOptions(value, true); - } - else - { - result.Name = std::wstring{value}; - } - - if (result.Error == NetworkArgumentParseError::None) - { - if (IsEmptyOrWhitespace(result.Name)) - { - result.Error = NetworkArgumentParseError::EmptyNetworkName; - return result; - } - - for (const auto& alias : result.Aliases) - { - if (IsEmptyOrWhitespace(alias)) - { - result.Error = NetworkArgumentParseError::EmptyAlias; - return result; - } - } - } - - return result; -} - // 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) { @@ -480,8 +402,8 @@ FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring } else { - throw ArgumentException( - std::format(L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); + throw ArgumentException(std::format( + L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); } } @@ -617,6 +539,84 @@ std::tuple ParseUlimit(const std::wstring& input, return {WideToMultiByte(input.substr(0, equalsPos)), soft, hard}; } +ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value) +{ + 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) + { + result.Error = NetworkArgumentParseError::UnsupportedOption; + result.ErrorValue = std::wstring{part}; + return; + } + + const auto key = part.substr(0, separator); + const auto optionValue = part.substr(separator + 1); + if (key == L"name") + { + if (parsedName) + { + result.Error = NetworkArgumentParseError::DuplicateNetworkName; + result.ErrorValue = std::wstring{key}; + return; + } + + parsedName = true; + result.Name = std::wstring{optionValue}; + } + else if (key == L"alias") + { + result.Aliases.emplace_back(optionValue); + } + else + { + result.Error = NetworkArgumentParseError::UnsupportedOption; + result.ErrorValue = std::wstring{key}; + return; + } + } + + if (requireName && !parsedName) + { + result.Error = NetworkArgumentParseError::EmptyNetworkName; + } + }; + + if (value.find(L'=') != std::wstring_view::npos) + { + parseOptions(value, true); + } + else + { + result.Name = std::wstring{value}; + } + + if (result.Error == NetworkArgumentParseError::None) + { + if (IsEmptyOrWhitespace(result.Name)) + { + result.Error = NetworkArgumentParseError::EmptyNetworkName; + return result; + } + + for (const auto& alias : result.Aliases) + { + if (IsEmptyOrWhitespace(alias)) + { + result.Error = NetworkArgumentParseError::EmptyAlias; + return result; + } + } + } + + return result; +} + std::pair ParseLabel(const std::wstring& value) { std::pair result{}; From 51f7fbb7f35a6b4cb032fca10de6d9e18fcd2db5 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 2 Jul 2026 17:38:47 -0700 Subject: [PATCH 08/10] Clarify network alias mode check Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/services/ContainerService.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index c7b8b89804..577f345d2d 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -44,6 +44,7 @@ static void SetContainerArguments(WSLCProcessOptions& options, std::vector 1); const auto& primary = options.Networks.front().Name; - THROW_HR_WITH_USER_ERROR_IF( - E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary)); + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary)); for (const auto& alias : options.NetworkAliases) { @@ -650,13 +650,15 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, since, until, tail)); wsl::windows::common::io::MultiHandleWait io; - io.AddHandle(std::make_unique>( - stdoutHandle.Release(), GetStdHandle(STD_OUTPUT_HANDLE))); + io.AddHandle( + std::make_unique>( + stdoutHandle.Release(), GetStdHandle(STD_OUTPUT_HANDLE))); if (!stderrHandle.Empty()) // This handle is only used for non-tty processes. { - io.AddHandle(std::make_unique>( - stderrHandle.Release(), GetStdHandle(STD_ERROR_HANDLE))); + io.AddHandle( + std::make_unique>( + stderrHandle.Release(), GetStdHandle(STD_ERROR_HANDLE))); } // TODO: Handle ctrl-c. From 5c0419137ffc3e75dfec2984810fb3fbdc6bfdf2 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 2 Jul 2026 17:52:22 -0700 Subject: [PATCH 09/10] Add WSLC network validation tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../wslc/WSLCCLIExecutionUnitTests.cpp | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp index 451a433ab9..39509b7455 100644 --- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp @@ -517,6 +517,52 @@ class WSLCCLIExecutionUnitTests 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 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_ParseNetworkAliasEmptyValue_ThrowsArgumentException) { auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,alias= ubuntu sh"); From 9514479738c3f26091f5291c2f4d268647fd616d Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 2 Jul 2026 18:09:29 -0700 Subject: [PATCH 10/10] Validate aliases on additional network modes Reject endpoint aliases on non-user-defined additional network names before the network lookup so built-in modes get the same user-facing error as primary network aliases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCContainer.cpp | 20 +++++++++++--------- test/windows/WSLCTests.cpp | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 80aaade4ce..f33a2718c4 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. @@ -288,18 +293,16 @@ 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)); } - auto config = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name); - THROW_HR_WITH_USER_ERROR_IF( - E_INVALIDARG, - Localization::MessageWslcAliasRequiresUserDefinedNetwork(), - config.Aliases.has_value() && name == "bridge"); - it->second = std::move(config); } return resolved; @@ -1663,14 +1666,13 @@ std::unique_ptr WSLCContainerImpl::Create( containerOptions.ContainerNetwork.Networks, containerOptions.ContainerNetwork.NetworksCount, networkMode, sessionNetworks); auto primaryConfig = - ResolveEndpointConfig( - 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 6e9e2941d7..3949acbb43 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -7314,6 +7314,27 @@ class WSLCTests 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(