diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index d7acd38ce..9c4c37685 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -158,6 +158,22 @@ struct Network NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Labels); }; +struct ConnectNetworkRequest +{ + using TResponse = void; + std::string Container; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ConnectNetworkRequest, Container); +}; + +struct DisconnectNetworkRequest +{ + using TResponse = void; + std::string Container; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(DisconnectNetworkRequest, Container); +}; + struct EmptyObject { }; diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 2ad3216f8..fca6b73b3 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -790,6 +790,8 @@ interface IWSLCSession : IUnknown HRESULT DeleteNetwork([in] LPCSTR Name); HRESULT ListNetworks([out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count); HRESULT InspectNetwork([in] LPCSTR Name, [out] LPSTR* Output); + HRESULT AttachContainerToNetwork([in] LPCSTR ContainerId, [in] const WSLCNetworkAttachment* Attachment); + HRESULT DetachContainerFromNetwork([in] LPCSTR ContainerId, [in] LPCSTR NetworkName); } // diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 1900c7d76..d3e4daff6 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -469,6 +469,16 @@ void DockerHTTPClient::RemoveNetwork(const std::string& Name) Transaction(verb::delete_, URL::Create("/networks/{}", Name)); } +void DockerHTTPClient::ConnectContainerToNetwork(const std::string& NetworkName, const docker_schema::ConnectNetworkRequest& Request) +{ + Transaction(verb::post, URL::Create("/networks/{}/connect", NetworkName), Request); +} + +void DockerHTTPClient::DisconnectContainerFromNetwork(const std::string& NetworkName, const docker_schema::DisconnectNetworkRequest& Request) +{ + Transaction(verb::post, URL::Create("/networks/{}/disconnect", NetworkName), Request); +} + std::vector DockerHTTPClient::ListNetworks() { return Transaction>(verb::get, URL::Create("/networks")); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index b9851f46c..54150487a 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -149,6 +149,8 @@ class DockerHTTPClient void RemoveNetwork(const std::string& Name); std::vector ListNetworks(); common::docker_schema::Network InspectNetwork(const std::string& Name); + void ConnectContainerToNetwork(const std::string& NetworkName, const common::docker_schema::ConnectNetworkRequest& Request); + void DisconnectContainerFromNetwork(const std::string& NetworkName, const common::docker_schema::DisconnectNetworkRequest& Request); // Image management. struct ListImagesFilters diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 4ebfcd758..47be24269 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -125,6 +125,11 @@ class WSLCContainerImpl return m_containerFlags; } + WSLCContainerNetworkType NetworkMode() const noexcept + { + return m_networkingMode; + } + static std::unique_ptr Create( const WSLCContainerOptions& Options, const std::string& Name, diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index bc40f2925..7d4d2489c 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -2295,6 +2295,115 @@ try } CATCH_RETURN(); +HRESULT WSLCSession::AttachContainerToNetwork(LPCSTR ContainerId, const WSLCNetworkAttachment* Attachment) +try +{ + COMServiceExecutionContext context; + + RETURN_HR_IF_NULL(E_POINTER, ContainerId); + RETURN_HR_IF_NULL(E_POINTER, Attachment); + + THROW_HR_WITH_USER_ERROR_IF(E_NOTIMPL, Localization::MessageWslcContainerIpAddressNotSupported(), Attachment->ContainerIpAddress != nullptr); + + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !Attachment->NetworkName || strlen(Attachment->NetworkName) == 0); + + std::string containerId = ContainerId; + std::string networkName = Attachment->NetworkName; + ValidateName(containerId.c_str(), WSLC_MAX_CONTAINER_NAME_LENGTH); + ValidateName(networkName.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); + + auto lock = m_lock.lock_shared(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + + std::scoped_lock locks(m_containersLock, m_networksLock); + std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; }); + + auto containerIt = m_containers.find(containerId); + THROW_HR_WITH_USER_ERROR_IF( + WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(containerId), containerIt == m_containers.end()); + + auto networkIt = m_networks.find(networkName); + THROW_HR_WITH_USER_ERROR_IF( + WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(networkName), networkIt == m_networks.end()); + + const auto networkMode = containerIt->second->NetworkMode(); + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, + Localization::MessageWslcAdditionalNetworksRequirePrimary(), + networkMode == WSLCContainerNetworkTypeHost || networkMode == WSLCContainerNetworkTypeNone); + + docker_schema::ConnectNetworkRequest request{}; + request.Container = containerId; + + try + { + m_dockerClient->ConnectContainerToNetwork(networkName, request); + } + catch (const DockerHTTPException& e) + { + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach container '%hs' to network '%hs'", containerId.c_str(), networkName.c_str()); + } + + WSL_LOG( + "ContainerAttachedToNetwork", + TraceLoggingValue(containerId.c_str(), "ContainerId"), + TraceLoggingValue(networkName.c_str(), "NetworkName")); + + return S_OK; +} +CATCH_RETURN(); + +HRESULT WSLCSession::DetachContainerFromNetwork(LPCSTR ContainerId, LPCSTR NetworkName) +try +{ + COMServiceExecutionContext context; + + RETURN_HR_IF_NULL(E_POINTER, ContainerId); + RETURN_HR_IF_NULL(E_POINTER, NetworkName); + + std::string containerId = ContainerId; + std::string networkName = NetworkName; + ValidateName(containerId.c_str(), WSLC_MAX_CONTAINER_NAME_LENGTH); + ValidateName(networkName.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); + + auto lock = m_lock.lock_shared(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + + std::scoped_lock locks(m_containersLock, m_networksLock); + std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; }); + + auto containerIt = m_containers.find(containerId); + THROW_HR_WITH_USER_ERROR_IF( + WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(containerId), containerIt == m_containers.end()); + + auto networkIt = m_networks.find(networkName); + THROW_HR_WITH_USER_ERROR_IF( + WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(networkName), networkIt == m_networks.end()); + + docker_schema::DisconnectNetworkRequest request{}; + request.Container = containerId; + + try + { + m_dockerClient->DisconnectContainerFromNetwork(networkName, request); + } + catch (const DockerHTTPException& e) + { + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to detach container '%hs' from network '%hs'", containerId.c_str(), networkName.c_str()); + } + + WSL_LOG( + "ContainerDetachedFromNetwork", + TraceLoggingValue(containerId.c_str(), "ContainerId"), + TraceLoggingValue(networkName.c_str(), "NetworkName")); + + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::ListNetworks(WSLCNetworkInformation** Networks, ULONG* Count) try { diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 572e721c8..a482b9b04 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -138,6 +138,8 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession IFACEMETHOD(DeleteNetwork)(_In_ LPCSTR Name) override; IFACEMETHOD(ListNetworks)(_Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count) override; IFACEMETHOD(InspectNetwork)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override; + IFACEMETHOD(AttachContainerToNetwork)(_In_ LPCSTR ContainerId, _In_ const WSLCNetworkAttachment* Attachment) override; + IFACEMETHOD(DetachContainerFromNetwork)(_In_ LPCSTR ContainerId, _In_ LPCSTR NetworkName) override; IFACEMETHOD(Terminate()) override; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index eb87c6376..733248de9 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6248,6 +6248,106 @@ class WSLCTests ValidateCOMErrorMessage(L"Container network name is required for custom network type."); } + WSLC_TEST_METHOD(AttachDetachContainerNetworkRoundTripTest) + { + const std::string networkName = "test-attach-detach-net"; + const std::string containerName = "test-attach-detach-ctr"; + + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); + + WSLCDriverOption opts[] = {{"Subnet", "172.50.0.0/16"}}; + WSLCNetworkOptions netOpts{}; + netOpts.Name = networkName.c_str(); + netOpts.Driver = "bridge"; + netOpts.DriverOpts = opts; + netOpts.DriverOptsCount = ARRAYSIZE(opts); + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts)); + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); }); + + WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged); + auto container = launcher.Launch(*m_defaultSession); + + WSLCNetworkAttachment attachment{}; + attachment.NetworkName = networkName.c_str(); + VERIFY_SUCCEEDED(container.Get().AttachToNetwork(&attachment)); + + auto inspect = container.Inspect(); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName)); + VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.at(networkName).IPAddress.empty()); + + VERIFY_SUCCEEDED(container.Get().DetachFromNetwork(networkName.c_str())); + + auto inspectAfter = container.Inspect(); + VERIFY_IS_FALSE(inspectAfter.NetworkSettings.Networks.contains(networkName)); + } + + WSLC_TEST_METHOD(AttachContainerEmptyNetworkNameTest) + { + const std::string containerName = "test-attach-empty-net"; + + WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}); + auto container = launcher.Launch(*m_defaultSession); + + WSLCNetworkAttachment attachment{}; + attachment.NetworkName = ""; + VERIFY_ARE_EQUAL(E_INVALIDARG, container.Get().AttachToNetwork(&attachment)); + ValidateCOMErrorMessage(L"Network name cannot be empty."); + } + + WSLC_TEST_METHOD(AttachHostOrNoneModeContainerRejectedTest) + { + const std::string networkName = "test-attach-mode-net"; + const std::string hostContainerName = "test-attach-host-ctr"; + const std::string noneContainerName = "test-attach-none-ctr"; + + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); + + WSLCDriverOption opts[] = {{"Subnet", "172.52.0.0/16"}}; + WSLCNetworkOptions netOpts{}; + netOpts.Name = networkName.c_str(); + netOpts.Driver = "bridge"; + netOpts.DriverOpts = opts; + netOpts.DriverOptsCount = ARRAYSIZE(opts); + VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts)); + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); }); + + { + WSLCContainerLauncher launcher( + "debian:latest", hostContainerName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost); + auto container = launcher.Launch(*m_defaultSession); + + WSLCNetworkAttachment attachment{}; + attachment.NetworkName = networkName.c_str(); + VERIFY_ARE_EQUAL(E_INVALIDARG, container.Get().AttachToNetwork(&attachment)); + ValidateCOMErrorMessage(L"Additional networks are not allowed when the primary network mode is 'host' or 'none'."); + } + + { + WSLCContainerLauncher launcher( + "debian:latest", noneContainerName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeNone); + auto container = launcher.Launch(*m_defaultSession); + + WSLCNetworkAttachment attachment{}; + attachment.NetworkName = networkName.c_str(); + VERIFY_ARE_EQUAL(E_INVALIDARG, container.Get().AttachToNetwork(&attachment)); + ValidateCOMErrorMessage(L"Additional networks are not allowed when the primary network mode is 'host' or 'none'."); + } + } + + WSLC_TEST_METHOD(AttachContainerWithIpAddressRejectedTest) + { + const std::string containerName = "test-attach-ip-ctr"; + + WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}); + auto container = launcher.Launch(*m_defaultSession); + + WSLCNetworkAttachment attachment{}; + attachment.NetworkName = "bridge"; + attachment.ContainerIpAddress = "10.0.0.5"; + VERIFY_ARE_EQUAL(E_NOTIMPL, container.Get().AttachToNetwork(&attachment)); + ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported."); + } + WSLC_TEST_METHOD(ContainerNetworkModeHappyPathTest) { // Start container A on the default (bridged) network, then start container B sharing A's @@ -6410,7 +6510,6 @@ class WSLCTests VERIFY_ARE_EQUAL(E_NOTIMPL, hr); ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported."); } - WSLC_TEST_METHOD(ContainerInspect) { // Helper to verify port mappings.