Support connecting and disconnecting containers to networks after creation (#40549)
beena352 committed
Jun 8, 2026 at 14:51 UTC
7f009ebe88b8d89f649d90373b0311686f66f504
8 files changed
+259
-1
localization/strings/en-US/Resources.resw
+8
@@ -2119,6 +2119,10 @@ Usage:
2119
<value>Container '{}' is running.</value>
2120
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2121
</data>
2122
+ <data name="MessageWslcContainerIpAddressNotSupported" xml:space="preserve">
2123
+ <value>ContainerIpAddress is not yet supported.</value>
2124
+ <comment>{Locked="ContainerIpAddress"}Command line arguments, file names and string inserts should not be translated</comment>
2125
+ </data>
2126
<data name="MessageWslcContainerModeNoPorts" xml:space="preserve">
2127
<value>Port mappings are not supported with container network mode; ports are owned by the target container.</value>
2128
</data>
@@ -2330,6 +2334,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2334
<value>Unsupported network mode: '{}'</value>
2335
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2336
</data>
2337
+ <data name="MessageWslcAdditionalNetworksRequirePrimary" xml:space="preserve">
2338
+ <value>Additional networks are not allowed when the primary network mode is 'host' or 'none'.</value>
2339
+ <comment>{Locked="host"}{Locked="none"}Command line arguments, file names and string inserts should not be translated</comment>
2340
+ </data>
2341
<data name="MessageWslcDuplicateNetwork" xml:space="preserve">
2342
<value>Duplicate network: '{}'</value>
2343
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/inc/docker_schema.h
+8
@@ -162,6 +162,14 @@ struct Network
162
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Labels);
163
};
164
165
+struct ContainerNetworkRequest
166
+{
167
+ using TResponse = void;
168
+ std::string Container;
169
+
170
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerNetworkRequest, Container);
171
+};
172
+
173
struct EmptyObject
174
{
175
};
src/windows/service/inc/wslc.idl
+9
-1
@@ -264,11 +264,17 @@ typedef struct _WSLCUlimit
264
typedef struct _WSLCNetworkConnection
265
{
266
[string] LPCSTR NetworkName;
267
-
267
[unique, size_is(SettingsCount)] const KeyValuePair* Settings;
268
ULONG SettingsCount;
269
} WSLCNetworkConnection;
270
271
+// Options for IWSLCContainer::ConnectToNetwork.
272
+typedef struct _WSLCNetworkConnectionOptions
273
+{
274
+ [unique] LPCSTR NetworkName;
275
+ [unique] LPCSTR ContainerIpAddress; // Reserved for future --ip support; must be NULL today.
276
+} WSLCNetworkConnectionOptions;
277
+
278
typedef struct _WSLCContainerNetwork
279
{
280
[unique, string] LPCSTR NetworkMode;
@@ -581,6 +587,8 @@ interface IWSLCContainer : IUnknown
587
HRESULT GetLabels([out, size_is(, *Count)] WSLCLabelInformation** Labels, [out] ULONG* Count);
588
HRESULT Kill([in] WSLCSignal Signal);
589
HRESULT Stats([out] LPSTR* Output);
590
+ HRESULT ConnectToNetwork([in] const WSLCNetworkConnectionOptions* Options);
591
+ HRESULT DisconnectFromNetwork([in] LPCSTR NetworkName);
592
}
593
594
typedef enum _WSLCDeletedImageType
src/windows/wslcsession/DockerHTTPClient.cpp
+10
@@ -449,6 +449,16 @@ void DockerHTTPClient::RemoveNetwork(const std::string& Name)
449
Transaction(verb::delete_, URL::Create("/networks/{}", Name));
450
}
451
452
+void DockerHTTPClient::ConnectContainerToNetwork(const std::string& NetworkName, const docker_schema::ContainerNetworkRequest& Request)
453
+{
454
+ Transaction(verb::post, URL::Create("/networks/{}/connect", NetworkName), Request);
455
+}
456
+
457
+void DockerHTTPClient::DisconnectContainerFromNetwork(const std::string& NetworkName, const docker_schema::ContainerNetworkRequest& Request)
458
+{
459
+ Transaction(verb::post, URL::Create("/networks/{}/disconnect", NetworkName), Request);
460
+}
461
+
462
std::vector<docker_schema::Network> DockerHTTPClient::ListNetworks()
463
{
464
return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Network>>(verb::get, URL::Create("/networks"));
src/windows/wslcsession/DockerHTTPClient.h
+2
@@ -150,6 +150,8 @@ public:
150
void RemoveNetwork(const std::string& Name);
151
std::vector<common::docker_schema::Network> ListNetworks();
152
common::docker_schema::Network InspectNetwork(const std::string& Name);
153
+ void ConnectContainerToNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
154
+ void DisconnectContainerFromNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
155
156
// Image management.
157
std::unique_ptr<HTTPRequestContext> PullImage(
src/windows/wslcsession/WSLCContainer.cpp
+82
@@ -2310,6 +2310,72 @@ void WSLCContainerImpl::GetLabels(WSLCLabelInformation** Labels, ULONG* Count) c
2310
*Labels = labelsArray.release();
2311
}
2312
2313
+void WSLCContainerImpl::ConnectToNetwork(const WSLCNetworkConnectionOptions* Options)
2314
+{
2315
+ THROW_HR_IF(E_POINTER, Options == nullptr);
2316
+ THROW_HR_WITH_USER_ERROR_IF(E_NOTIMPL, Localization::MessageWslcContainerIpAddressNotSupported(), Options->ContainerIpAddress != nullptr);
2317
+
2318
+ THROW_HR_WITH_USER_ERROR_IF(
2319
+ E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !Options->NetworkName || strlen(Options->NetworkName) == 0);
2320
+
2321
+ auto lock = m_lock.lock_shared();
2322
+
2323
+ THROW_HR_WITH_USER_ERROR_IF(
2324
+ E_INVALIDARG,
2325
+ Localization::MessageWslcAdditionalNetworksRequirePrimary(),
2326
+ m_networkMode == "host" || m_networkMode == "none");
2327
+
2328
+ common::docker_schema::ContainerNetworkRequest request{};
2329
+ request.Container = m_id;
2330
+
2331
+ try
2332
+ {
2333
+ m_dockerClient.ConnectContainerToNetwork(Options->NetworkName, request);
2334
+ }
2335
+ catch (const DockerHTTPException& e)
2336
+ {
2337
+ THROW_HR_WITH_USER_ERROR_IF(
2338
+ WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(Options->NetworkName), e.StatusCode() == 404);
2339
+ THROW_DOCKER_USER_ERROR_MSG(e, "Failed to connect container '%hs' to network '%hs'", m_id.c_str(), Options->NetworkName);
2340
+ }
2341
+
2342
+ WSL_LOG(
2343
+ "ContainerConnectedToNetwork",
2344
+ TraceLoggingValue(m_id.c_str(), "ContainerId"),
2345
+ TraceLoggingValue(Options->NetworkName, "NetworkName"));
2346
+}
2347
+
2348
+void WSLCContainerImpl::DisconnectFromNetwork(LPCSTR NetworkName)
2349
+{
2350
+ THROW_HR_IF(E_POINTER, NetworkName == nullptr);
2351
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), strlen(NetworkName) == 0);
2352
+
2353
+ auto lock = m_lock.lock_shared();
2354
+
2355
+ THROW_HR_WITH_USER_ERROR_IF(
2356
+ E_INVALIDARG,
2357
+ Localization::MessageWslcAdditionalNetworksRequirePrimary(),
2358
+ m_networkMode == "host" || m_networkMode == "none");
2359
+
2360
+ common::docker_schema::ContainerNetworkRequest request{};
2361
+ request.Container = m_id;
2362
+
2363
+ try
2364
+ {
2365
+ m_dockerClient.DisconnectContainerFromNetwork(NetworkName, request);
2366
+ }
2367
+ catch (const DockerHTTPException& e)
2368
+ {
2369
+ THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(NetworkName), e.StatusCode() == 404);
2370
+ THROW_DOCKER_USER_ERROR_MSG(e, "Failed to disconnect container '%hs' from network '%hs'", m_id.c_str(), NetworkName);
2371
+ }
2372
+
2373
+ WSL_LOG(
2374
+ "ContainerDisconnectedFromNetwork",
2375
+ TraceLoggingValue(m_id.c_str(), "ContainerId"),
2376
+ TraceLoggingValue(NetworkName, "NetworkName"));
2377
+}
2378
+
2379
HRESULT WSLCContainer::GetLabels(WSLCLabelInformation** Labels, ULONG* Count)
2380
try
2381
{
@@ -2323,6 +2389,22 @@ try
2389
}
2390
CATCH_RETURN();
2391
2392
+HRESULT WSLCContainer::ConnectToNetwork(const WSLCNetworkConnectionOptions* Options)
2393
+try
2394
+{
2395
+ COMServiceExecutionContext context;
2396
+ return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options);
2397
+}
2398
+CATCH_RETURN();
2399
+
2400
+HRESULT WSLCContainer::DisconnectFromNetwork(LPCSTR NetworkName)
2401
+try
2402
+{
2403
+ COMServiceExecutionContext context;
2404
+ return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName);
2405
+}
2406
+CATCH_RETURN();
2407
+
2408
HRESULT WSLCContainer::InterfaceSupportsErrorInfo(REFIID riid)
2409
{
2410
return riid == __uuidof(IWSLCContainer) ? S_OK : S_FALSE;
src/windows/wslcsession/WSLCContainer.h
+4
@@ -105,6 +105,8 @@ public:
105
void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const;
106
void Stats(LPSTR* Output) const;
107
void GetLabels(WSLCLabelInformation** Labels, ULONG* Count) const;
108
+ void ConnectToNetwork(const WSLCNetworkConnectionOptions* Options);
109
+ void DisconnectFromNetwork(LPCSTR NetworkName);
110
111
void CopyTo(IWSLCContainer** Container) const;
112
@@ -237,6 +239,8 @@ public:
239
IFACEMETHOD(GetName)(_Out_ LPSTR* Name) override;
240
IFACEMETHOD(GetLabels)(_Out_ WSLCLabelInformation** Labels, _Out_ ULONG* Count) override;
241
IFACEMETHOD(Stats)(_Out_ LPSTR* Output) override;
242
+ IFACEMETHOD(ConnectToNetwork)(_In_ const WSLCNetworkConnectionOptions* Options) override;
243
+ IFACEMETHOD(DisconnectFromNetwork)(_In_ LPCSTR NetworkName) override;
244
245
IFACEMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
246
test/windows/WSLCTests.cpp
+136
@@ -6538,6 +6538,142 @@ class WSLCTests
6538
VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), m_defaultSession->DeleteNetwork(additionalNetwork.c_str()));
6539
}
6540
6541
+ WSLC_TEST_METHOD(ConnectDisconnectContainerNetworkTest)
6542
+ {
6543
+ auto launchContainer = [&](const std::string& name, std::string networkMode = "bridge") {
6544
+ WSLCContainerLauncher launcher("debian:latest", name, {"sleep", "99999"}, {}, std::move(networkMode));
6545
+ return launcher.Launch(*m_defaultSession);
6546
+ };
6547
+
6548
+ auto createNetwork = [&](const std::string& name, const char* subnet) {
6549
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(name.c_str()));
6550
+ WSLCDriverOption opts[] = {{"Subnet", subnet}};
6551
+ WSLCNetworkOptions netOpts{};
6552
+ netOpts.Name = name.c_str();
6553
+ netOpts.Driver = "bridge";
6554
+ netOpts.DriverOpts = opts;
6555
+ netOpts.DriverOptsCount = ARRAYSIZE(opts);
6556
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts, nullptr));
6557
+ };
6558
+
6559
+ // Verifies both ConnectToNetwork and DisconnectFromNetwork reject with the same error for the given network name.
6560
+ auto expectBothReject = [&](auto& container, LPCSTR networkName, HRESULT hr, LPCWSTR message) {
6561
+ WSLCNetworkConnectionOptions options{};
6562
+ options.NetworkName = networkName;
6563
+ VERIFY_ARE_EQUAL(hr, container.Get().ConnectToNetwork(&options));
6564
+ ValidateCOMErrorMessage(message);
6565
+ VERIFY_ARE_EQUAL(hr, container.Get().DisconnectFromNetwork(networkName));
6566
+ ValidateCOMErrorMessage(message);
6567
+ };
6568
+
6569
+ // Round-trip: connect to a network, verify via inspect, then disconnect.
6570
+ {
6571
+ const std::string networkName = "test-connect-disconnect-net";
6572
+ createNetwork(networkName, "172.53.0.0/16");
6573
+ auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
6574
+
6575
+ auto container = launchContainer("test-connect-disconnect-ctr");
6576
+
6577
+ WSLCNetworkConnectionOptions options{};
6578
+ options.NetworkName = networkName.c_str();
6579
+ VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
6580
+
6581
+ auto inspect = container.Inspect();
6582
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
6583
+ VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.at(networkName).IPAddress.empty());
6584
+
6585
+ VERIFY_SUCCEEDED(container.Get().DisconnectFromNetwork(networkName.c_str()));
6586
+
6587
+ auto inspectAfter = container.Inspect();
6588
+ VERIFY_IS_FALSE(inspectAfter.NetworkSettings.Networks.contains(networkName));
6589
+ VERIFY_IS_TRUE(inspectAfter.NetworkSettings.Networks.contains("bridge"));
6590
+ }
6591
+
6592
+ // Empty and non-existent network name.
6593
+ {
6594
+ auto container = launchContainer("test-connect-invalid-name");
6595
+
6596
+ expectBothReject(container, "", E_INVALIDARG, L"Network name cannot be empty.");
6597
+
6598
+ const std::string nonExistentNetwork = "nonexistent-network";
6599
+ const auto expectedError =
6600
+ std::format(L"Network not found: '{}'", std::wstring(nonExistentNetwork.begin(), nonExistentNetwork.end()));
6601
+ expectBothReject(container, nonExistentNetwork.c_str(), WSLC_E_NETWORK_NOT_FOUND, expectedError.c_str());
6602
+ }
6603
+
6604
+ // Host and none mode rejection.
6605
+ {
6606
+ const std::string networkName = "test-connect-mode-net";
6607
+ createNetwork(networkName, "172.52.0.0/16");
6608
+ auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
6609
+
6610
+ auto expectModeRejection = [&](const std::string& name, std::string mode) {
6611
+ auto container = launchContainer(name, std::move(mode));
6612
+ expectBothReject(
6613
+ container,
6614
+ networkName.c_str(),
6615
+ E_INVALIDARG,
6616
+ L"Additional networks are not allowed when the primary network mode is 'host' or 'none'.");
6617
+ };
6618
+
6619
+ expectModeRejection("test-connect-host-ctr", "host");
6620
+ expectModeRejection("test-connect-none-ctr", "none");
6621
+ }
6622
+
6623
+ // ContainerIpAddress not supported.
6624
+ {
6625
+ auto container = launchContainer("test-connect-ip-ctr");
6626
+
6627
+ WSLCNetworkConnectionOptions options{};
6628
+ options.NetworkName = "bridge";
6629
+ options.ContainerIpAddress = "10.0.0.5";
6630
+ VERIFY_ARE_EQUAL(E_NOTIMPL, container.Get().ConnectToNetwork(&options));
6631
+ ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported.");
6632
+ }
6633
+
6634
+ // Connect and disconnect from the container's primary network.
6635
+ {
6636
+ const std::string containerName = "test-connect-primary-ctr";
6637
+ auto container = launchContainer(containerName);
6638
+
6639
+ // Connect to primary should fail — already connected.
6640
+ WSLCNetworkConnectionOptions options{};
6641
+ options.NetworkName = "bridge";
6642
+ VERIFY_ARE_EQUAL(E_FAIL, container.Get().ConnectToNetwork(&options));
6643
+ // Docker returns the container name in the error, not the ID.
6644
+ const auto expectedError = std::format(
6645
+ L"endpoint with name {} already exists in network bridge", std::wstring(containerName.begin(), containerName.end()));
6646
+ ValidateCOMErrorMessage(expectedError);
6647
+
6648
+ // Disconnect from primary should succeed.
6649
+ VERIFY_SUCCEEDED(container.Get().DisconnectFromNetwork("bridge"));
6650
+
6651
+ auto inspect = container.Inspect();
6652
+ VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.contains("bridge"));
6653
+ }
6654
+
6655
+ // Connect to same secondary network twice.
6656
+ {
6657
+ const std::string networkName = "test-connect-twice-net";
6658
+ const std::string containerName = "test-connect-twice-ctr";
6659
+ createNetwork(networkName, "172.51.0.0/16");
6660
+ auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
6661
+
6662
+ auto container = launchContainer(containerName);
6663
+
6664
+ WSLCNetworkConnectionOptions options{};
6665
+ options.NetworkName = networkName.c_str();
6666
+ VERIFY_SUCCEEDED(container.Get().ConnectToNetwork(&options));
6667
+ VERIFY_ARE_EQUAL(E_FAIL, container.Get().ConnectToNetwork(&options));
6668
+ // Docker returns the container name in the error, not the ID.
6669
+ const auto expectedError = std::format(
6670
+ L"endpoint with name {} already exists in network {}",
6671
+ std::wstring(containerName.begin(), containerName.end()),
6672
+ std::wstring(networkName.begin(), networkName.end()));
6673
+ ValidateCOMErrorMessage(expectedError);
6674
+ }
6675
+ }
6676
+
6677
WSLC_TEST_METHOD(ContainerNetworkModeHappyPathTest)
6678
{
6679
// Start container A on the default (bridged) network, then start container B sharing A's