Add --ip, --link, --link-local-ip, --driver-opt, --network-alias flag… (#41070)

beena352 committed Jul 22, 2026 at 14:46 UTC aa14d77e9736c5fe803cd007b1b15987b828d6ed
16 files changed +792 -97
localization/strings/en-US/Resources.resw
+40 -9
@@ -2134,10 +2134,6 @@ Usage:
2134 <value>Container '{}' is running.</value>
2135 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2136 </data>
2137 - <data name="MessageWslcContainerIpAddressNotSupported" xml:space="preserve">
2138 - <value>ContainerIpAddress is not yet supported.</value>
2139 - <comment>{Locked="ContainerIpAddress"}Command line arguments, file names and string inserts should not be translated</comment>
2140 - </data>
2137 <data name="MessageWslcContainerModeNoPorts" xml:space="preserve">
2138 <value>Port mappings are not supported with container network mode; ports are owned by the target container.</value>
2139 </data>
@@ -2383,18 +2379,33 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2379 <value>Invalid stop timeout value: {}</value>
2380 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2381 </data>
2386 - <data name="MessageWslcAdditionalNetworksRequirePrimary" xml:space="preserve">
2387 - <value>Additional networks are not allowed when the primary network mode is 'host' or 'none'.</value>
2388 - <comment>{Locked="host"}{Locked="none"}Command line arguments, file names and string inserts should not be translated</comment>
2382 + <data name="MessageWslcNetworkModeNoAdditionalNetworks" xml:space="preserve">
2383 + <value>The primary network mode '{}' does not support connecting or disconnecting additional networks.</value>
2384 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2385 </data>
2386 <data name="MessageWslcDuplicateNetwork" xml:space="preserve">
2387 <value>Duplicate network: '{}'</value>
2388 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2389 </data>
2394 - <data name="MessageWslcEndpointSettingsNotSupported" xml:space="preserve">
2395 - <value>Endpoint settings are not yet supported (network '{}').</value>
2390 + <data name="MessageWslcDriverOptDuplicate" xml:space="preserve">
2391 + <value>Duplicate driver option '{}'.</value>
2392 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2393 + </data>
2394 + <data name="MessageWslcDriverOptInvalid" xml:space="preserve">
2395 + <value>Invalid driver option '{}'; expected 'key=value'.</value>
2396 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2397 + </data>
2398 + <data name="MessageWslcEndpointSettingUnknown" xml:space="preserve">
2399 + <value>Unknown endpoint setting '{}' for network '{}'.</value>
2400 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2401 + </data>
2402 + <data name="MessageWslcIpAddressSingleValue" xml:space="preserve">
2403 + <value>Only one IP address may be specified for network '{}'.</value>
2404 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2405 </data>
2406 + <data name="MessageWslcLinkEmpty" xml:space="preserve">
2407 + <value>Network link cannot be empty.</value>
2408 + </data>
2409 <data name="MessageWslcAliasEmpty" xml:space="preserve">
2410 <value>Network alias cannot be empty.</value>
2411 </data>
@@ -2402,6 +2413,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2413 <value>Network aliases require a user-defined network. Use --network to specify one.</value>
2414 <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2415 </data>
2416 + <data name="MessageWslcEndpointSettingsRequireNetwork" xml:space="preserve">
2417 + <value>Endpoint settings are not supported for network mode '{}'.</value>
2418 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2419 + </data>
2420 <data name="MessageWslcAliasAmbiguousWithMultipleNetworks" xml:space="preserve">
2421 <value>Network aliases cannot be specified when multiple networks are requested. Use a single --network argument.</value>
2422 <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
@@ -2992,6 +3007,22 @@ On first run, creates the file with all settings commented out at their defaults
3007 <data name="WSLCCLI_NetworkAliasArgDescription" xml:space="preserve">
3008 <value>Add a network-scoped alias for the container</value>
3009 </data>
3010 + <data name="WSLCCLI_DriverOptArgDescription" xml:space="preserve">
3011 + <value>Set endpoint driver options (--driver-opt key=value)</value>
3012 + <comment>{Locked="--driver-opt "}Command line arguments, file names and string inserts should not be translated</comment>
3013 + </data>
3014 + <data name="WSLCCLI_IpAddressArgDescription" xml:space="preserve">
3015 + <value>Assign the container an IPv4 address on the network</value>
3016 + <comment>{Locked="IPv4"}Command line arguments, file names and string inserts should not be translated</comment>
3017 + </data>
3018 + <data name="WSLCCLI_LinkArgDescription" xml:space="preserve">
3019 + <value>Add link to another container (--link name:alias)</value>
3020 + <comment>{Locked="--link "}Command line arguments, file names and string inserts should not be translated</comment>
3021 + </data>
3022 + <data name="WSLCCLI_LinkLocalIpArgDescription" xml:space="preserve">
3023 + <value>Add a link-local IPv4 address for the container</value>
3024 + <comment>{Locked="IPv4"}Command line arguments, file names and string inserts should not be translated</comment>
3025 + </data>
3026 <data name="WSLCCLI_NetworkAliasEmptyError" xml:space="preserve">
3027 <value>Invalid {} value: network alias cannot be empty or whitespace</value>
3028 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/inc/docker_schema.h
+78 -17
@@ -166,14 +166,78 @@ struct Network
166 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Options, Labels);
167 };
168
169 +struct EndpointIPAMConfig
170 +{
171 + std::string IPv4Address;
172 + std::optional<std::vector<std::string>> LinkLocalIPs;
173 +};
174 +
175 +inline void to_json(nlohmann::json& j, const EndpointIPAMConfig& v)
176 +{
177 + j = nlohmann::json::object();
178 + if (!v.IPv4Address.empty())
179 + {
180 + j["IPv4Address"] = v.IPv4Address;
181 + }
182 + if (v.LinkLocalIPs.has_value() && !v.LinkLocalIPs->empty())
183 + {
184 + j["LinkLocalIPs"] = *v.LinkLocalIPs;
185 + }
186 +}
187 +
188 +struct EndpointConfig
189 +{
190 + std::optional<std::vector<std::string>> Aliases;
191 + std::optional<EndpointIPAMConfig> IPAMConfig;
192 + std::optional<std::vector<std::string>> Links;
193 + std::optional<std::map<std::string, std::string>> DriverOpts;
194 +};
195 +
196 +inline void to_json(nlohmann::json& j, const EndpointConfig& v)
197 +{
198 + j = nlohmann::json::object();
199 + if (v.Aliases.has_value() && !v.Aliases->empty())
200 + {
201 + j["Aliases"] = *v.Aliases;
202 + }
203 + if (v.IPAMConfig.has_value())
204 + {
205 + auto ipam = nlohmann::json(*v.IPAMConfig);
206 + if (!ipam.empty())
207 + {
208 + j["IPAMConfig"] = std::move(ipam);
209 + }
210 + }
211 + if (v.Links.has_value() && !v.Links->empty())
212 + {
213 + j["Links"] = *v.Links;
214 + }
215 + if (v.DriverOpts.has_value() && !v.DriverOpts->empty())
216 + {
217 + j["DriverOpts"] = *v.DriverOpts;
218 + }
219 +}
220 +
221 struct ContainerNetworkRequest
222 {
223 using TResponse = void;
224 std::string Container;
173 -
174 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerNetworkRequest, Container);
225 + std::optional<EndpointConfig> EndpointConfig;
226 };
227
228 +inline void to_json(nlohmann::json& j, const ContainerNetworkRequest& v)
229 +{
230 + j = nlohmann::json{{"Container", v.Container}};
231 + if (v.EndpointConfig.has_value())
232 + {
233 + auto endpoint = nlohmann::json(*v.EndpointConfig);
234 + if (!endpoint.empty())
235 + {
236 + j["EndpointConfig"] = std::move(endpoint);
237 + }
238 + }
239 +}
240 +
241 struct Mount
242 {
243 std::string Name;
@@ -245,6 +309,14 @@ struct HostConfig
309 HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs, Devices, DeviceRequests, ShmSize, Memory, NanoCpus, Ulimits);
310 };
311
312 +struct InspectEndpointIPAMConfig
313 +{
314 + std::string IPv4Address;
315 + std::optional<std::vector<std::string>> LinkLocalIPs;
316 +
317 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointIPAMConfig, IPv4Address, LinkLocalIPs);
318 +};
319 +
320 struct EndpointSettings
321 {
322 std::string IPAddress;
@@ -252,24 +324,13 @@ struct EndpointSettings
324 std::string MacAddress;
325 int IPPrefixLen{};
326 std::optional<std::vector<std::string>> Aliases;
327 + std::optional<std::vector<std::string>> Links;
328 + std::optional<std::map<std::string, std::string>> DriverOpts;
329 + std::optional<InspectEndpointIPAMConfig> IPAMConfig;
330
256 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases);
331 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases, Links, DriverOpts, IPAMConfig);
332 };
333
259 -struct EndpointConfig
260 -{
261 - std::optional<std::vector<std::string>> Aliases;
262 -};
263 -
264 -inline void to_json(nlohmann::json& j, const EndpointConfig& v)
265 -{
266 - j = nlohmann::json::object();
267 - if (v.Aliases.has_value() && !v.Aliases->empty())
268 - {
269 - j["Aliases"] = *v.Aliases;
270 - }
271 -}
272 -
334 struct NetworkingConfig
335 {
336 std::map<std::string, EndpointConfig> EndpointsConfig;
src/windows/inc/wslc_schema.h
+12 -1
@@ -113,6 +113,14 @@ struct ContainerConfig
113 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout, Healthcheck);
114 };
115
116 +struct InspectEndpointIPAMConfig
117 +{
118 + std::string IPv4Address;
119 + std::vector<std::string> LinkLocalIPs;
120 +
121 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointIPAMConfig, IPv4Address, LinkLocalIPs);
122 +};
123 +
124 struct InspectEndpointSettings
125 {
126 std::string IPAddress;
@@ -120,8 +128,11 @@ struct InspectEndpointSettings
128 std::string MacAddress;
129 int IPPrefixLen{};
130 std::vector<std::string> Aliases;
131 + std::vector<std::string> Links;
132 + std::map<std::string, std::string> DriverOpts;
133 + std::optional<InspectEndpointIPAMConfig> IPAMConfig;
134
124 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases);
135 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectEndpointSettings, IPAddress, Gateway, MacAddress, IPPrefixLen, Aliases, Links, DriverOpts, IPAMConfig);
136 };
137
138 struct InspectNetworkSettings
src/windows/service/inc/wslc.idl
+4 -1
@@ -232,7 +232,10 @@ typedef struct _WSLCNetworkConnection
232 typedef struct _WSLCNetworkConnectionOptions
233 {
234 [unique] LPCSTR NetworkName;
235 - [unique] LPCSTR ContainerIpAddress; // Reserved for future --ip support; must be NULL today.
235 + // Endpoint settings for the network connection (Aliases, IPAddress, Links, LinkLocalIPs, DriverOpts).
236 + // KVP-encoded; duplicate keys are allowed (e.g., multiple "Aliases" entries).
237 + [unique, size_is(SettingsCount)] const KeyValuePair* Settings;
238 + ULONG SettingsCount;
239 } WSLCNetworkConnectionOptions;
240
241 typedef struct _WSLCContainerNetwork
src/windows/wslc/arguments/ArgumentDefinitions.h
+4
@@ -51,6 +51,7 @@ _(DNSOption, "dns-option", NO_ALIAS, Kind::Value, L
51 _(DNSSearch, "dns-search", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSSearchArgDescription()) \
52 _(Domainname, "domainname", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DomainnameArgDescription()) \
53 _(Driver, "driver", L"d", Kind::Value, Localization::WSLCCLI_DriverOptionDescription()) \
54 +_(DriverOpt, "driver-opt", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DriverOptArgDescription()) \
55 _(Entrypoint, "entrypoint", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EntrypointArgDescription()) \
56 _(Env, "env", L"e", Kind::Value, Localization::WSLCCLI_EnvArgDescription()) \
57 _(EnvFile, "env-file", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EnvFileArgDescription()) \
@@ -78,9 +79,12 @@ _(ImportFile, "file", NO_ALIAS, Kind::Positional, L
79 _(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \
80 _(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \
81 _(Internal, "internal", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NetworkInternalArgDescription()) \
82 +_(IpAddress, "ip", NO_ALIAS, Kind::Value, Localization::WSLCCLI_IpAddressArgDescription()) \
83 _(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
84 _(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
85 _(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
86 +_(Link, "link", NO_ALIAS, Kind::Value, Localization::WSLCCLI_LinkArgDescription()) \
87 +_(LinkLocalIp, "link-local-ip", NO_ALIAS, Kind::Value, Localization::WSLCCLI_LinkLocalIpArgDescription()) \
88 _(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \
89 _(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
90 _(Network, "network", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkArgDescription()) \
src/windows/wslc/commands/NetworkConnectCommand.cpp
+7 -1
@@ -29,6 +29,11 @@ std::vector<Argument> NetworkConnectCommand::GetArguments() const
29 return {
30 Argument::Create(ArgType::NetworkName, true),
31 Argument::Create(ArgType::ContainerId, true),
32 + Argument::Create(ArgType::DriverOpt, false, NO_LIMIT),
33 + Argument::Create(ArgType::IpAddress, false),
34 + Argument::Create(ArgType::Link, false, NO_LIMIT),
35 + Argument::Create(ArgType::LinkLocalIp, false, NO_LIMIT),
36 + Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
37 };
38 }
39
@@ -44,7 +49,8 @@ std::wstring NetworkConnectCommand::LongDescription() const
49
50 void NetworkConnectCommand::ExecuteInternal(CLIExecutionContext& context) const
51 {
47 - context << ResolveSession //
52 + context << ResolveSession //
53 + << SetNetworkEndpointOptionsFromArgs //
54 << ConnectNetwork;
55 }
56 } // namespace wsl::windows::wslc
src/windows/wslc/core/ExecutionContextData.h
+3
@@ -15,6 +15,7 @@ Abstract:
15 #include "EnumVariantMap.h"
16 #include "ContainerModel.h"
17 #include "ImageModel.h"
18 +#include "NetworkModel.h"
19 #include "SessionModel.h"
20 #include "wslc.h"
21
@@ -39,6 +40,7 @@ enum class Data : size_t
40 Images,
41 Volumes,
42 Networks,
43 + NetworkEndpointOptions,
44
45 Max
46 };
@@ -55,6 +57,7 @@ namespace details {
57 DEFINE_DATA_MAPPING(Images, std::vector<wsl::windows::wslc::models::ImageInformation>);
58 DEFINE_DATA_MAPPING(Volumes, std::vector<WSLCVolumeInformation>);
59 DEFINE_DATA_MAPPING(Networks, std::vector<WSLCNetworkInformation>);
60 + DEFINE_DATA_MAPPING(NetworkEndpointOptions, wsl::windows::wslc::models::NetworkEndpointOptions);
61 } // namespace details
62
63 struct DataMap : wsl::windows::wslc::EnumBasedVariantMap<Data, wsl::windows::wslc::execution::details::DataMapping>
src/windows/wslc/services/NetworkModel.h
+20
@@ -30,6 +30,26 @@ struct CreateNetworkOptions
30 std::optional<std::string> Gateway;
31 };
32
33 +struct NetworkEndpointOptions
34 +{
35 + std::vector<std::string> Aliases;
36 + std::optional<std::string> IpAddress;
37 + std::vector<std::string> Links;
38 + std::vector<std::string> LinkLocalIps;
39 + std::vector<std::string> DriverOpts;
40 +};
41 +
42 +struct ConnectNetworkOptions
43 +{
44 + std::string NetworkName;
45 + std::string ContainerId;
46 + std::vector<std::string> Aliases;
47 + std::optional<std::string> IpAddress;
48 + std::vector<std::string> Links;
49 + std::vector<std::string> LinkLocalIps;
50 + std::vector<std::string> DriverOpts;
51 +};
52 +
53 struct PruneNetworksResult
54 {
55 std::vector<std::string> PrunedNetworks;
src/windows/wslc/services/NetworkService.cpp
+36 -3
@@ -116,13 +116,46 @@ models::PruneNetworksResult NetworkService::Prune(models::Session& session, cons
116 return result;
117 }
118
119 -void NetworkService::Connect(models::Session& session, const std::string& networkName, const std::string& containerId)
119 +void NetworkService::Connect(models::Session& session, const models::ConnectNetworkOptions& connectOptions)
120 {
121 wil::com_ptr<IWSLCContainer> container;
122 - THROW_IF_FAILED(session.Get()->OpenContainer(containerId.c_str(), &container));
122 + THROW_IF_FAILED(session.Get()->OpenContainer(connectOptions.ContainerId.c_str(), &container));
123 +
124 + // Build KVPs so the pointers remain valid for the duration of the ConnectToNetwork call.
125 + std::vector<KeyValuePair> settings;
126 + settings.reserve(
127 + connectOptions.Aliases.size() + connectOptions.Links.size() + connectOptions.LinkLocalIps.size() +
128 + connectOptions.DriverOpts.size() + (connectOptions.IpAddress.has_value() ? 1 : 0));
129 +
130 + for (const auto& alias : connectOptions.Aliases)
131 + {
132 + settings.push_back({.Key = "Aliases", .Value = alias.c_str()});
133 + }
134 +
135 + if (connectOptions.IpAddress.has_value())
136 + {
137 + settings.push_back({.Key = "IPAddress", .Value = connectOptions.IpAddress->c_str()});
138 + }
139 +
140 + for (const auto& link : connectOptions.Links)
141 + {
142 + settings.push_back({.Key = "Links", .Value = link.c_str()});
143 + }
144 +
145 + for (const auto& linkLocalIp : connectOptions.LinkLocalIps)
146 + {
147 + settings.push_back({.Key = "LinkLocalIPs", .Value = linkLocalIp.c_str()});
148 + }
149 +
150 + for (const auto& entry : connectOptions.DriverOpts)
151 + {
152 + settings.push_back({.Key = "DriverOpts", .Value = entry.c_str()});
153 + }
154
155 WSLCNetworkConnectionOptions options{};
125 - options.NetworkName = networkName.c_str();
156 + options.NetworkName = connectOptions.NetworkName.c_str();
157 + options.Settings = settings.empty() ? nullptr : settings.data();
158 + options.SettingsCount = static_cast<ULONG>(settings.size());
159 THROW_IF_FAILED(container->ConnectToNetwork(&options));
160 }
161
src/windows/wslc/services/NetworkService.h
+1 -1
@@ -27,7 +27,7 @@ struct NetworkService
27 static std::vector<WSLCNetworkInformation> List(models::Session& session);
28 static wsl::windows::common::wslc_schema::Network Inspect(models::Session& session, const std::string& name);
29 static models::PruneNetworksResult Prune(models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
30 - static void Connect(models::Session& session, const std::string& networkName, const std::string& containerId);
30 + static void Connect(models::Session& session, const models::ConnectNetworkOptions& connectOptions);
31 static void Disconnect(models::Session& session, const std::string& networkName, const std::string& containerId);
32 };
33 } // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/NetworkTasks.cpp
+44 -3
@@ -231,12 +231,21 @@ void PruneNetworks(CLIExecutionContext& context)
231 void ConnectNetwork(CLIExecutionContext& context)
232 {
233 WI_ASSERT(context.Data.Contains(Data::Session));
234 + WI_ASSERT(context.Data.Contains(Data::NetworkEndpointOptions));
235 WI_ASSERT(context.Args.Contains(ArgType::NetworkName));
236 WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
237
237 - const auto networkName = WideToMultiByte(context.Args.Get<ArgType::NetworkName>());
238 - const auto containerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
239 - NetworkService::Connect(context.Data.Get<Data::Session>(), networkName, containerId);
238 + const auto& endpoint = context.Data.Get<Data::NetworkEndpointOptions>();
239 + models::ConnectNetworkOptions options{};
240 + options.NetworkName = WideToMultiByte(context.Args.Get<ArgType::NetworkName>());
241 + options.ContainerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
242 + options.Aliases = endpoint.Aliases;
243 + options.IpAddress = endpoint.IpAddress;
244 + options.Links = endpoint.Links;
245 + options.LinkLocalIps = endpoint.LinkLocalIps;
246 + options.DriverOpts = endpoint.DriverOpts;
247 +
248 + NetworkService::Connect(context.Data.Get<Data::Session>(), options);
249 }
250
251 void DisconnectNetwork(CLIExecutionContext& context)
@@ -249,4 +258,36 @@ void DisconnectNetwork(CLIExecutionContext& context)
258 const auto containerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
259 NetworkService::Disconnect(context.Data.Get<Data::Session>(), networkName, containerId);
260 }
261 +
262 +void SetNetworkEndpointOptionsFromArgs(CLIExecutionContext& context)
263 +{
264 + models::NetworkEndpointOptions options{};
265 +
266 + for (const auto& alias : context.Args.GetAll<ArgType::NetworkAlias>())
267 + {
268 + options.Aliases.emplace_back(WideToMultiByte(alias));
269 + }
270 +
271 + if (context.Args.Contains(ArgType::IpAddress))
272 + {
273 + options.IpAddress = WideToMultiByte(context.Args.Get<ArgType::IpAddress>());
274 + }
275 +
276 + for (const auto& link : context.Args.GetAll<ArgType::Link>())
277 + {
278 + options.Links.emplace_back(WideToMultiByte(link));
279 + }
280 +
281 + for (const auto& linkLocalIp : context.Args.GetAll<ArgType::LinkLocalIp>())
282 + {
283 + options.LinkLocalIps.emplace_back(WideToMultiByte(linkLocalIp));
284 + }
285 +
286 + for (const auto& driverOpt : context.Args.GetAll<ArgType::DriverOpt>())
287 + {
288 + options.DriverOpts.emplace_back(WideToMultiByte(driverOpt));
289 + }
290 +
291 + context.Data.Add<Data::NetworkEndpointOptions>(std::move(options));
292 +}
293 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/NetworkTasks.h
+1
@@ -24,4 +24,5 @@ void ListNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
24 void PruneNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
25 void ConnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context);
26 void DisconnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context);
27 +void SetNetworkEndpointOptionsFromArgs(wsl::windows::wslc::execution::CLIExecutionContext& context);
28 } // namespace wsl::windows::wslc::task
src/windows/wslcsession/WSLCContainer.cpp
+95 -29
@@ -231,9 +231,8 @@ std::string ResolveNetworkMode(LPCSTR networkMode, bool hasRequestedPorts, const
231 return std::string{mode};
232 }
233
234 -// Parses the primary endpoint's Settings KVP. Today only "Aliases" is recognised; unknown
235 -// keys are rejected to avoid silently dropping caller data (mirrors the per-connection guard).
236 -EndpointConfig ResolvePrimaryEndpointConfig(const KeyValuePair* settings, ULONG count, std::string_view networkMode)
234 +// Unknown Settings keys are rejected rather than silently dropped, so callers get a clear error.
235 +EndpointConfig ResolveEndpointConfig(const KeyValuePair* settings, ULONG count, std::string_view networkName)
236 {
237 EndpointConfig config{};
238 if (count == 0)
@@ -245,24 +244,83 @@ EndpointConfig ResolvePrimaryEndpointConfig(const KeyValuePair* settings, ULONG
244
245 auto parsed = ParseKeyMultiValuePairs(settings, count);
246
247 + static constexpr std::array knownKeys{"Aliases", "IPAddress", "Links", "LinkLocalIPs", "DriverOpts"};
248 for (const auto& [key, _] : parsed)
249 {
250 THROW_HR_WITH_USER_ERROR_IF(
251 - E_NOTIMPL, Localization::MessageWslcEndpointSettingsNotSupported(std::string{networkMode}), key != "Aliases");
251 + E_INVALIDARG,
252 + Localization::MessageWslcEndpointSettingUnknown(key, std::string{networkName}),
253 + std::find(knownKeys.begin(), knownKeys.end(), key) == knownKeys.end());
254 }
255
256 + auto isBlank = [](const std::string& value) {
257 + return value.empty() || std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); });
258 + };
259 +
260 if (auto it = parsed.find("Aliases"); it != parsed.end())
261 {
262 for (const auto& alias : it->second)
263 {
258 - THROW_HR_WITH_USER_ERROR_IF(
259 - E_INVALIDARG,
260 - Localization::MessageWslcAliasEmpty(),
261 - alias.empty() || std::all_of(alias.begin(), alias.end(), [](unsigned char ch) { return std::isspace(ch); }));
264 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasEmpty(), isBlank(alias));
265 }
266 config.Aliases = std::move(it->second);
267 }
268
269 + if (auto it = parsed.find("IPAddress"); it != parsed.end())
270 + {
271 + THROW_HR_WITH_USER_ERROR_IF(
272 + E_INVALIDARG, Localization::MessageWslcIpAddressSingleValue(std::string{networkName}), it->second.size() != 1);
273 +
274 + const auto& address = it->second.front();
275 + in_addr parsedAddress{};
276 + ParseIpv4Address(address.c_str(), parsedAddress);
277 +
278 + EndpointIPAMConfig ipam{};
279 + ipam.IPv4Address = address;
280 + config.IPAMConfig = std::move(ipam);
281 + }
282 +
283 + if (auto it = parsed.find("Links"); it != parsed.end())
284 + {
285 + for (const auto& link : it->second)
286 + {
287 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcLinkEmpty(), isBlank(link));
288 + }
289 + config.Links = std::move(it->second);
290 + }
291 +
292 + if (auto it = parsed.find("LinkLocalIPs"); it != parsed.end())
293 + {
294 + for (const auto& address : it->second)
295 + {
296 + in_addr parsedAddress{};
297 + ParseIpv4Address(address.c_str(), parsedAddress);
298 + }
299 + if (!config.IPAMConfig.has_value())
300 + {
301 + config.IPAMConfig = EndpointIPAMConfig{};
302 + }
303 + config.IPAMConfig->LinkLocalIPs = std::move(it->second);
304 + }
305 +
306 + if (auto it = parsed.find("DriverOpts"); it != parsed.end())
307 + {
308 + std::map<std::string, std::string> driverOpts;
309 + for (const auto& entry : it->second)
310 + {
311 + const auto separator = entry.find('=');
312 + THROW_HR_WITH_USER_ERROR_IF(
313 + E_INVALIDARG, Localization::MessageWslcDriverOptInvalid(entry), separator == std::string::npos || separator == 0);
314 +
315 + auto key = entry.substr(0, separator);
316 + auto value = entry.substr(separator + 1);
317 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDriverOptInvalid(entry), isBlank(key));
318 + THROW_HR_WITH_USER_ERROR_IF(
319 + E_INVALIDARG, Localization::MessageWslcDriverOptDuplicate(key), !driverOpts.try_emplace(key, std::move(value)).second);
320 + }
321 + config.DriverOpts = std::move(driverOpts);
322 + }
323 +
324 return config;
325 }
326
@@ -294,9 +352,7 @@ std::map<std::string, EndpointConfig> ResolveEndpoints(
352 WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), !sessionNetworks.contains(name));
353 }
354
297 - // Per-endpoint Settings are reserved for future use (IPAddress, Aliases, ...). No keys
298 - // are interpreted today, so reject any non-empty payload rather than silently dropping it.
299 - THROW_HR_WITH_USER_ERROR_IF(E_NOTIMPL, Localization::MessageWslcEndpointSettingsNotSupported(name), connections[i].SettingsCount > 0);
355 + it->second = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name);
356 }
357 return resolved;
358 }
@@ -1523,6 +1579,15 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1579 wslcEndpoint.MacAddress = endpoint.MacAddress;
1580 wslcEndpoint.IPPrefixLen = endpoint.IPPrefixLen;
1581 wslcEndpoint.Aliases = endpoint.Aliases.value_or(std::vector<std::string>{});
1582 + wslcEndpoint.Links = endpoint.Links.value_or(std::vector<std::string>{});
1583 + wslcEndpoint.DriverOpts = endpoint.DriverOpts.value_or(std::map<std::string, std::string>{});
1584 + if (endpoint.IPAMConfig.has_value())
1585 + {
1586 + wslc_schema::InspectEndpointIPAMConfig ipam{};
1587 + ipam.IPv4Address = endpoint.IPAMConfig->IPv4Address;
1588 + ipam.LinkLocalIPs = endpoint.IPAMConfig->LinkLocalIPs.value_or(std::vector<std::string>{});
1589 + wslcEndpoint.IPAMConfig = std::move(ipam);
1590 + }
1591 wslcInspect.NetworkSettings.Networks[name] = std::move(wslcEndpoint);
1592 }
1593
@@ -1846,29 +1911,32 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1911 containerOptions.ContainerNetwork.Networks, containerOptions.ContainerNetwork.NetworksCount, networkMode, sessionNetworks);
1912
1913 auto primaryConfig =
1849 - ResolvePrimaryEndpointConfig(containerOptions.ContainerNetwork.Settings, containerOptions.ContainerNetwork.SettingsCount, networkMode);
1914 + ResolveEndpointConfig(containerOptions.ContainerNetwork.Settings, containerOptions.ContainerNetwork.SettingsCount, networkMode);
1915
1851 - // Aliases require a user-defined endpoint. bridge/host/none/container: modes don't support them.
1916 THROW_HR_WITH_USER_ERROR_IF(
1917 E_INVALIDARG,
1918 Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
1919 primaryConfig.Aliases.has_value() && (networkMode == "bridge" || !NetworkModeAllocatesVmPorts(networkMode)));
1920
1921 + const bool hasNonAliasEndpointSettings =
1922 + primaryConfig.IPAMConfig.has_value() || primaryConfig.Links.has_value() || primaryConfig.DriverOpts.has_value();
1923 + // N.B. NetworkModeAllocatesVmPorts is reused here as the "supports endpoint settings" predicate: modes
1924 + // that lack a dedicated netns (host/none/container:*) also can't accept per-endpoint settings.
1925 + THROW_HR_WITH_USER_ERROR_IF(
1926 + E_INVALIDARG,
1927 + Localization::MessageWslcEndpointSettingsRequireNetwork(networkMode),
1928 + hasNonAliasEndpointSettings && !NetworkModeAllocatesVmPorts(networkMode));
1929 +
1930 auto mappedPorts = BuildPortMappings(ports, networkMode, virtualMachine);
1931
1932 request.HostConfig.NetworkMode = networkMode;
1933 request.NetworkingConfig.EndpointsConfig = std::move(endpoints);
1934
1862 - // Docker API v1.44 (Docker 25.x) requires the primary network to be present in EndpointsConfig
1863 - // when EndpointsConfig is non-empty. Insert it so Docker attaches all networks at create time.
1864 - // Also insert when the caller supplied aliases for the primary endpoint.
1865 - if (NetworkModeAllocatesVmPorts(networkMode) && (!request.NetworkingConfig.EndpointsConfig.empty() || primaryConfig.Aliases.has_value()))
1935 + const bool hasPrimaryEndpointSettings = primaryConfig.Aliases.has_value() || primaryConfig.IPAMConfig.has_value() ||
1936 + primaryConfig.Links.has_value() || primaryConfig.DriverOpts.has_value();
1937 + if (NetworkModeAllocatesVmPorts(networkMode) && (!request.NetworkingConfig.EndpointsConfig.empty() || hasPrimaryEndpointSettings))
1938 {
1867 - auto [it, _] = request.NetworkingConfig.EndpointsConfig.try_emplace(networkMode);
1868 - if (primaryConfig.Aliases.has_value())
1869 - {
1870 - it->second.Aliases = std::move(primaryConfig.Aliases);
1871 - }
1939 + request.NetworkingConfig.EndpointsConfig[networkMode] = std::move(primaryConfig);
1940 }
1941
1942 for (const auto& e : mappedPorts)
@@ -2670,20 +2738,20 @@ void WSLCContainerImpl::GetLabels(WSLCLabelInformation** Labels, ULONG* Count) c
2738 void WSLCContainerImpl::ConnectToNetwork(const WSLCNetworkConnectionOptions* Options)
2739 {
2740 THROW_HR_IF(E_POINTER, Options == nullptr);
2673 - THROW_HR_WITH_USER_ERROR_IF(E_NOTIMPL, Localization::MessageWslcContainerIpAddressNotSupported(), Options->ContainerIpAddress != nullptr);
2741
2742 THROW_HR_WITH_USER_ERROR_IF(
2743 E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !Options->NetworkName || strlen(Options->NetworkName) == 0);
2744
2745 + auto endpointConfig = ResolveEndpointConfig(Options->Settings, Options->SettingsCount, Options->NetworkName);
2746 +
2747 auto lock = m_lock.lock_shared();
2748
2749 THROW_HR_WITH_USER_ERROR_IF(
2681 - E_INVALIDARG,
2682 - Localization::MessageWslcAdditionalNetworksRequirePrimary(),
2683 - m_networkMode == "host" || m_networkMode == "none");
2750 + E_INVALIDARG, Localization::MessageWslcNetworkModeNoAdditionalNetworks(m_networkMode), !NetworkModeAllocatesVmPorts(m_networkMode));
2751
2752 common::docker_schema::ContainerNetworkRequest request{};
2753 request.Container = m_id;
2754 + request.EndpointConfig = std::move(endpointConfig);
2755
2756 try
2757 {
@@ -2710,9 +2778,7 @@ void WSLCContainerImpl::DisconnectFromNetwork(LPCSTR NetworkName)
2778 auto lock = m_lock.lock_shared();
2779
2780 THROW_HR_WITH_USER_ERROR_IF(
2713 - E_INVALIDARG,
2714 - Localization::MessageWslcAdditionalNetworksRequirePrimary(),
2715 - m_networkMode == "host" || m_networkMode == "none");
2781 + E_INVALIDARG, Localization::MessageWslcNetworkModeNoAdditionalNetworks(m_networkMode), !NetworkModeAllocatesVmPorts(m_networkMode));
2782
2783 common::docker_schema::ContainerNetworkRequest request{};
2784 request.Container = m_id;
test/windows/WSLCTests.cpp
+331 -31
@@ -7159,12 +7159,10 @@ class WSLCTests
7159 ValidateCOMErrorMessageContains(L"Network name");
7160 }
7161
7162 - WSLC_TEST_METHOD(ContainerNetworkEndpointSettingsNotImplementedTest)
7162 + WSLC_TEST_METHOD(ContainerNetworkEndpointSettingsUnknownKeyTest)
7163 {
7164 - // Per-endpoint Settings (Aliases, IPAMConfig, etc.) are reserved for a future PR.
7165 - // Until that lands, any non-empty Settings payload must be rejected with E_NOTIMPL
7166 - // so callers don't silently lose data.
7167 - const std::string networkName = "custom-net-settings";
7164 + // Unknown endpoint setting keys must be rejected at container creation time.
7165 + const std::string networkName = "custom-net-settings-unknown";
7166 LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
7167
7168 WSLCNetworkOptions networkOptions{};
@@ -7174,7 +7172,7 @@ class WSLCTests
7172 auto networkCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
7173
7174 LPCSTR args[] = {"sleep", "99999"};
7177 - KeyValuePair settings[] = {{"Aliases", "my-alias"}};
7175 + KeyValuePair settings[] = {{"BogusKey", "value"}};
7176 WSLCNetworkConnection connection{};
7177 connection.NetworkName = networkName.c_str();
7178 connection.Settings = settings;
@@ -7182,7 +7180,7 @@ class WSLCTests
7180
7181 WSLCContainerOptions options{};
7182 options.Image = "debian:latest";
7185 - options.Name = "test-endpoint-settings";
7183 + options.Name = "test-endpoint-settings-unknown";
7184 options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
7185 options.ContainerNetwork.NetworkMode = "bridge";
7186 options.ContainerNetwork.Networks = &connection;
@@ -7190,8 +7188,10 @@ class WSLCTests
7188
7189 wil::com_ptr<IWSLCContainer> container;
7190 auto hr = m_defaultSession->CreateContainer(&options, nullptr, &container);
7193 - VERIFY_ARE_EQUAL(E_NOTIMPL, hr);
7194 - ValidateCOMErrorMessage(L"Endpoint settings are not yet supported (network 'custom-net-settings').");
7191 + VERIFY_ARE_EQUAL(E_INVALIDARG, hr);
7192 + const auto expectedError =
7193 + std::format(L"Unknown endpoint setting 'BogusKey' for network '{}'.", std::wstring(networkName.begin(), networkName.end()));
7194 + ValidateCOMErrorMessage(expectedError);
7195 }
7196
7197 WSLC_TEST_METHOD(ContainerUnsupportedColonNetworkModeRejectedTest)
@@ -7544,34 +7544,36 @@ class WSLCTests
7544 expectBothReject(container, nonExistentNetwork.c_str(), WSLC_E_NETWORK_NOT_FOUND, expectedError.c_str());
7545 }
7546
7547 - // Host and none mode rejection.
7547 + // Host, none, and container:* mode rejection.
7548 {
7549 const std::string networkName = "test-connect-mode-net";
7550 createNetwork(networkName, "172.52.0.0/16");
7551 auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
7552
7553 - auto expectModeRejection = [&](const std::string& name, std::string mode) {
7554 - auto container = launchContainer(name, std::move(mode));
7555 - expectBothReject(
7556 - container,
7557 - networkName.c_str(),
7558 - E_INVALIDARG,
7559 - L"Additional networks are not allowed when the primary network mode is 'host' or 'none'.");
7553 + auto expectModeRejection = [&](const std::string& name, const std::string& mode) {
7554 + auto container = launchContainer(name, mode);
7555 + const auto expected = std::format(
7556 + L"The primary network mode '{}' does not support connecting or disconnecting additional networks.",
7557 + std::wstring(mode.begin(), mode.end()));
7558 + expectBothReject(container, networkName.c_str(), E_INVALIDARG, expected.c_str());
7559 };
7560
7561 expectModeRejection("test-connect-host-ctr", "host");
7562 expectModeRejection("test-connect-none-ctr", "none");
7564 - }
7563
7566 - // ContainerIpAddress not supported.
7567 - {
7568 - auto container = launchContainer("test-connect-ip-ctr");
7564 + // container:<name> is resolved to container:<id> internally, so the emitted mode string
7565 + // contains the target's container ID rather than its name. Verify HRESULT + a stable substring.
7566 + const std::string ctrModeTarget = "test-connect-ctrmode-target";
7567 + auto target = launchContainer(ctrModeTarget);
7568 + auto ctrModeContainer = launchContainer("test-connect-ctrmode-ctr", "container:" + ctrModeTarget);
7569 + const std::wstring expectedSubstring = L"does not support connecting or disconnecting additional networks";
7570
7571 WSLCNetworkConnectionOptions options{};
7571 - options.NetworkName = "bridge";
7572 - options.ContainerIpAddress = "10.0.0.5";
7573 - VERIFY_ARE_EQUAL(E_NOTIMPL, container.Get().ConnectToNetwork(&options));
7574 - ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported.");
7572 + options.NetworkName = networkName.c_str();
7573 + VERIFY_ARE_EQUAL(E_INVALIDARG, ctrModeContainer.Get().ConnectToNetwork(&options));
7574 + ValidateCOMErrorMessageContains(expectedSubstring);
7575 + VERIFY_ARE_EQUAL(E_INVALIDARG, ctrModeContainer.Get().DisconnectFromNetwork(networkName.c_str()));
7576 + ValidateCOMErrorMessageContains(expectedSubstring);
7577 }
7578
7579 // Connect and disconnect from the container's primary network.
@@ -7617,6 +7619,221 @@ class WSLCTests
7619 }
7620 }
7621
7622 + WSLC_TEST_METHOD(NetworkConnectEndpointSettingsTest)
7623 + {
7624 + const std::string networkName = "connect-endpoint-net";
7625 + const std::string subnet = "172.70.0.0/16";
7626 +
7627 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
7628 + WSLCNetworkOptions netOpts{};
7629 + netOpts.Name = networkName.c_str();
7630 + netOpts.Driver = "bridge";
7631 + netOpts.Subnet = subnet.c_str();
7632 + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts, nullptr));
7633 + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
7634 +
7635 + auto launchContainer = [&](const std::string& name) {
7636 + WSLCContainerLauncher launcher("debian:latest", name, {"sleep", "99999"}, {}, "bridge");
7637 + return launcher.Launch(*m_defaultSession);
7638 + };
7639 +
7640 + auto expectConnectReject =
7641 + [&](auto& container, const std::vector<KeyValuePair>& settings, HRESULT expectedResult, const std::wstring& expectedMessage) {
7642 + WSLCNetworkConnectionOptions options{};
7643 + options.NetworkName = networkName.c_str();
7644 + options.Settings = settings.data();
7645 + options.SettingsCount = static_cast<ULONG>(settings.size());
7646 + VERIFY_ARE_EQUAL(expectedResult, container.Get().ConnectToNetwork(&options));
7647 + ValidateCOMErrorMessage(expectedMessage);
7648 + };
7649 +
7650 + // Unknown endpoint setting key rejected.
7651 + {
7652 + auto container = launchContainer("connect-endpoint-unknown");
7653 + const std::string unknownKey = "BogusKey";
7654 + const std::string unknownValue = "value";
7655 + const auto expected = std::format(
7656 + L"Unknown endpoint setting '{}' for network '{}'.",
7657 + std::wstring(unknownKey.begin(), unknownKey.end()),
7658 + std::wstring(networkName.begin(), networkName.end()));
7659 + expectConnectReject(container, {{unknownKey.c_str(), unknownValue.c_str()}}, E_INVALIDARG, expected);
7660 + }
7661 +
7662 + // Malformed IPv4 address rejected.
7663 + {
7664 + auto container = launchContainer("connect-endpoint-badip");
7665 + const std::string badIp = "not-an-ip";
7666 + const auto expected = std::format(L"Invalid IP address '{}'", std::wstring(badIp.begin(), badIp.end()));
7667 + expectConnectReject(container, {{"IPAddress", badIp.c_str()}}, E_INVALIDARG, expected);
7668 + }
7669 +
7670 + // Multiple IPAddress values rejected.
7671 + {
7672 + auto container = launchContainer("connect-endpoint-dupip");
7673 + const std::string firstIp = "172.70.0.5";
7674 + const std::string secondIp = "172.70.0.6";
7675 + const auto expected = std::format(
7676 + L"Only one IP address may be specified for network '{}'.", std::wstring(networkName.begin(), networkName.end()));
7677 + expectConnectReject(container, {{"IPAddress", firstIp.c_str()}, {"IPAddress", secondIp.c_str()}}, E_INVALIDARG, expected);
7678 + }
7679 +
7680 + // Empty link rejected.
7681 + {
7682 + auto container = launchContainer("connect-endpoint-emptylink");
7683 + expectConnectReject(container, {{"Links", ""}}, E_INVALIDARG, L"Network link cannot be empty.");
7684 + }
7685 +
7686 + // Malformed driver option (missing '=') rejected.
7687 + {
7688 + auto container = launchContainer("connect-endpoint-badopt");
7689 + const std::string badEntry = "no-equals";
7690 + const auto expected =
7691 + std::format(L"Invalid driver option '{}'; expected 'key=value'.", std::wstring(badEntry.begin(), badEntry.end()));
7692 + expectConnectReject(container, {{"DriverOpts", badEntry.c_str()}}, E_INVALIDARG, expected);
7693 + }
7694 +
7695 + // Duplicate driver option key rejected.
7696 + {
7697 + auto container = launchContainer("connect-endpoint-dupopt");
7698 + const std::string dupKey = "mtu";
7699 + const std::string firstEntry = "mtu=1500";
7700 + const std::string secondEntry = "mtu=1400";
7701 + const auto expected = std::format(L"Duplicate driver option '{}'.", std::wstring(dupKey.begin(), dupKey.end()));
7702 + expectConnectReject(container, {{"DriverOpts", firstEntry.c_str()}, {"DriverOpts", secondEntry.c_str()}}, E_INVALIDARG, expected);
7703 + }
7704 +
7705 + // Malformed link-local IPv4 address rejected.
7706 + {
7707 + auto container = launchContainer("connect-endpoint-badlli");
7708 + const std::string badIp = "not-a-link-local";
7709 + const auto expected = std::format(L"Invalid IP address '{}'", std::wstring(badIp.begin(), badIp.end()));
7710 + expectConnectReject(container, {{"LinkLocalIPs", badIp.c_str()}}, E_INVALIDARG, expected);
7711 + }
7712 +
7713 + // Empty/whitespace-only alias rejected.
7714 + {
7715 + auto container = launchContainer("connect-endpoint-emptyalias");
7716 + expectConnectReject(container, {{"Aliases", " "}}, E_INVALIDARG, L"Network alias cannot be empty.");
7717 + }
7718 +
7719 + // Driver option with empty key ('=value') rejected.
7720 + {
7721 + auto container = launchContainer("connect-endpoint-emptykey");
7722 + const std::string badEntry = "=value";
7723 + const auto expected =
7724 + std::format(L"Invalid driver option '{}'; expected 'key=value'.", std::wstring(badEntry.begin(), badEntry.end()));
7725 + expectConnectReject(container, {{"DriverOpts", badEntry.c_str()}}, E_INVALIDARG, expected);
7726 + }
7727 +
7728 + // Driver option value containing '=' — everything after the first '=' is preserved verbatim.
7729 + {
7730 + auto container = launchContainer("connect-endpoint-eqvalue");
7731 + const std::string driverOptKey = "com.docker.network.endpoint.custom";
7732 + const std::string driverOptValue = "a=b=c";
7733 + const std::string driverOptEntry = driverOptKey + "=" + driverOptValue;
7734 +
7735 + const std::vector<KeyValuePair> settings{{"DriverOpts", driverOptEntry.c_str()}};
7736 + WSLCNetworkConnectionOptions options{};
7737 + options.NetworkName = networkName.c_str();
7738 + options.Settings = settings.data();
7739 + options.SettingsCount = static_cast<ULONG>(settings.size());
7740 + VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
7741 +
7742 + auto inspect = container.Inspect();
7743 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
7744 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
7745 + const auto opt = endpoint.DriverOpts.find(driverOptKey);
7746 + VERIFY_IS_TRUE(opt != endpoint.DriverOpts.end());
7747 + VERIFY_ARE_EQUAL(driverOptValue, opt->second);
7748 + }
7749 +
7750 + // Aliases + IPAddress + LinkLocalIPs + DriverOpts together — success, inspect verifies every field.
7751 + {
7752 + auto container = launchContainer("connect-endpoint-combo");
7753 + const std::string alias1 = "primary-alias";
7754 + const std::string alias2 = "secondary-alias";
7755 + const std::string ipAddress = "172.70.0.42";
7756 + const std::string linkLocal = "169.254.10.5";
7757 + const std::string driverOptEntry = "com.docker.network.endpoint.custom=verify";
7758 + const std::string driverOptKey = "com.docker.network.endpoint.custom";
7759 + const std::string driverOptValue = "verify";
7760 +
7761 + const std::vector<KeyValuePair> settings{
7762 + {"Aliases", alias1.c_str()},
7763 + {"Aliases", alias2.c_str()},
7764 + {"IPAddress", ipAddress.c_str()},
7765 + {"LinkLocalIPs", linkLocal.c_str()},
7766 + {"DriverOpts", driverOptEntry.c_str()},
7767 + };
7768 +
7769 + WSLCNetworkConnectionOptions options{};
7770 + options.NetworkName = networkName.c_str();
7771 + options.Settings = settings.data();
7772 + options.SettingsCount = static_cast<ULONG>(settings.size());
7773 + VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
7774 +
7775 + auto inspect = container.Inspect();
7776 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
7777 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
7778 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias1) != endpoint.Aliases.end());
7779 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias2) != endpoint.Aliases.end());
7780 + VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAddress);
7781 + VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
7782 + VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAMConfig->IPv4Address);
7783 + VERIFY_IS_TRUE(std::ranges::find(endpoint.IPAMConfig->LinkLocalIPs, linkLocal) != endpoint.IPAMConfig->LinkLocalIPs.end());
7784 + const auto opt = endpoint.DriverOpts.find(driverOptKey);
7785 + VERIFY_IS_TRUE(opt != endpoint.DriverOpts.end());
7786 + VERIFY_ARE_EQUAL(driverOptValue, opt->second);
7787 + }
7788 +
7789 + // Links: connect two containers to the same user-defined bridge, verify Links echoes back in inspect.
7790 + {
7791 + const std::string targetName = "connect-endpoint-link-target";
7792 + const std::string targetAlias = "db";
7793 + auto target = launchContainer(targetName);
7794 + WSLCNetworkConnectionOptions targetOptions{};
7795 + targetOptions.NetworkName = networkName.c_str();
7796 + const std::vector<KeyValuePair> targetSettings{{"Aliases", targetAlias.c_str()}};
7797 + targetOptions.Settings = targetSettings.data();
7798 + targetOptions.SettingsCount = static_cast<ULONG>(targetSettings.size());
7799 + VERIFY_SUCCEEDED(target.Get().ConnectToNetwork(&targetOptions));
7800 +
7801 + auto source = launchContainer("connect-endpoint-link-source");
7802 + const std::string linkEntry = targetName + ":" + targetAlias;
7803 + const std::vector<KeyValuePair> sourceSettings{{"Links", linkEntry.c_str()}};
7804 + WSLCNetworkConnectionOptions sourceOptions{};
7805 + sourceOptions.NetworkName = networkName.c_str();
7806 + sourceOptions.Settings = sourceSettings.data();
7807 + sourceOptions.SettingsCount = static_cast<ULONG>(sourceSettings.size());
7808 + VERIFY_SUCCEEDED(source.Get().ConnectToNetwork(&sourceOptions));
7809 +
7810 + auto inspect = source.Inspect();
7811 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
7812 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
7813 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Links, linkEntry) != endpoint.Links.end());
7814 + }
7815 +
7816 + // Connecting a stopped container succeeds — Docker allows attach in created/exited state.
7817 + {
7818 + auto container = launchContainer("connect-endpoint-stopped");
7819 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
7820 +
7821 + const std::string alias = "stopped-alias";
7822 + const std::vector<KeyValuePair> settings{{"Aliases", alias.c_str()}};
7823 +
7824 + WSLCNetworkConnectionOptions options{};
7825 + options.NetworkName = networkName.c_str();
7826 + options.Settings = settings.data();
7827 + options.SettingsCount = static_cast<ULONG>(settings.size());
7828 + VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
7829 +
7830 + auto inspect = container.Inspect();
7831 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
7832 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
7833 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias) != endpoint.Aliases.end());
7834 + }
7835 + }
7836 +
7837 WSLC_TEST_METHOD(NetworkAliasCreateTest)
7838 {
7839 auto createNetwork = [&](const std::string& name, const char* subnet) {
@@ -7729,14 +7946,16 @@ class WSLCTests
7946 expectError("alias-ctr-empty", networkName, {""}, E_INVALIDARG, L"Network alias cannot be empty.");
7947 }
7948
7732 - // Unknown KVP key on primary settings — rejected with E_NOTIMPL.
7949 + // Unknown KVP key on primary settings — rejected with the unified endpoint-settings error.
7950 {
7951 const std::string networkName = "alias-net-unknown";
7952 + const std::string unknownKey = "BogusKey";
7953 + const std::string unknownValue = "value";
7954 createNetwork(networkName, "172.63.0.0/16");
7955 auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
7956
7957 LPCSTR args[] = {"sleep", "99999"};
7739 - const KeyValuePair settings[] = {{"IPAddress", "10.0.0.5"}};
7958 + const KeyValuePair settings[] = {{unknownKey.c_str(), unknownValue.c_str()}};
7959 WSLCContainerOptions options{};
7960 options.Image = "debian:latest";
7961 options.Name = "alias-ctr-unknown";
@@ -7746,11 +7965,92 @@ class WSLCTests
7965 options.ContainerNetwork.SettingsCount = ARRAYSIZE(settings);
7966
7967 wil::com_ptr<IWSLCContainer> container;
7749 - VERIFY_ARE_EQUAL(E_NOTIMPL, m_defaultSession->CreateContainer(&options, nullptr, &container));
7968 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateContainer(&options, nullptr, &container));
7969 ValidateCOMErrorMessage(std::format(
7751 - L"Endpoint settings are not yet supported (network '{}').",
7752 - std::wstring(networkName.begin(), networkName.end()))
7753 - .c_str());
7970 + L"Unknown endpoint setting '{}' for network '{}'.",
7971 + std::wstring(unknownKey.begin(), unknownKey.end()),
7972 + std::wstring(networkName.begin(), networkName.end())));
7973 + }
7974 +
7975 + // Primary endpoint settings (IPAddress + Aliases + LinkLocalIPs + DriverOpts) round-trip via inspect at create time.
7976 + {
7977 + const std::string networkName = "alias-net-combo";
7978 + createNetwork(networkName, "172.64.0.0/16");
7979 + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
7980 +
7981 + const std::string alias1 = "primary-alias";
7982 + const std::string alias2 = "secondary-alias";
7983 + const std::string ipAddress = "172.64.0.42";
7984 + const std::string linkLocal = "169.254.11.5";
7985 + const std::string driverOptEntry = "com.docker.network.endpoint.custom=verify";
7986 + const std::string driverOptKey = "com.docker.network.endpoint.custom";
7987 + const std::string driverOptValue = "verify";
7988 +
7989 + LPCSTR args[] = {"sleep", "99999"};
7990 + const KeyValuePair settings[] = {
7991 + {"Aliases", alias1.c_str()},
7992 + {"Aliases", alias2.c_str()},
7993 + {"IPAddress", ipAddress.c_str()},
7994 + {"LinkLocalIPs", linkLocal.c_str()},
7995 + {"DriverOpts", driverOptEntry.c_str()},
7996 + };
7997 + WSLCContainerOptions options{};
7998 + options.Image = "debian:latest";
7999 + options.Name = "alias-ctr-combo";
8000 + options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
8001 + options.ContainerNetwork.NetworkMode = networkName.c_str();
8002 + options.ContainerNetwork.Settings = settings;
8003 + options.ContainerNetwork.SettingsCount = ARRAYSIZE(settings);
8004 +
8005 + wil::com_ptr<IWSLCContainer> containerCom;
8006 + VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, nullptr, &containerCom));
8007 + RunningWSLCContainer container(std::move(containerCom), WSLCProcessFlagsNone);
8008 + VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, nullptr, nullptr));
8009 +
8010 + auto inspect = container.Inspect();
8011 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
8012 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
8013 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias1) != endpoint.Aliases.end());
8014 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias2) != endpoint.Aliases.end());
8015 + VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAddress);
8016 + VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
8017 + VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAMConfig->IPv4Address);
8018 + VERIFY_IS_TRUE(std::ranges::find(endpoint.IPAMConfig->LinkLocalIPs, linkLocal) != endpoint.IPAMConfig->LinkLocalIPs.end());
8019 + const auto opt = endpoint.DriverOpts.find(driverOptKey);
8020 + VERIFY_IS_TRUE(opt != endpoint.DriverOpts.end());
8021 + VERIFY_ARE_EQUAL(driverOptValue, opt->second);
8022 + }
8023 +
8024 + // Primary endpoint Links: launch a target container with an alias, then a source with --link at create time.
8025 + {
8026 + const std::string networkName = "alias-net-link";
8027 + const std::string targetName = "alias-ctr-link-target";
8028 + const std::string targetAlias = "db";
8029 + createNetwork(networkName, "172.65.0.0/16");
8030 + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
8031 +
8032 + auto target = launchWithAliases(targetName, networkName, {targetAlias});
8033 +
8034 + const std::string linkEntry = targetName + ":" + targetAlias;
8035 + LPCSTR args[] = {"sleep", "99999"};
8036 + const KeyValuePair settings[] = {{"Links", linkEntry.c_str()}};
8037 + WSLCContainerOptions options{};
8038 + options.Image = "debian:latest";
8039 + options.Name = "alias-ctr-link-source";
8040 + options.InitProcessOptions.CommandLine = {.Values = args, .Count = ARRAYSIZE(args)};
8041 + options.ContainerNetwork.NetworkMode = networkName.c_str();
8042 + options.ContainerNetwork.Settings = settings;
8043 + options.ContainerNetwork.SettingsCount = ARRAYSIZE(settings);
8044 +
8045 + wil::com_ptr<IWSLCContainer> sourceCom;
8046 + VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, nullptr, &sourceCom));
8047 + RunningWSLCContainer source(std::move(sourceCom), WSLCProcessFlagsNone);
8048 + VERIFY_SUCCEEDED(source.Get().Start(WSLCContainerStartFlagsAttach, nullptr, nullptr));
8049 +
8050 + auto inspect = source.Inspect();
8051 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
8052 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
8053 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Links, linkEntry) != endpoint.Links.end());
8054 }
8055 }
8056
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+6
@@ -110,6 +110,12 @@ class WSLCCLIExecutionUnitTests
110 dataMap.Add<Data::Networks>(std::move(networks));
111 handled = true;
112 }
113 + else if (dataType == Data::NetworkEndpointOptions)
114 + {
115 + wsl::windows::wslc::models::NetworkEndpointOptions endpointOptions;
116 + dataMap.Add<Data::NetworkEndpointOptions>(std::move(endpointOptions));
117 + handled = true;
118 + }
119
120 if (!handled)
121 {
test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp
+110 -1
@@ -33,6 +33,7 @@ class WSLCE2ENetworkTests
33 TEST_CLASS_CLEANUP(ClassCleanup)
34 {
35 EnsureContainerDoesNotExist(WslcContainerName);
36 + EnsureContainerDoesNotExist(WslcTargetContainerName);
37 EnsureNetworkDoesNotExist(TestNetworkName);
38 EnsureImageIsDeleted(DebianImage);
39 return true;
@@ -41,6 +42,7 @@ class WSLCE2ENetworkTests
42 TEST_METHOD_SETUP(TestMethodSetup)
43 {
44 EnsureContainerDoesNotExist(WslcContainerName);
45 + EnsureContainerDoesNotExist(WslcTargetContainerName);
46 EnsureNetworkDoesNotExist(TestNetworkName);
47 return true;
48 }
@@ -156,7 +158,9 @@ class WSLCE2ENetworkTests
158 result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName));
159 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
160 VERIFY_IS_TRUE(result.Stderr.has_value());
159 - VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*'host' or 'none'*Error code: *\r\n");
161 + VerifyPatternMatch(
162 + string::WideToMultiByte(result.Stderr.value()),
163 + "*does not support connecting or disconnecting additional networks*Error code: *\r\n");
164 }
165
166 WSLC_TEST_METHOD(WSLCE2E_Network_Connect_AlreadyConnected_DockerErrorPropagated)
@@ -174,6 +178,110 @@ class WSLCE2ENetworkTests
178 VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*already exists*Error code: *\r\n");
179 }
180
181 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_WithEndpointFlags_RoundTrips)
182 + {
183 + const std::wstring subnet = L"172.72.0.0/16";
184 + const std::wstring ipAddress = L"172.72.0.42";
185 + const std::wstring alias1 = L"primary-alias";
186 + const std::wstring alias2 = L"secondary-alias";
187 + const std::wstring linkLocal = L"169.254.11.5";
188 +
189 + auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
190 + result.Verify({.Stderr = L"", .ExitCode = 0});
191 +
192 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
193 + result.Verify({.Stderr = L"", .ExitCode = 0});
194 +
195 + result = RunWslc(std::format(
196 + L"network connect --network-alias {} --network-alias {} --ip {} --link-local-ip {} {} {}", alias1, alias2, ipAddress, linkLocal, TestNetworkName, WslcContainerName));
197 + result.Verify({.Stderr = L"", .ExitCode = 0});
198 +
199 + const auto inspect = InspectContainer(WslcContainerName);
200 + const auto networkKey = string::WideToMultiByte(TestNetworkName);
201 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkKey));
202 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkKey);
203 + VERIFY_ARE_EQUAL(string::WideToMultiByte(ipAddress), endpoint.IPAddress);
204 + VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
205 + VERIFY_ARE_EQUAL(string::WideToMultiByte(ipAddress), endpoint.IPAMConfig->IPv4Address);
206 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, string::WideToMultiByte(alias1)) != endpoint.Aliases.end());
207 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, string::WideToMultiByte(alias2)) != endpoint.Aliases.end());
208 + VERIFY_IS_TRUE(
209 + std::ranges::find(endpoint.IPAMConfig->LinkLocalIPs, string::WideToMultiByte(linkLocal)) !=
210 + endpoint.IPAMConfig->LinkLocalIPs.end());
211 + }
212 +
213 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_InvalidIp_Rejected)
214 + {
215 + const std::wstring badIp = L"not-an-ip";
216 +
217 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
218 + result.Verify({.Stderr = L"", .ExitCode = 0});
219 +
220 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
221 + result.Verify({.Stderr = L"", .ExitCode = 0});
222 +
223 + result = RunWslc(std::format(L"network connect --ip {} {} {}", badIp, TestNetworkName, WslcContainerName));
224 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
225 + VERIFY_IS_TRUE(result.Stderr.has_value());
226 + VerifyPatternMatch(
227 + string::WideToMultiByte(result.Stderr.value()), std::format("*Invalid IP address '{}'*", string::WideToMultiByte(badIp)));
228 + }
229 +
230 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_DriverOpt_RoundTrips)
231 + {
232 + const std::wstring driverOptKey = L"com.docker.network.endpoint.custom";
233 + const std::wstring driverOptValue = L"verify";
234 +
235 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
236 + result.Verify({.Stderr = L"", .ExitCode = 0});
237 +
238 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
239 + result.Verify({.Stderr = L"", .ExitCode = 0});
240 +
241 + result = RunWslc(std::format(L"network connect --driver-opt {}={} {} {}", driverOptKey, driverOptValue, TestNetworkName, WslcContainerName));
242 + result.Verify({.Stderr = L"", .ExitCode = 0});
243 +
244 + const auto inspect = InspectContainer(WslcContainerName);
245 + const auto networkKey = string::WideToMultiByte(TestNetworkName);
246 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkKey));
247 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkKey);
248 + const auto keyUtf8 = string::WideToMultiByte(driverOptKey);
249 + const auto valueUtf8 = string::WideToMultiByte(driverOptValue);
250 + const auto driverOptIt = endpoint.DriverOpts.find(keyUtf8);
251 + VERIFY_IS_TRUE(driverOptIt != endpoint.DriverOpts.end());
252 + VERIFY_ARE_EQUAL(valueUtf8, driverOptIt->second);
253 + }
254 +
255 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_Link_RoundTrips)
256 + {
257 + const std::wstring targetAlias = L"db";
258 +
259 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
260 + result.Verify({.Stderr = L"", .ExitCode = 0});
261 +
262 + result = RunWslc(std::format(
263 + L"container run -d --network {} --network-alias {} --name {} {} sleep infinity",
264 + TestNetworkName,
265 + targetAlias,
266 + WslcTargetContainerName,
267 + DebianImage.NameAndTag()));
268 + result.Verify({.Stderr = L"", .ExitCode = 0});
269 +
270 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
271 + result.Verify({.Stderr = L"", .ExitCode = 0});
272 +
273 + const std::wstring linkEntry = std::format(L"{}:{}", WslcTargetContainerName, targetAlias);
274 + result = RunWslc(std::format(L"network connect --link {} {} {}", linkEntry, TestNetworkName, WslcContainerName));
275 + result.Verify({.Stderr = L"", .ExitCode = 0});
276 +
277 + const auto inspect = InspectContainer(WslcContainerName);
278 + const auto networkKey = string::WideToMultiByte(TestNetworkName);
279 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkKey));
280 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkKey);
281 + const auto linkEntryUtf8 = string::WideToMultiByte(linkEntry);
282 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Links, linkEntryUtf8) != endpoint.Links.end());
283 + }
284 +
285 WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_Valid)
286 {
287 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
@@ -218,6 +326,7 @@ class WSLCE2ENetworkTests
326
327 private:
328 const std::wstring WslcContainerName = L"wslc-e2e-network-connect-container";
329 + const std::wstring WslcTargetContainerName = L"wslc-e2e-network-connect-target";
330 const std::wstring TestNetworkName = L"wslc-e2e-network-connect";
331 const TestImage& DebianImage = DebianTestImage();
332 };