diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index fccec5f14..77aa05f75 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="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
host mode networking is not supported
diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp
index cdbb50a0c..d88eaebc9 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 0cde0d926..942d24ad0 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 9fc0fa4e5..04d9a0389 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 a2e949e74..cc3cb274e 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.cpp
+++ b/src/windows/wslc/arguments/ArgumentValidation.cpp
@@ -114,19 +114,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;
}
@@ -166,6 +154,34 @@ static const std::unordered_map SignalMap = {
{L"SIGPOLL", WSLCSignalSIGPOLL}, {L"SIGPWR", WSLCSignalSIGPWR}, {L"SIGSYS", WSLCSignalSIGSYS},
};
+namespace {
+ 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)
@@ -192,6 +208,35 @@ 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());
+ }
+ }
+}
+
// 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)
{
@@ -494,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{};
diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h
index 83e0f4eb4..bb54990a6 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 eb7bd2f1a..4ddf05fc1 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;
@@ -51,7 +59,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 9a85ebf98..577f345d2 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -42,6 +42,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(
Session& session, const std::string& image, const ContainerOptions& options, IWarningCallback* warningCallback = nullptr)
{
@@ -54,14 +60,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 +82,24 @@ 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);
}
@@ -627,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.
diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp
index ad7d112d8..b7510e4d2 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 = validation::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 674b0b0cd..f33a2718c 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())
@@ -288,15 +293,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;
}
@@ -1659,13 +1666,13 @@ 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(
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 04b07f9fa..3949acbb4 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,55 @@ 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 9a5ca6218..39509b745 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,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 +455,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 +471,108 @@ 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 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");
+
+ 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 33e6c89ab..084f195bd 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 1f74a95e6..14e5487fd 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 =