diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index aa0f5bf72c..17e9126cbe 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2134,10 +2134,6 @@ Usage:
Container '{}' is running.
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
-
- ContainerIpAddress is not yet supported.
- {Locked="ContainerIpAddress"}Command line arguments, file names and string inserts should not be translated
-
Port mappings are not supported with container network mode; ports are owned by the target container.
@@ -2383,18 +2379,33 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Invalid stop timeout value: {}
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
-
- Additional networks are not allowed when the primary network mode is 'host' or 'none'.
- {Locked="host"}{Locked="none"}Command line arguments, file names and string inserts should not be translated
+
+ The primary network mode '{}' does not support connecting or disconnecting additional networks.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
Duplicate network: '{}'
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
-
- Endpoint settings are not yet supported (network '{}').
+
+ Duplicate driver option '{}'.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Invalid driver option '{}'; expected 'key=value'.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Unknown endpoint setting '{}' for network '{}'.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Only one IP address may be specified for network '{}'.
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ Network link cannot be empty.
+
Network alias cannot be empty.
@@ -2402,6 +2413,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Network aliases require a user-defined network. Use --network to specify one.
{Locked="--network "}Command line arguments, file names and string inserts should not be translated
+
+ Endpoint settings are not supported for network mode '{}'.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
Network aliases cannot be specified when multiple networks are requested. Use a single --network argument.
{Locked="--network "}Command line arguments, file names and string inserts should not be translated
@@ -2992,6 +3007,22 @@ On first run, creates the file with all settings commented out at their defaults
Add a network-scoped alias for the container
+
+ Set endpoint driver options (--driver-opt key=value)
+ {Locked="--driver-opt "}Command line arguments, file names and string inserts should not be translated
+
+
+ Assign the container an IPv4 address on the network
+ {Locked="IPv4"}Command line arguments, file names and string inserts should not be translated
+
+
+ Add link to another container (--link name:alias)
+ {Locked="--link "}Command line arguments, file names and string inserts should not be translated
+
+
+ Add a link-local IPv4 address for the container
+ {Locked="IPv4"}Command line arguments, file names and string inserts should not be translated
+
Invalid {} value: network alias cannot be empty or whitespace
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h
index 87b383a829..549fb023ea 100644
--- a/src/windows/inc/docker_schema.h
+++ b/src/windows/inc/docker_schema.h
@@ -166,14 +166,78 @@ struct Network
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Options, Labels);
};
+struct EndpointIPAMConfig
+{
+ std::string IPv4Address;
+ std::optional> LinkLocalIPs;
+};
+
+inline void to_json(nlohmann::json& j, const EndpointIPAMConfig& v)
+{
+ j = nlohmann::json::object();
+ if (!v.IPv4Address.empty())
+ {
+ j["IPv4Address"] = v.IPv4Address;
+ }
+ if (v.LinkLocalIPs.has_value() && !v.LinkLocalIPs->empty())
+ {
+ j["LinkLocalIPs"] = *v.LinkLocalIPs;
+ }
+}
+
+struct EndpointConfig
+{
+ std::optional> Aliases;
+ std::optional IPAMConfig;
+ std::optional> Links;
+ std::optional> DriverOpts;
+};
+
+inline void to_json(nlohmann::json& j, const EndpointConfig& v)
+{
+ j = nlohmann::json::object();
+ if (v.Aliases.has_value() && !v.Aliases->empty())
+ {
+ j["Aliases"] = *v.Aliases;
+ }
+ if (v.IPAMConfig.has_value())
+ {
+ auto ipam = nlohmann::json(*v.IPAMConfig);
+ if (!ipam.empty())
+ {
+ j["IPAMConfig"] = std::move(ipam);
+ }
+ }
+ if (v.Links.has_value() && !v.Links->empty())
+ {
+ j["Links"] = *v.Links;
+ }
+ if (v.DriverOpts.has_value() && !v.DriverOpts->empty())
+ {
+ j["DriverOpts"] = *v.DriverOpts;
+ }
+}
+
struct ContainerNetworkRequest
{
using TResponse = void;
std::string Container;
-
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerNetworkRequest, Container);
+ std::optional EndpointConfig;
};
+inline void to_json(nlohmann::json& j, const ContainerNetworkRequest& v)
+{
+ j = nlohmann::json{{"Container", v.Container}};
+ if (v.EndpointConfig.has_value())
+ {
+ auto endpoint = nlohmann::json(*v.EndpointConfig);
+ if (!endpoint.empty())
+ {
+ j["EndpointConfig"] = std::move(endpoint);
+ }
+ }
+}
+
struct Mount
{
std::string Name;
@@ -245,6 +309,14 @@ struct HostConfig
HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs, Devices, DeviceRequests, ShmSize, Memory, NanoCpus, Ulimits);
};
+struct InspectEndpointIPAMConfig
+{
+ std::string IPv4Address;
+ std::optional> LinkLocalIPs;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointIPAMConfig, IPv4Address, LinkLocalIPs);
+};
+
struct EndpointSettings
{
std::string IPAddress;
@@ -252,24 +324,13 @@ struct EndpointSettings
std::string MacAddress;
int IPPrefixLen{};
std::optional> Aliases;
+ std::optional> Links;
+ std::optional> DriverOpts;
+ std::optional IPAMConfig;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases, Links, DriverOpts, IPAMConfig);
};
-struct EndpointConfig
-{
- std::optional> Aliases;
-};
-
-inline void to_json(nlohmann::json& j, const EndpointConfig& v)
-{
- j = nlohmann::json::object();
- if (v.Aliases.has_value() && !v.Aliases->empty())
- {
- j["Aliases"] = *v.Aliases;
- }
-}
-
struct NetworkingConfig
{
std::map EndpointsConfig;
diff --git a/src/windows/inc/wslc_schema.h b/src/windows/inc/wslc_schema.h
index ccbd20a1fd..92c9b9eb22 100644
--- a/src/windows/inc/wslc_schema.h
+++ b/src/windows/inc/wslc_schema.h
@@ -113,6 +113,14 @@ struct ContainerConfig
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout, Healthcheck);
};
+struct InspectEndpointIPAMConfig
+{
+ std::string IPv4Address;
+ std::vector LinkLocalIPs;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointIPAMConfig, IPv4Address, LinkLocalIPs);
+};
+
struct InspectEndpointSettings
{
std::string IPAddress;
@@ -120,8 +128,11 @@ struct InspectEndpointSettings
std::string MacAddress;
int IPPrefixLen{};
std::vector Aliases;
+ std::vector Links;
+ std::map DriverOpts;
+ std::optional IPAMConfig;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases, Links, DriverOpts, IPAMConfig);
};
struct InspectNetworkSettings
diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl
index 35e272a71f..e0f67727a9 100644
--- a/src/windows/service/inc/wslc.idl
+++ b/src/windows/service/inc/wslc.idl
@@ -232,7 +232,10 @@ typedef struct _WSLCNetworkConnection
typedef struct _WSLCNetworkConnectionOptions
{
[unique] LPCSTR NetworkName;
- [unique] LPCSTR ContainerIpAddress; // Reserved for future --ip support; must be NULL today.
+ // Endpoint settings for the network connection (Aliases, IPAddress, Links, LinkLocalIPs, DriverOpts).
+ // KVP-encoded; duplicate keys are allowed (e.g., multiple "Aliases" entries).
+ [unique, size_is(SettingsCount)] const KeyValuePair* Settings;
+ ULONG SettingsCount;
} WSLCNetworkConnectionOptions;
typedef struct _WSLCContainerNetwork
diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h
index 268ab69ff8..db463e711b 100644
--- a/src/windows/wslc/arguments/ArgumentDefinitions.h
+++ b/src/windows/wslc/arguments/ArgumentDefinitions.h
@@ -51,6 +51,7 @@ _(DNSOption, "dns-option", NO_ALIAS, Kind::Value, L
_(DNSSearch, "dns-search", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSSearchArgDescription()) \
_(Domainname, "domainname", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DomainnameArgDescription()) \
_(Driver, "driver", L"d", Kind::Value, Localization::WSLCCLI_DriverOptionDescription()) \
+_(DriverOpt, "driver-opt", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DriverOptArgDescription()) \
_(Entrypoint, "entrypoint", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EntrypointArgDescription()) \
_(Env, "env", L"e", Kind::Value, Localization::WSLCCLI_EnvArgDescription()) \
_(EnvFile, "env-file", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EnvFileArgDescription()) \
@@ -78,9 +79,12 @@ _(ImportFile, "file", NO_ALIAS, Kind::Positional, L
_(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \
_(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \
_(Internal, "internal", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NetworkInternalArgDescription()) \
+_(IpAddress, "ip", NO_ALIAS, Kind::Value, Localization::WSLCCLI_IpAddressArgDescription()) \
_(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
_(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
_(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
+_(Link, "link", NO_ALIAS, Kind::Value, Localization::WSLCCLI_LinkArgDescription()) \
+_(LinkLocalIp, "link-local-ip", NO_ALIAS, Kind::Value, Localization::WSLCCLI_LinkLocalIpArgDescription()) \
_(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \
_(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
_(Network, "network", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkArgDescription()) \
diff --git a/src/windows/wslc/commands/NetworkConnectCommand.cpp b/src/windows/wslc/commands/NetworkConnectCommand.cpp
index 462af190fb..daee586b7d 100644
--- a/src/windows/wslc/commands/NetworkConnectCommand.cpp
+++ b/src/windows/wslc/commands/NetworkConnectCommand.cpp
@@ -29,6 +29,11 @@ std::vector NetworkConnectCommand::GetArguments() const
return {
Argument::Create(ArgType::NetworkName, true),
Argument::Create(ArgType::ContainerId, true),
+ Argument::Create(ArgType::DriverOpt, false, NO_LIMIT),
+ Argument::Create(ArgType::IpAddress, false),
+ Argument::Create(ArgType::Link, false, NO_LIMIT),
+ Argument::Create(ArgType::LinkLocalIp, false, NO_LIMIT),
+ Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
};
}
@@ -44,7 +49,8 @@ std::wstring NetworkConnectCommand::LongDescription() const
void NetworkConnectCommand::ExecuteInternal(CLIExecutionContext& context) const
{
- context << ResolveSession //
+ context << ResolveSession //
+ << SetNetworkEndpointOptionsFromArgs //
<< ConnectNetwork;
}
} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/core/ExecutionContextData.h b/src/windows/wslc/core/ExecutionContextData.h
index b43018e2c9..93c185884f 100644
--- a/src/windows/wslc/core/ExecutionContextData.h
+++ b/src/windows/wslc/core/ExecutionContextData.h
@@ -15,6 +15,7 @@ Module Name:
#include "EnumVariantMap.h"
#include "ContainerModel.h"
#include "ImageModel.h"
+#include "NetworkModel.h"
#include "SessionModel.h"
#include "wslc.h"
@@ -39,6 +40,7 @@ enum class Data : size_t
Images,
Volumes,
Networks,
+ NetworkEndpointOptions,
Max
};
@@ -55,6 +57,7 @@ namespace details {
DEFINE_DATA_MAPPING(Images, std::vector);
DEFINE_DATA_MAPPING(Volumes, std::vector);
DEFINE_DATA_MAPPING(Networks, std::vector);
+ DEFINE_DATA_MAPPING(NetworkEndpointOptions, wsl::windows::wslc::models::NetworkEndpointOptions);
} // namespace details
struct DataMap : wsl::windows::wslc::EnumBasedVariantMap
diff --git a/src/windows/wslc/services/NetworkModel.h b/src/windows/wslc/services/NetworkModel.h
index c696f6eb1f..62eb04daef 100644
--- a/src/windows/wslc/services/NetworkModel.h
+++ b/src/windows/wslc/services/NetworkModel.h
@@ -30,6 +30,26 @@ struct CreateNetworkOptions
std::optional Gateway;
};
+struct NetworkEndpointOptions
+{
+ std::vector Aliases;
+ std::optional IpAddress;
+ std::vector Links;
+ std::vector LinkLocalIps;
+ std::vector DriverOpts;
+};
+
+struct ConnectNetworkOptions
+{
+ std::string NetworkName;
+ std::string ContainerId;
+ std::vector Aliases;
+ std::optional IpAddress;
+ std::vector Links;
+ std::vector LinkLocalIps;
+ std::vector DriverOpts;
+};
+
struct PruneNetworksResult
{
std::vector PrunedNetworks;
diff --git a/src/windows/wslc/services/NetworkService.cpp b/src/windows/wslc/services/NetworkService.cpp
index 5e98756d51..052f645c4f 100644
--- a/src/windows/wslc/services/NetworkService.cpp
+++ b/src/windows/wslc/services/NetworkService.cpp
@@ -116,13 +116,46 @@ models::PruneNetworksResult NetworkService::Prune(models::Session& session, cons
return result;
}
-void NetworkService::Connect(models::Session& session, const std::string& networkName, const std::string& containerId)
+void NetworkService::Connect(models::Session& session, const models::ConnectNetworkOptions& connectOptions)
{
wil::com_ptr container;
- THROW_IF_FAILED(session.Get()->OpenContainer(containerId.c_str(), &container));
+ THROW_IF_FAILED(session.Get()->OpenContainer(connectOptions.ContainerId.c_str(), &container));
+
+ // Build KVPs so the pointers remain valid for the duration of the ConnectToNetwork call.
+ std::vector settings;
+ settings.reserve(
+ connectOptions.Aliases.size() + connectOptions.Links.size() + connectOptions.LinkLocalIps.size() +
+ connectOptions.DriverOpts.size() + (connectOptions.IpAddress.has_value() ? 1 : 0));
+
+ for (const auto& alias : connectOptions.Aliases)
+ {
+ settings.push_back({.Key = "Aliases", .Value = alias.c_str()});
+ }
+
+ if (connectOptions.IpAddress.has_value())
+ {
+ settings.push_back({.Key = "IPAddress", .Value = connectOptions.IpAddress->c_str()});
+ }
+
+ for (const auto& link : connectOptions.Links)
+ {
+ settings.push_back({.Key = "Links", .Value = link.c_str()});
+ }
+
+ for (const auto& linkLocalIp : connectOptions.LinkLocalIps)
+ {
+ settings.push_back({.Key = "LinkLocalIPs", .Value = linkLocalIp.c_str()});
+ }
+
+ for (const auto& entry : connectOptions.DriverOpts)
+ {
+ settings.push_back({.Key = "DriverOpts", .Value = entry.c_str()});
+ }
WSLCNetworkConnectionOptions options{};
- options.NetworkName = networkName.c_str();
+ options.NetworkName = connectOptions.NetworkName.c_str();
+ options.Settings = settings.empty() ? nullptr : settings.data();
+ options.SettingsCount = static_cast(settings.size());
THROW_IF_FAILED(container->ConnectToNetwork(&options));
}
diff --git a/src/windows/wslc/services/NetworkService.h b/src/windows/wslc/services/NetworkService.h
index e0e30db79b..ab66a30b7d 100644
--- a/src/windows/wslc/services/NetworkService.h
+++ b/src/windows/wslc/services/NetworkService.h
@@ -25,7 +25,7 @@ struct NetworkService
static std::vector List(models::Session& session);
static wsl::windows::common::wslc_schema::Network Inspect(models::Session& session, const std::string& name);
static models::PruneNetworksResult Prune(models::Session& session, const std::vector>& filters = {});
- static void Connect(models::Session& session, const std::string& networkName, const std::string& containerId);
+ static void Connect(models::Session& session, const models::ConnectNetworkOptions& connectOptions);
static void Disconnect(models::Session& session, const std::string& networkName, const std::string& containerId);
};
} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp
index 423cd74ada..dc33b3f39e 100644
--- a/src/windows/wslc/tasks/NetworkTasks.cpp
+++ b/src/windows/wslc/tasks/NetworkTasks.cpp
@@ -231,12 +231,21 @@ void PruneNetworks(CLIExecutionContext& context)
void ConnectNetwork(CLIExecutionContext& context)
{
WI_ASSERT(context.Data.Contains(Data::Session));
+ WI_ASSERT(context.Data.Contains(Data::NetworkEndpointOptions));
WI_ASSERT(context.Args.Contains(ArgType::NetworkName));
WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
- const auto networkName = WideToMultiByte(context.Args.Get());
- const auto containerId = WideToMultiByte(context.Args.Get());
- NetworkService::Connect(context.Data.Get(), networkName, containerId);
+ const auto& endpoint = context.Data.Get();
+ models::ConnectNetworkOptions options{};
+ options.NetworkName = WideToMultiByte(context.Args.Get());
+ options.ContainerId = WideToMultiByte(context.Args.Get());
+ options.Aliases = endpoint.Aliases;
+ options.IpAddress = endpoint.IpAddress;
+ options.Links = endpoint.Links;
+ options.LinkLocalIps = endpoint.LinkLocalIps;
+ options.DriverOpts = endpoint.DriverOpts;
+
+ NetworkService::Connect(context.Data.Get(), options);
}
void DisconnectNetwork(CLIExecutionContext& context)
@@ -249,4 +258,36 @@ void DisconnectNetwork(CLIExecutionContext& context)
const auto containerId = WideToMultiByte(context.Args.Get());
NetworkService::Disconnect(context.Data.Get(), networkName, containerId);
}
+
+void SetNetworkEndpointOptionsFromArgs(CLIExecutionContext& context)
+{
+ models::NetworkEndpointOptions options{};
+
+ for (const auto& alias : context.Args.GetAll())
+ {
+ options.Aliases.emplace_back(WideToMultiByte(alias));
+ }
+
+ if (context.Args.Contains(ArgType::IpAddress))
+ {
+ options.IpAddress = WideToMultiByte(context.Args.Get());
+ }
+
+ for (const auto& link : context.Args.GetAll())
+ {
+ options.Links.emplace_back(WideToMultiByte(link));
+ }
+
+ for (const auto& linkLocalIp : context.Args.GetAll())
+ {
+ options.LinkLocalIps.emplace_back(WideToMultiByte(linkLocalIp));
+ }
+
+ for (const auto& driverOpt : context.Args.GetAll())
+ {
+ options.DriverOpts.emplace_back(WideToMultiByte(driverOpt));
+ }
+
+ context.Data.Add(std::move(options));
+}
} // namespace wsl::windows::wslc::task
diff --git a/src/windows/wslc/tasks/NetworkTasks.h b/src/windows/wslc/tasks/NetworkTasks.h
index ad590ae1a1..5ff66f7743 100644
--- a/src/windows/wslc/tasks/NetworkTasks.h
+++ b/src/windows/wslc/tasks/NetworkTasks.h
@@ -24,4 +24,5 @@ void ListNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
void PruneNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
void ConnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context);
void DisconnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context);
+void SetNetworkEndpointOptionsFromArgs(wsl::windows::wslc::execution::CLIExecutionContext& context);
} // namespace wsl::windows::wslc::task
diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp
index 24c7399932..318df6797f 100644
--- a/src/windows/wslcsession/WSLCContainer.cpp
+++ b/src/windows/wslcsession/WSLCContainer.cpp
@@ -231,9 +231,8 @@ 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)
+// Unknown Settings keys are rejected rather than silently dropped, so callers get a clear error.
+EndpointConfig ResolveEndpointConfig(const KeyValuePair* settings, ULONG count, std::string_view networkName)
{
EndpointConfig config{};
if (count == 0)
@@ -245,24 +244,83 @@ EndpointConfig ResolvePrimaryEndpointConfig(const KeyValuePair* settings, ULONG
auto parsed = ParseKeyMultiValuePairs(settings, count);
+ static constexpr std::array knownKeys{"Aliases", "IPAddress", "Links", "LinkLocalIPs", "DriverOpts"};
for (const auto& [key, _] : parsed)
{
THROW_HR_WITH_USER_ERROR_IF(
- E_NOTIMPL, Localization::MessageWslcEndpointSettingsNotSupported(std::string{networkMode}), key != "Aliases");
+ E_INVALIDARG,
+ Localization::MessageWslcEndpointSettingUnknown(key, std::string{networkName}),
+ std::find(knownKeys.begin(), knownKeys.end(), key) == knownKeys.end());
}
+ auto isBlank = [](const std::string& value) {
+ return value.empty() || std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); });
+ };
+
if (auto it = parsed.find("Aliases"); it != parsed.end())
{
for (const auto& alias : it->second)
{
- THROW_HR_WITH_USER_ERROR_IF(
- E_INVALIDARG,
- Localization::MessageWslcAliasEmpty(),
- alias.empty() || std::all_of(alias.begin(), alias.end(), [](unsigned char ch) { return std::isspace(ch); }));
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasEmpty(), isBlank(alias));
}
config.Aliases = std::move(it->second);
}
+ if (auto it = parsed.find("IPAddress"); it != parsed.end())
+ {
+ THROW_HR_WITH_USER_ERROR_IF(
+ E_INVALIDARG, Localization::MessageWslcIpAddressSingleValue(std::string{networkName}), it->second.size() != 1);
+
+ const auto& address = it->second.front();
+ in_addr parsedAddress{};
+ ParseIpv4Address(address.c_str(), parsedAddress);
+
+ EndpointIPAMConfig ipam{};
+ ipam.IPv4Address = address;
+ config.IPAMConfig = std::move(ipam);
+ }
+
+ if (auto it = parsed.find("Links"); it != parsed.end())
+ {
+ for (const auto& link : it->second)
+ {
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcLinkEmpty(), isBlank(link));
+ }
+ config.Links = std::move(it->second);
+ }
+
+ if (auto it = parsed.find("LinkLocalIPs"); it != parsed.end())
+ {
+ for (const auto& address : it->second)
+ {
+ in_addr parsedAddress{};
+ ParseIpv4Address(address.c_str(), parsedAddress);
+ }
+ if (!config.IPAMConfig.has_value())
+ {
+ config.IPAMConfig = EndpointIPAMConfig{};
+ }
+ config.IPAMConfig->LinkLocalIPs = std::move(it->second);
+ }
+
+ if (auto it = parsed.find("DriverOpts"); it != parsed.end())
+ {
+ std::map driverOpts;
+ for (const auto& entry : it->second)
+ {
+ const auto separator = entry.find('=');
+ THROW_HR_WITH_USER_ERROR_IF(
+ E_INVALIDARG, Localization::MessageWslcDriverOptInvalid(entry), separator == std::string::npos || separator == 0);
+
+ auto key = entry.substr(0, separator);
+ auto value = entry.substr(separator + 1);
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDriverOptInvalid(entry), isBlank(key));
+ THROW_HR_WITH_USER_ERROR_IF(
+ E_INVALIDARG, Localization::MessageWslcDriverOptDuplicate(key), !driverOpts.try_emplace(key, std::move(value)).second);
+ }
+ config.DriverOpts = std::move(driverOpts);
+ }
+
return config;
}
@@ -294,9 +352,7 @@ 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);
+ it->second = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name);
}
return resolved;
}
@@ -1523,6 +1579,15 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
wslcEndpoint.MacAddress = endpoint.MacAddress;
wslcEndpoint.IPPrefixLen = endpoint.IPPrefixLen;
wslcEndpoint.Aliases = endpoint.Aliases.value_or(std::vector{});
+ wslcEndpoint.Links = endpoint.Links.value_or(std::vector{});
+ wslcEndpoint.DriverOpts = endpoint.DriverOpts.value_or(std::map{});
+ if (endpoint.IPAMConfig.has_value())
+ {
+ wslc_schema::InspectEndpointIPAMConfig ipam{};
+ ipam.IPv4Address = endpoint.IPAMConfig->IPv4Address;
+ ipam.LinkLocalIPs = endpoint.IPAMConfig->LinkLocalIPs.value_or(std::vector{});
+ wslcEndpoint.IPAMConfig = std::move(ipam);
+ }
wslcInspect.NetworkSettings.Networks[name] = std::move(wslcEndpoint);
}
@@ -1846,29 +1911,32 @@ std::shared_ptr WSLCContainerImpl::Create(
containerOptions.ContainerNetwork.Networks, containerOptions.ContainerNetwork.NetworksCount, networkMode, sessionNetworks);
auto primaryConfig =
- ResolvePrimaryEndpointConfig(containerOptions.ContainerNetwork.Settings, containerOptions.ContainerNetwork.SettingsCount, networkMode);
+ ResolveEndpointConfig(containerOptions.ContainerNetwork.Settings, containerOptions.ContainerNetwork.SettingsCount, networkMode);
- // Aliases require a user-defined endpoint. bridge/host/none/container: modes don't support them.
THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG,
Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
primaryConfig.Aliases.has_value() && (networkMode == "bridge" || !NetworkModeAllocatesVmPorts(networkMode)));
+ const bool hasNonAliasEndpointSettings =
+ primaryConfig.IPAMConfig.has_value() || primaryConfig.Links.has_value() || primaryConfig.DriverOpts.has_value();
+ // N.B. NetworkModeAllocatesVmPorts is reused here as the "supports endpoint settings" predicate: modes
+ // that lack a dedicated netns (host/none/container:*) also can't accept per-endpoint settings.
+ THROW_HR_WITH_USER_ERROR_IF(
+ E_INVALIDARG,
+ Localization::MessageWslcEndpointSettingsRequireNetwork(networkMode),
+ hasNonAliasEndpointSettings && !NetworkModeAllocatesVmPorts(networkMode));
+
auto mappedPorts = BuildPortMappings(ports, networkMode, virtualMachine);
request.HostConfig.NetworkMode = networkMode;
request.NetworkingConfig.EndpointsConfig = std::move(endpoints);
- // Docker API v1.44 (Docker 25.x) requires the primary network to be present in EndpointsConfig
- // when EndpointsConfig is non-empty. Insert it so Docker attaches all networks at create time.
- // Also insert when the caller supplied aliases for the primary endpoint.
- if (NetworkModeAllocatesVmPorts(networkMode) && (!request.NetworkingConfig.EndpointsConfig.empty() || primaryConfig.Aliases.has_value()))
+ const bool hasPrimaryEndpointSettings = primaryConfig.Aliases.has_value() || primaryConfig.IPAMConfig.has_value() ||
+ primaryConfig.Links.has_value() || primaryConfig.DriverOpts.has_value();
+ if (NetworkModeAllocatesVmPorts(networkMode) && (!request.NetworkingConfig.EndpointsConfig.empty() || hasPrimaryEndpointSettings))
{
- auto [it, _] = request.NetworkingConfig.EndpointsConfig.try_emplace(networkMode);
- if (primaryConfig.Aliases.has_value())
- {
- it->second.Aliases = std::move(primaryConfig.Aliases);
- }
+ request.NetworkingConfig.EndpointsConfig[networkMode] = std::move(primaryConfig);
}
for (const auto& e : mappedPorts)
@@ -2670,20 +2738,20 @@ void WSLCContainerImpl::GetLabels(WSLCLabelInformation** Labels, ULONG* Count) c
void WSLCContainerImpl::ConnectToNetwork(const WSLCNetworkConnectionOptions* Options)
{
THROW_HR_IF(E_POINTER, Options == nullptr);
- THROW_HR_WITH_USER_ERROR_IF(E_NOTIMPL, Localization::MessageWslcContainerIpAddressNotSupported(), Options->ContainerIpAddress != nullptr);
THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !Options->NetworkName || strlen(Options->NetworkName) == 0);
+ auto endpointConfig = ResolveEndpointConfig(Options->Settings, Options->SettingsCount, Options->NetworkName);
+
auto lock = m_lock.lock_shared();
THROW_HR_WITH_USER_ERROR_IF(
- E_INVALIDARG,
- Localization::MessageWslcAdditionalNetworksRequirePrimary(),
- m_networkMode == "host" || m_networkMode == "none");
+ E_INVALIDARG, Localization::MessageWslcNetworkModeNoAdditionalNetworks(m_networkMode), !NetworkModeAllocatesVmPorts(m_networkMode));
common::docker_schema::ContainerNetworkRequest request{};
request.Container = m_id;
+ request.EndpointConfig = std::move(endpointConfig);
try
{
@@ -2710,9 +2778,7 @@ void WSLCContainerImpl::DisconnectFromNetwork(LPCSTR NetworkName)
auto lock = m_lock.lock_shared();
THROW_HR_WITH_USER_ERROR_IF(
- E_INVALIDARG,
- Localization::MessageWslcAdditionalNetworksRequirePrimary(),
- m_networkMode == "host" || m_networkMode == "none");
+ E_INVALIDARG, Localization::MessageWslcNetworkModeNoAdditionalNetworks(m_networkMode), !NetworkModeAllocatesVmPorts(m_networkMode));
common::docker_schema::ContainerNetworkRequest request{};
request.Container = m_id;
diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp
index edc13644b6..ff939ed9d1 100644
--- a/test/windows/WSLCTests.cpp
+++ b/test/windows/WSLCTests.cpp
@@ -7159,12 +7159,10 @@ class WSLCTests
ValidateCOMErrorMessageContains(L"Network name");
}
- WSLC_TEST_METHOD(ContainerNetworkEndpointSettingsNotImplementedTest)
+ WSLC_TEST_METHOD(ContainerNetworkEndpointSettingsUnknownKeyTest)
{
- // 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.
- const std::string networkName = "custom-net-settings";
+ // Unknown endpoint setting keys must be rejected at container creation time.
+ const std::string networkName = "custom-net-settings-unknown";
LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
WSLCNetworkOptions networkOptions{};
@@ -7174,7 +7172,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[] = {{"BogusKey", "value"}};
WSLCNetworkConnection connection{};
connection.NetworkName = networkName.c_str();
connection.Settings = settings;
@@ -7182,7 +7180,7 @@ class WSLCTests
WSLCContainerOptions options{};
options.Image = "debian:latest";
- options.Name = "test-endpoint-settings";
+ options.Name = "test-endpoint-settings-unknown";
options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
options.ContainerNetwork.NetworkMode = "bridge";
options.ContainerNetwork.Networks = &connection;
@@ -7190,8 +7188,10 @@ class WSLCTests
wil::com_ptr container;
auto hr = m_defaultSession->CreateContainer(&options, nullptr, &container);
- VERIFY_ARE_EQUAL(E_NOTIMPL, hr);
- ValidateCOMErrorMessage(L"Endpoint settings are not yet supported (network 'custom-net-settings').");
+ VERIFY_ARE_EQUAL(E_INVALIDARG, hr);
+ const auto expectedError =
+ std::format(L"Unknown endpoint setting 'BogusKey' for network '{}'.", std::wstring(networkName.begin(), networkName.end()));
+ ValidateCOMErrorMessage(expectedError);
}
WSLC_TEST_METHOD(ContainerUnsupportedColonNetworkModeRejectedTest)
@@ -7544,34 +7544,36 @@ class WSLCTests
expectBothReject(container, nonExistentNetwork.c_str(), WSLC_E_NETWORK_NOT_FOUND, expectedError.c_str());
}
- // Host and none mode rejection.
+ // Host, none, and container:* mode rejection.
{
const std::string networkName = "test-connect-mode-net";
createNetwork(networkName, "172.52.0.0/16");
auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
- auto expectModeRejection = [&](const std::string& name, std::string mode) {
- auto container = launchContainer(name, std::move(mode));
- expectBothReject(
- container,
- networkName.c_str(),
- E_INVALIDARG,
- L"Additional networks are not allowed when the primary network mode is 'host' or 'none'.");
+ auto expectModeRejection = [&](const std::string& name, const std::string& mode) {
+ auto container = launchContainer(name, mode);
+ const auto expected = std::format(
+ L"The primary network mode '{}' does not support connecting or disconnecting additional networks.",
+ std::wstring(mode.begin(), mode.end()));
+ expectBothReject(container, networkName.c_str(), E_INVALIDARG, expected.c_str());
};
expectModeRejection("test-connect-host-ctr", "host");
expectModeRejection("test-connect-none-ctr", "none");
- }
- // ContainerIpAddress not supported.
- {
- auto container = launchContainer("test-connect-ip-ctr");
+ // container: is resolved to container: internally, so the emitted mode string
+ // contains the target's container ID rather than its name. Verify HRESULT + a stable substring.
+ const std::string ctrModeTarget = "test-connect-ctrmode-target";
+ auto target = launchContainer(ctrModeTarget);
+ auto ctrModeContainer = launchContainer("test-connect-ctrmode-ctr", "container:" + ctrModeTarget);
+ const std::wstring expectedSubstring = L"does not support connecting or disconnecting additional networks";
WSLCNetworkConnectionOptions options{};
- options.NetworkName = "bridge";
- options.ContainerIpAddress = "10.0.0.5";
- VERIFY_ARE_EQUAL(E_NOTIMPL, container.Get().ConnectToNetwork(&options));
- ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported.");
+ options.NetworkName = networkName.c_str();
+ VERIFY_ARE_EQUAL(E_INVALIDARG, ctrModeContainer.Get().ConnectToNetwork(&options));
+ ValidateCOMErrorMessageContains(expectedSubstring);
+ VERIFY_ARE_EQUAL(E_INVALIDARG, ctrModeContainer.Get().DisconnectFromNetwork(networkName.c_str()));
+ ValidateCOMErrorMessageContains(expectedSubstring);
}
// Connect and disconnect from the container's primary network.
@@ -7617,6 +7619,221 @@ class WSLCTests
}
}
+ WSLC_TEST_METHOD(NetworkConnectEndpointSettingsTest)
+ {
+ const std::string networkName = "connect-endpoint-net";
+ const std::string subnet = "172.70.0.0/16";
+
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
+ WSLCNetworkOptions netOpts{};
+ netOpts.Name = networkName.c_str();
+ netOpts.Driver = "bridge";
+ netOpts.Subnet = subnet.c_str();
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts, nullptr));
+ auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
+
+ auto launchContainer = [&](const std::string& name) {
+ WSLCContainerLauncher launcher("debian:latest", name, {"sleep", "99999"}, {}, "bridge");
+ return launcher.Launch(*m_defaultSession);
+ };
+
+ auto expectConnectReject =
+ [&](auto& container, const std::vector& settings, HRESULT expectedResult, const std::wstring& expectedMessage) {
+ WSLCNetworkConnectionOptions options{};
+ options.NetworkName = networkName.c_str();
+ options.Settings = settings.data();
+ options.SettingsCount = static_cast(settings.size());
+ VERIFY_ARE_EQUAL(expectedResult, container.Get().ConnectToNetwork(&options));
+ ValidateCOMErrorMessage(expectedMessage);
+ };
+
+ // Unknown endpoint setting key rejected.
+ {
+ auto container = launchContainer("connect-endpoint-unknown");
+ const std::string unknownKey = "BogusKey";
+ const std::string unknownValue = "value";
+ const auto expected = std::format(
+ L"Unknown endpoint setting '{}' for network '{}'.",
+ std::wstring(unknownKey.begin(), unknownKey.end()),
+ std::wstring(networkName.begin(), networkName.end()));
+ expectConnectReject(container, {{unknownKey.c_str(), unknownValue.c_str()}}, E_INVALIDARG, expected);
+ }
+
+ // Malformed IPv4 address rejected.
+ {
+ auto container = launchContainer("connect-endpoint-badip");
+ const std::string badIp = "not-an-ip";
+ const auto expected = std::format(L"Invalid IP address '{}'", std::wstring(badIp.begin(), badIp.end()));
+ expectConnectReject(container, {{"IPAddress", badIp.c_str()}}, E_INVALIDARG, expected);
+ }
+
+ // Multiple IPAddress values rejected.
+ {
+ auto container = launchContainer("connect-endpoint-dupip");
+ const std::string firstIp = "172.70.0.5";
+ const std::string secondIp = "172.70.0.6";
+ const auto expected = std::format(
+ L"Only one IP address may be specified for network '{}'.", std::wstring(networkName.begin(), networkName.end()));
+ expectConnectReject(container, {{"IPAddress", firstIp.c_str()}, {"IPAddress", secondIp.c_str()}}, E_INVALIDARG, expected);
+ }
+
+ // Empty link rejected.
+ {
+ auto container = launchContainer("connect-endpoint-emptylink");
+ expectConnectReject(container, {{"Links", ""}}, E_INVALIDARG, L"Network link cannot be empty.");
+ }
+
+ // Malformed driver option (missing '=') rejected.
+ {
+ auto container = launchContainer("connect-endpoint-badopt");
+ const std::string badEntry = "no-equals";
+ const auto expected =
+ std::format(L"Invalid driver option '{}'; expected 'key=value'.", std::wstring(badEntry.begin(), badEntry.end()));
+ expectConnectReject(container, {{"DriverOpts", badEntry.c_str()}}, E_INVALIDARG, expected);
+ }
+
+ // Duplicate driver option key rejected.
+ {
+ auto container = launchContainer("connect-endpoint-dupopt");
+ const std::string dupKey = "mtu";
+ const std::string firstEntry = "mtu=1500";
+ const std::string secondEntry = "mtu=1400";
+ const auto expected = std::format(L"Duplicate driver option '{}'.", std::wstring(dupKey.begin(), dupKey.end()));
+ expectConnectReject(container, {{"DriverOpts", firstEntry.c_str()}, {"DriverOpts", secondEntry.c_str()}}, E_INVALIDARG, expected);
+ }
+
+ // Malformed link-local IPv4 address rejected.
+ {
+ auto container = launchContainer("connect-endpoint-badlli");
+ const std::string badIp = "not-a-link-local";
+ const auto expected = std::format(L"Invalid IP address '{}'", std::wstring(badIp.begin(), badIp.end()));
+ expectConnectReject(container, {{"LinkLocalIPs", badIp.c_str()}}, E_INVALIDARG, expected);
+ }
+
+ // Empty/whitespace-only alias rejected.
+ {
+ auto container = launchContainer("connect-endpoint-emptyalias");
+ expectConnectReject(container, {{"Aliases", " "}}, E_INVALIDARG, L"Network alias cannot be empty.");
+ }
+
+ // Driver option with empty key ('=value') rejected.
+ {
+ auto container = launchContainer("connect-endpoint-emptykey");
+ const std::string badEntry = "=value";
+ const auto expected =
+ std::format(L"Invalid driver option '{}'; expected 'key=value'.", std::wstring(badEntry.begin(), badEntry.end()));
+ expectConnectReject(container, {{"DriverOpts", badEntry.c_str()}}, E_INVALIDARG, expected);
+ }
+
+ // Driver option value containing '=' — everything after the first '=' is preserved verbatim.
+ {
+ auto container = launchContainer("connect-endpoint-eqvalue");
+ const std::string driverOptKey = "com.docker.network.endpoint.custom";
+ const std::string driverOptValue = "a=b=c";
+ const std::string driverOptEntry = driverOptKey + "=" + driverOptValue;
+
+ const std::vector settings{{"DriverOpts", driverOptEntry.c_str()}};
+ WSLCNetworkConnectionOptions options{};
+ options.NetworkName = networkName.c_str();
+ options.Settings = settings.data();
+ options.SettingsCount = static_cast(settings.size());
+ VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
+ const auto opt = endpoint.DriverOpts.find(driverOptKey);
+ VERIFY_IS_TRUE(opt != endpoint.DriverOpts.end());
+ VERIFY_ARE_EQUAL(driverOptValue, opt->second);
+ }
+
+ // Aliases + IPAddress + LinkLocalIPs + DriverOpts together — success, inspect verifies every field.
+ {
+ auto container = launchContainer("connect-endpoint-combo");
+ const std::string alias1 = "primary-alias";
+ const std::string alias2 = "secondary-alias";
+ const std::string ipAddress = "172.70.0.42";
+ const std::string linkLocal = "169.254.10.5";
+ const std::string driverOptEntry = "com.docker.network.endpoint.custom=verify";
+ const std::string driverOptKey = "com.docker.network.endpoint.custom";
+ const std::string driverOptValue = "verify";
+
+ const std::vector settings{
+ {"Aliases", alias1.c_str()},
+ {"Aliases", alias2.c_str()},
+ {"IPAddress", ipAddress.c_str()},
+ {"LinkLocalIPs", linkLocal.c_str()},
+ {"DriverOpts", driverOptEntry.c_str()},
+ };
+
+ WSLCNetworkConnectionOptions options{};
+ options.NetworkName = networkName.c_str();
+ options.Settings = settings.data();
+ options.SettingsCount = static_cast(settings.size());
+ VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias1) != endpoint.Aliases.end());
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias2) != endpoint.Aliases.end());
+ VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAddress);
+ VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
+ VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAMConfig->IPv4Address);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.IPAMConfig->LinkLocalIPs, linkLocal) != endpoint.IPAMConfig->LinkLocalIPs.end());
+ const auto opt = endpoint.DriverOpts.find(driverOptKey);
+ VERIFY_IS_TRUE(opt != endpoint.DriverOpts.end());
+ VERIFY_ARE_EQUAL(driverOptValue, opt->second);
+ }
+
+ // Links: connect two containers to the same user-defined bridge, verify Links echoes back in inspect.
+ {
+ const std::string targetName = "connect-endpoint-link-target";
+ const std::string targetAlias = "db";
+ auto target = launchContainer(targetName);
+ WSLCNetworkConnectionOptions targetOptions{};
+ targetOptions.NetworkName = networkName.c_str();
+ const std::vector targetSettings{{"Aliases", targetAlias.c_str()}};
+ targetOptions.Settings = targetSettings.data();
+ targetOptions.SettingsCount = static_cast(targetSettings.size());
+ VERIFY_SUCCEEDED(target.Get().ConnectToNetwork(&targetOptions));
+
+ auto source = launchContainer("connect-endpoint-link-source");
+ const std::string linkEntry = targetName + ":" + targetAlias;
+ const std::vector sourceSettings{{"Links", linkEntry.c_str()}};
+ WSLCNetworkConnectionOptions sourceOptions{};
+ sourceOptions.NetworkName = networkName.c_str();
+ sourceOptions.Settings = sourceSettings.data();
+ sourceOptions.SettingsCount = static_cast(sourceSettings.size());
+ VERIFY_SUCCEEDED(source.Get().ConnectToNetwork(&sourceOptions));
+
+ auto inspect = source.Inspect();
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Links, linkEntry) != endpoint.Links.end());
+ }
+
+ // Connecting a stopped container succeeds — Docker allows attach in created/exited state.
+ {
+ auto container = launchContainer("connect-endpoint-stopped");
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
+
+ const std::string alias = "stopped-alias";
+ const std::vector settings{{"Aliases", alias.c_str()}};
+
+ WSLCNetworkConnectionOptions options{};
+ options.NetworkName = networkName.c_str();
+ options.Settings = settings.data();
+ options.SettingsCount = static_cast(settings.size());
+ VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias) != endpoint.Aliases.end());
+ }
+ }
+
WSLC_TEST_METHOD(NetworkAliasCreateTest)
{
auto createNetwork = [&](const std::string& name, const char* subnet) {
@@ -7729,14 +7946,16 @@ class WSLCTests
expectError("alias-ctr-empty", networkName, {""}, E_INVALIDARG, L"Network alias cannot be empty.");
}
- // Unknown KVP key on primary settings — rejected with E_NOTIMPL.
+ // Unknown KVP key on primary settings — rejected with the unified endpoint-settings error.
{
const std::string networkName = "alias-net-unknown";
+ const std::string unknownKey = "BogusKey";
+ const std::string unknownValue = "value";
createNetwork(networkName, "172.63.0.0/16");
auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
LPCSTR args[] = {"sleep", "99999"};
- const KeyValuePair settings[] = {{"IPAddress", "10.0.0.5"}};
+ const KeyValuePair settings[] = {{unknownKey.c_str(), unknownValue.c_str()}};
WSLCContainerOptions options{};
options.Image = "debian:latest";
options.Name = "alias-ctr-unknown";
@@ -7746,11 +7965,92 @@ class WSLCTests
options.ContainerNetwork.SettingsCount = ARRAYSIZE(settings);
wil::com_ptr container;
- VERIFY_ARE_EQUAL(E_NOTIMPL, m_defaultSession->CreateContainer(&options, nullptr, &container));
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateContainer(&options, nullptr, &container));
ValidateCOMErrorMessage(std::format(
- L"Endpoint settings are not yet supported (network '{}').",
- std::wstring(networkName.begin(), networkName.end()))
- .c_str());
+ L"Unknown endpoint setting '{}' for network '{}'.",
+ std::wstring(unknownKey.begin(), unknownKey.end()),
+ std::wstring(networkName.begin(), networkName.end())));
+ }
+
+ // Primary endpoint settings (IPAddress + Aliases + LinkLocalIPs + DriverOpts) round-trip via inspect at create time.
+ {
+ const std::string networkName = "alias-net-combo";
+ createNetwork(networkName, "172.64.0.0/16");
+ auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
+
+ const std::string alias1 = "primary-alias";
+ const std::string alias2 = "secondary-alias";
+ const std::string ipAddress = "172.64.0.42";
+ const std::string linkLocal = "169.254.11.5";
+ const std::string driverOptEntry = "com.docker.network.endpoint.custom=verify";
+ const std::string driverOptKey = "com.docker.network.endpoint.custom";
+ const std::string driverOptValue = "verify";
+
+ LPCSTR args[] = {"sleep", "99999"};
+ const KeyValuePair settings[] = {
+ {"Aliases", alias1.c_str()},
+ {"Aliases", alias2.c_str()},
+ {"IPAddress", ipAddress.c_str()},
+ {"LinkLocalIPs", linkLocal.c_str()},
+ {"DriverOpts", driverOptEntry.c_str()},
+ };
+ WSLCContainerOptions options{};
+ options.Image = "debian:latest";
+ options.Name = "alias-ctr-combo";
+ options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
+ options.ContainerNetwork.NetworkMode = networkName.c_str();
+ options.ContainerNetwork.Settings = settings;
+ options.ContainerNetwork.SettingsCount = ARRAYSIZE(settings);
+
+ wil::com_ptr containerCom;
+ VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, nullptr, &containerCom));
+ RunningWSLCContainer container(std::move(containerCom), WSLCProcessFlagsNone);
+ VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, nullptr, nullptr));
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias1) != endpoint.Aliases.end());
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias2) != endpoint.Aliases.end());
+ VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAddress);
+ VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
+ VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAMConfig->IPv4Address);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.IPAMConfig->LinkLocalIPs, linkLocal) != endpoint.IPAMConfig->LinkLocalIPs.end());
+ const auto opt = endpoint.DriverOpts.find(driverOptKey);
+ VERIFY_IS_TRUE(opt != endpoint.DriverOpts.end());
+ VERIFY_ARE_EQUAL(driverOptValue, opt->second);
+ }
+
+ // Primary endpoint Links: launch a target container with an alias, then a source with --link at create time.
+ {
+ const std::string networkName = "alias-net-link";
+ const std::string targetName = "alias-ctr-link-target";
+ const std::string targetAlias = "db";
+ createNetwork(networkName, "172.65.0.0/16");
+ auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
+
+ auto target = launchWithAliases(targetName, networkName, {targetAlias});
+
+ const std::string linkEntry = targetName + ":" + targetAlias;
+ LPCSTR args[] = {"sleep", "99999"};
+ const KeyValuePair settings[] = {{"Links", linkEntry.c_str()}};
+ WSLCContainerOptions options{};
+ options.Image = "debian:latest";
+ options.Name = "alias-ctr-link-source";
+ options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
+ options.ContainerNetwork.NetworkMode = networkName.c_str();
+ options.ContainerNetwork.Settings = settings;
+ options.ContainerNetwork.SettingsCount = ARRAYSIZE(settings);
+
+ wil::com_ptr sourceCom;
+ VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, nullptr, &sourceCom));
+ RunningWSLCContainer source(std::move(sourceCom), WSLCProcessFlagsNone);
+ VERIFY_SUCCEEDED(source.Get().Start(WSLCContainerStartFlagsAttach, nullptr, nullptr));
+
+ auto inspect = source.Inspect();
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Links, linkEntry) != endpoint.Links.end());
}
}
diff --git a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
index 9a5ca6218c..2cc25cb0ef 100644
--- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
@@ -110,6 +110,12 @@ class WSLCCLIExecutionUnitTests
dataMap.Add(std::move(networks));
handled = true;
}
+ else if (dataType == Data::NetworkEndpointOptions)
+ {
+ wsl::windows::wslc::models::NetworkEndpointOptions endpointOptions;
+ dataMap.Add(std::move(endpointOptions));
+ handled = true;
+ }
if (!handled)
{
diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp
index e31a7258e7..7a9964e0ca 100644
--- a/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp
@@ -33,6 +33,7 @@ class WSLCE2ENetworkTests
TEST_CLASS_CLEANUP(ClassCleanup)
{
EnsureContainerDoesNotExist(WslcContainerName);
+ EnsureContainerDoesNotExist(WslcTargetContainerName);
EnsureNetworkDoesNotExist(TestNetworkName);
EnsureImageIsDeleted(DebianImage);
return true;
@@ -41,6 +42,7 @@ class WSLCE2ENetworkTests
TEST_METHOD_SETUP(TestMethodSetup)
{
EnsureContainerDoesNotExist(WslcContainerName);
+ EnsureContainerDoesNotExist(WslcTargetContainerName);
EnsureNetworkDoesNotExist(TestNetworkName);
return true;
}
@@ -156,7 +158,9 @@ class WSLCE2ENetworkTests
result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName));
VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
VERIFY_IS_TRUE(result.Stderr.has_value());
- VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*'host' or 'none'*Error code: *\r\n");
+ VerifyPatternMatch(
+ string::WideToMultiByte(result.Stderr.value()),
+ "*does not support connecting or disconnecting additional networks*Error code: *\r\n");
}
WSLC_TEST_METHOD(WSLCE2E_Network_Connect_AlreadyConnected_DockerErrorPropagated)
@@ -174,6 +178,110 @@ class WSLCE2ENetworkTests
VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*already exists*Error code: *\r\n");
}
+ WSLC_TEST_METHOD(WSLCE2E_Network_Connect_WithEndpointFlags_RoundTrips)
+ {
+ const std::wstring subnet = L"172.72.0.0/16";
+ const std::wstring ipAddress = L"172.72.0.42";
+ const std::wstring alias1 = L"primary-alias";
+ const std::wstring alias2 = L"secondary-alias";
+ const std::wstring linkLocal = L"169.254.11.5";
+
+ auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(
+ L"network connect --network-alias {} --network-alias {} --ip {} --link-local-ip {} {} {}", alias1, alias2, ipAddress, linkLocal, TestNetworkName, WslcContainerName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ const auto networkKey = string::WideToMultiByte(TestNetworkName);
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkKey));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkKey);
+ VERIFY_ARE_EQUAL(string::WideToMultiByte(ipAddress), endpoint.IPAddress);
+ VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
+ VERIFY_ARE_EQUAL(string::WideToMultiByte(ipAddress), endpoint.IPAMConfig->IPv4Address);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, string::WideToMultiByte(alias1)) != endpoint.Aliases.end());
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, string::WideToMultiByte(alias2)) != endpoint.Aliases.end());
+ VERIFY_IS_TRUE(
+ std::ranges::find(endpoint.IPAMConfig->LinkLocalIPs, string::WideToMultiByte(linkLocal)) !=
+ endpoint.IPAMConfig->LinkLocalIPs.end());
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Network_Connect_InvalidIp_Rejected)
+ {
+ const std::wstring badIp = L"not-an-ip";
+
+ auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"network connect --ip {} {} {}", badIp, TestNetworkName, WslcContainerName));
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
+ VERIFY_IS_TRUE(result.Stderr.has_value());
+ VerifyPatternMatch(
+ string::WideToMultiByte(result.Stderr.value()), std::format("*Invalid IP address '{}'*", string::WideToMultiByte(badIp)));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Network_Connect_DriverOpt_RoundTrips)
+ {
+ const std::wstring driverOptKey = L"com.docker.network.endpoint.custom";
+ const std::wstring driverOptValue = L"verify";
+
+ auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"network connect --driver-opt {}={} {} {}", driverOptKey, driverOptValue, TestNetworkName, WslcContainerName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ const auto networkKey = string::WideToMultiByte(TestNetworkName);
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkKey));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkKey);
+ const auto keyUtf8 = string::WideToMultiByte(driverOptKey);
+ const auto valueUtf8 = string::WideToMultiByte(driverOptValue);
+ const auto driverOptIt = endpoint.DriverOpts.find(keyUtf8);
+ VERIFY_IS_TRUE(driverOptIt != endpoint.DriverOpts.end());
+ VERIFY_ARE_EQUAL(valueUtf8, driverOptIt->second);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Network_Connect_Link_RoundTrips)
+ {
+ const std::wstring targetAlias = L"db";
+
+ auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(
+ L"container run -d --network {} --network-alias {} --name {} {} sleep infinity",
+ TestNetworkName,
+ targetAlias,
+ WslcTargetContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const std::wstring linkEntry = std::format(L"{}:{}", WslcTargetContainerName, targetAlias);
+ result = RunWslc(std::format(L"network connect --link {} {} {}", linkEntry, TestNetworkName, WslcContainerName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ const auto networkKey = string::WideToMultiByte(TestNetworkName);
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkKey));
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkKey);
+ const auto linkEntryUtf8 = string::WideToMultiByte(linkEntry);
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Links, linkEntryUtf8) != endpoint.Links.end());
+ }
+
WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_Valid)
{
auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
@@ -218,6 +326,7 @@ class WSLCE2ENetworkTests
private:
const std::wstring WslcContainerName = L"wslc-e2e-network-connect-container";
+ const std::wstring WslcTargetContainerName = L"wslc-e2e-network-connect-target";
const std::wstring TestNetworkName = L"wslc-e2e-network-connect";
const TestImage& DebianImage = DebianTestImage();
};