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
};