@samitouri / QOSAMI-WSL / commits / 7f009ebe

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