@samitouri / QOSAMI-WSL / commits / 6fd99bb6

Add support for host loopback (#41264)

* Save state * Add test coverage * Format * Rethink the host loopback setup * Apply PR feedback

Blue committed Aug 7, 2026 at 17:28 UTC 6fd99bb6f398794fd73691a2929cf9aef55178ae
14 files changed +166 -7
packages.config
+1 -1
@@ -19,7 +19,7 @@
19 <package id="Microsoft.WSL.bsdtar" version="0.0.2-2" />
20 <package id="Microsoft.WSL.Dependencies.amd64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
21 <package id="Microsoft.WSL.Dependencies.arm64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
22 - <package id="Microsoft.WSL.DeviceHost" version="1.2.56-0" />
22 + <package id="Microsoft.WSL.DeviceHost" version="1.2.57-0" />
23 <package id="Microsoft.WSL.Kernel" version="6.18.35.2-1" targetFramework="native" />
24 <package id="Microsoft.WSL.LinuxSdk" version="1.23.0" targetFramework="native" />
25 <package id="Microsoft.WSL.TestData" version="0.5.0" />
src/windows/common/ConsommeNetworking.cpp
+42 -2
@@ -130,12 +130,18 @@ WslVirtioNetConfig BuildVirtioNetConfig(
130 } // namespace
131
132 ConsommeNetworking::ConsommeNetworking(
133 - GnsChannel&& gnsChannel, ConsommeNetworkingFlags flags, LPCWSTR dnsOptions, std::shared_ptr<GuestDeviceManager> guestDeviceManager, wil::shared_handle userToken) :
133 + GnsChannel&& gnsChannel,
134 + ConsommeNetworkingFlags flags,
135 + LPCWSTR dnsOptions,
136 + LPCSTR hostLoopback,
137 + std::shared_ptr<GuestDeviceManager> guestDeviceManager,
138 + wil::shared_handle userToken) :
139 m_guestDeviceManager(std::move(guestDeviceManager)),
140 m_userToken(std::move(userToken)),
141 m_gnsChannel(std::move(gnsChannel)),
142 m_flags(flags),
138 - m_dnsOptions(dnsOptions)
143 + m_dnsOptions(dnsOptions),
144 + m_hostLoopback(hostLoopback ? hostLoopback : "")
145 {
146 }
147
@@ -158,6 +164,12 @@ void ConsommeNetworking::Initialize()
164 SetupLoopbackDevice();
165 }
166
167 + if (!m_hostLoopback.empty())
168 + {
169 + THROW_HR_IF(E_UNEXPECTED, !WI_IsFlagSet(m_flags, ConsommeNetworkingFlags::LocalhostRelay));
170 + SetupHostLoopback();
171 + }
172 +
173 THROW_IF_WIN32_ERROR(NotifyNetworkConnectivityHintChange(&ConsommeNetworking::OnNetworkConnectivityChange, this, TRUE, &m_networkNotifyHandle));
174 }
175
@@ -380,6 +392,34 @@ void ConsommeNetworking::RefreshGuestConnection()
392 m_networkSettings = std::move(networkSettings);
393 }
394
395 +void ConsommeNetworking::SetupHostLoopback()
396 +{
397 + const auto loopbackDevice = m_guestDeviceManager->GetVirtioNetDevice(c_loopbackDeviceName);
398 +
399 + SOCKADDR_INET loopback{};
400 + loopback.Ipv4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
401 + loopback.Ipv4.sin_family = AF_INET;
402 + auto loopbackIp = ToIpAddress(loopback);
403 +
404 + IpAddress virtualAddress;
405 + THROW_IF_FAILED(loopbackDevice->CreateVirtualAddress(&loopbackIp, &virtualAddress));
406 +
407 + uint32_t addressBytes{};
408 + std::memcpy(&addressBytes, &virtualAddress.bytes[0], sizeof(addressBytes));
409 +
410 + std::string virtualAddressString(INET_ADDRSTRLEN, '\0');
411 + RtlIpv4AddressToStringA(reinterpret_cast<const IN_ADDR*>(&virtualAddress.bytes[0]), virtualAddressString.data());
412 + virtualAddressString.resize(std::strlen(virtualAddressString.data()));
413 +
414 + const auto eth0 = m_guestDeviceManager->GetVirtioNetDevice(c_eth0DeviceName);
415 + THROW_IF_FAILED(eth0->CreateDNSRecord(DnsRecordType_A, m_hostLoopback.c_str(), virtualAddressString.c_str()));
416 +
417 + WSL_LOG(
418 + "SetupHostLoopback",
419 + TraceLoggingValue(m_hostLoopback.c_str(), "DnsName"),
420 + TraceLoggingValue(virtualAddressString.c_str(), "VirtualAddress"));
421 +}
422 +
423 void ConsommeNetworking::SetupLoopbackDevice()
424 {
425 auto loopbackSettings = std::make_shared<NetworkSettings>();
src/windows/common/ConsommeNetworking.h
+9 -1
@@ -23,7 +23,13 @@ DEFINE_ENUM_FLAG_OPERATORS(ConsommeNetworkingFlags);
23 class ConsommeNetworking : public INetworkingEngine
24 {
25 public:
26 - ConsommeNetworking(GnsChannel&& gnsChannel, ConsommeNetworkingFlags flags, LPCWSTR dnsOptions, std::shared_ptr<GuestDeviceManager> guestDeviceManager, wil::shared_handle userToken);
26 + ConsommeNetworking(
27 + GnsChannel&& gnsChannel,
28 + ConsommeNetworkingFlags flags,
29 + LPCWSTR dnsOptions,
30 + LPCSTR hostLoopback,
31 + std::shared_ptr<GuestDeviceManager> guestDeviceManager,
32 + wil::shared_handle userToken);
33
34 ~ConsommeNetworking() override;
35
@@ -50,6 +56,7 @@ private:
56 uint16_t ModifyOpenPorts(
57 _In_ PCWSTR tag, _In_ const SOCKADDR_INET& hostAddress, _In_ uint16_t HostPort, _In_ uint16_t GuestPort, _In_ int protocol, _In_ bool isOpen) const;
58 void RefreshGuestConnection();
59 + void SetupHostLoopback();
60 void SetupLoopbackDevice();
61 void SendDefaultRoute(const std::wstring& gateway, wsl::shared::hns::ModifyRequestType requestType);
62 void SendIpv6Address(const networking::EndpointIpAddress& ipAddress, wsl::shared::hns::ModifyRequestType requestType);
@@ -68,6 +75,7 @@ private:
75 std::shared_ptr<networking::NetworkSettings> m_networkSettings;
76 ConsommeNetworkingFlags m_flags = ConsommeNetworkingFlags::None;
77 LPCWSTR m_dnsOptions = nullptr;
78 + std::string m_hostLoopback;
79 std::optional<GUID> m_localhostAdapterId;
80 std::optional<GUID> m_adapterId;
81 std::optional<WslVirtioNetConfig> m_virtioNetConfig;
src/windows/common/WSLCSessionDefaults.h
+1
@@ -21,6 +21,7 @@ inline constexpr const wchar_t DefaultSessionName[] = L"wslc-cli";
21 inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin";
22 inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions";
23 inline constexpr const wchar_t DefaultStorageVhdName[] = L"storage.vhdx";
24 +inline constexpr const char DefaultHostLoopback[] = "host.wslc.internal";
25 inline constexpr uint32_t DefaultBootTimeoutMs = 30000;
26 inline constexpr const char ContainerdStorageMountPoint[] = "/var/lib/docker";
27
src/windows/common/WSLCUserSettings.cpp
+14
@@ -57,6 +57,10 @@ static constexpr std::string_view s_DefaultSettingsTemplate =
57 " # used without an explicit address (default: 127.0.0.1)\n"
58 " # defaultBindingAddress: default\n"
59 "\n"
60 + " # DNS name that resolves to the host loopback address (default: host.wslc.internal).\n"
61 + " # Set to \"none\" to disable the entry.\n"
62 + " # hostLoopback: default\n"
63 + "\n"
64 " # Seconds an idle session VM stays running before it is torn down (default: 30)\n"
65 " # idleTimeout: default\n"
66 "\n"
@@ -129,6 +133,16 @@ namespace details {
133 return value;
134 }
135
136 + WSLC_VALIDATE_SETTING(SessionHostLoopback)
137 + {
138 + if (value == "none")
139 + {
140 + return std::string{};
141 + }
142 +
143 + return !value.empty() ? std::optional{value} : std::nullopt;
144 + }
145 +
146 WSLC_VALIDATE_SETTING(SessionPortRelay)
147 {
148 if (value == "virtionet")
src/windows/common/WSLCUserSettings.h
+3
@@ -15,6 +15,7 @@ Abstract:
15 #pragma once
16 #include "defs.h"
17 #include "EnumVariantMap.h"
18 +#include "WSLCSessionDefaults.h"
19 #include "wslc.h"
20 #include <cstdint>
21 #include <filesystem>
@@ -41,6 +42,7 @@ enum class Setting : size_t
42 SessionNetworkingMode,
43 SessionHostFileShareMode,
44 SessionDnsTunneling,
45 + SessionHostLoopback,
46 CredentialStore,
47 SessionPortRelay,
48 SessionDefaultBindingAddress,
@@ -98,6 +100,7 @@ namespace details {
100 DEFINE_SETTING_MAPPING(SessionNetworkingMode, std::string, WSLCNetworkingMode, WSLCNetworkingModeConsomme, "session.networkingMode")
101 DEFINE_SETTING_MAPPING(SessionHostFileShareMode, std::string, HostFileShareMode, HostFileShareMode::VirtioFs, "session.hostFileShareMode")
102 DEFINE_SETTING_MAPPING(SessionDnsTunneling, bool, bool, true, "session.dnsTunneling")
103 + DEFINE_SETTING_MAPPING(SessionHostLoopback, std::string, std::string, DefaultHostLoopback, "session.hostLoopback")
104 DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore")
105 DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay")
106 DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress")
src/windows/service/exe/HcsVirtualMachine.cpp
+4 -1
@@ -89,6 +89,7 @@ HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings)
89 m_vmIdString = wsl::shared::string::GuidToString<wchar_t>(m_vmId, wsl::shared::string::GuidToStringFlags::Uppercase);
90 m_featureFlags = Settings->FeatureFlags;
91 m_networkingMode = Settings->NetworkingMode;
92 + m_hostLoopback = Settings->HostLoopback ? Settings->HostLoopback : "";
93 m_bootTimeoutMs = Settings->BootTimeoutMs;
94
95 // Build HCS settings
@@ -504,7 +505,7 @@ try
505 }
506
507 m_networkEngine = std::make_unique<wsl::core::ConsommeNetworking>(
507 - wsl::core::GnsChannel(std::move(gnsSocketHandle)), flags, nullptr, m_guestDeviceManager, m_userToken);
508 + wsl::core::GnsChannel(std::move(gnsSocketHandle)), flags, nullptr, m_hostLoopback.c_str(), m_guestDeviceManager, m_userToken);
509 }
510 else
511 {
@@ -954,6 +955,7 @@ WSLCVirtualMachineFactory::WSLCVirtualMachineFactory(_In_ const WSLCSessionSetti
955 m_bootTimeoutMs = Settings->BootTimeoutMs;
956 m_networkingMode = Settings->NetworkingMode;
957 m_featureFlags = Settings->FeatureFlags;
958 + m_hostLoopback = Settings->HostLoopback ? Settings->HostLoopback : "";
959 m_storageFlags = Settings->StorageFlags;
960 }
961
@@ -968,6 +970,7 @@ WSLCSessionSettings WSLCVirtualMachineFactory::BuildSettings()
970 settings.BootTimeoutMs = m_bootTimeoutMs;
971 settings.NetworkingMode = m_networkingMode;
972 settings.FeatureFlags = m_featureFlags;
973 + settings.HostLoopback = m_hostLoopback.empty() ? nullptr : m_hostLoopback.c_str();
974 settings.StorageFlags = m_storageFlags;
975 settings.RootVhdOverride = m_rootVhdOverride ? m_rootVhdOverride->c_str() : nullptr;
976 settings.RootVhdTypeOverride = m_rootVhdTypeOverride ? m_rootVhdTypeOverride->c_str() : nullptr;
src/windows/service/exe/HcsVirtualMachine.h
+2
@@ -84,6 +84,7 @@ private:
84 wil::shared_handle m_userToken;
85 WSLCFeatureFlags m_featureFlags{};
86 WSLCNetworkingMode m_networkingMode{};
87 + std::string m_hostLoopback;
88
89 bool m_swiotlbConfigured = false;
90
@@ -150,6 +151,7 @@ private:
151 ULONG m_bootTimeoutMs{};
152 WSLCNetworkingMode m_networkingMode{};
153 WSLCFeatureFlags m_featureFlags{};
154 + std::string m_hostLoopback;
155 WSLCSessionStorageFlags m_storageFlags{};
156 };
157
src/windows/service/exe/WSLCSessionManager.cpp
+3 -1
@@ -61,6 +61,7 @@ struct SessionSettings
61 {
62 std::wstring DisplayName;
63 std::wstring StoragePath;
64 + std::string HostLoopback;
65 WSLCSessionSettings Settings{};
66
67 NON_COPYABLE(SessionSettings);
@@ -111,10 +112,11 @@ struct SessionSettings
112
113 private:
114 SessionSettings(std::wstring name, std::wstring path, WSLCSessionStorageFlags storageFlags, const settings::UserSettings& userSettings) :
114 - DisplayName(std::move(name)), StoragePath(std::move(path))
115 + DisplayName(std::move(name)), StoragePath(std::move(path)), HostLoopback(userSettings.Get<settings::Setting::SessionHostLoopback>())
116 {
117 Settings.DisplayName = DisplayName.c_str();
118 Settings.StoragePath = StoragePath.c_str();
119 + Settings.HostLoopback = HostLoopback.empty() ? nullptr : HostLoopback.c_str();
120 auto cpuCount = userSettings.Get<settings::Setting::SessionCpuCount>();
121 Settings.CpuCount = cpuCount > 0 ? cpuCount : wsl::windows::common::wslutil::GetLogicalProcessorCount();
122 auto memoryMb = userSettings.Get<settings::Setting::SessionMemoryMb>();
src/windows/service/exe/WslCoreVm.cpp
+1 -1
@@ -615,7 +615,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
615 dnsTunnelingSocket.reset();
616
617 m_networkingEngine = std::make_unique<wsl::core::ConsommeNetworking>(
618 - std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken);
618 + std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, nullptr, m_guestDeviceManager, m_userToken);
619 }
620 else if (m_vmConfig.NetworkingMode == NetworkingMode::Bridged)
621 {
src/windows/service/inc/wslc.idl
+1
@@ -498,6 +498,7 @@ typedef struct _WSLCSessionSettings {
498 ULONG BootTimeoutMs;
499 WSLCNetworkingMode NetworkingMode;
500 WSLCFeatureFlags FeatureFlags;
501 + [unique] LPCSTR HostLoopback;
502 WSLCHandle DmesgOutput;
503 WSLCSessionStorageFlags StorageFlags;
504 ULONG IdleTimeoutSec;
test/windows/WslcSdkTests.cpp
+20
@@ -816,6 +816,26 @@ class WslcSdkTests
816 }
817 }
818
819 + WSLC_TEST_METHOD(SessionHostLoopbackDisabled)
820 + {
821 + constexpr uint16_t c_hostLoopbackTestPort = 1237;
822 + const auto endpoint = std::format(L"http://127.0.0.1:{}/", c_hostLoopbackTestPort);
823 + UniqueWebServer server(endpoint.c_str(), L"sdk-loopback-enabled");
824 + ExpectHttpResponse(endpoint.c_str(), HTTP_STATUS_OK, true);
825 +
826 + const auto command = std::format(
827 + "if python3 -c \"import http.client,socket;"
828 + "a=socket.getaddrinfo('host.wslc.internal',{},socket.AF_INET,socket.SOCK_STREAM)[0][4];"
829 + "c=http.client.HTTPConnection(*a,timeout=5);"
830 + "c.request('GET','/');"
831 + "c.getresponse().read()\";"
832 + "then echo enabled; else echo disabled; fi",
833 + c_hostLoopbackTestPort);
834 + auto output = RunContainerAndCapture(m_defaultSession, "python:3.12-alpine", {"/bin/sh", "-c", command.c_str()});
835 +
836 + VERIFY_ARE_EQUAL("disabled\n", output.stdoutOutput);
837 + }
838 +
839 WSLC_TEST_METHOD(ContainerPortMapping)
840 {
841 // Negative: null mappings with nonzero count must fail.
test/windows/wslc/WSLCCLISettingsUnitTests.cpp
+29
@@ -122,6 +122,7 @@ class WSLCCLISettingsUnitTests
122 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
123 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
124 VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
125 + VERIFY_ARE_EQUAL(std::string("host.wslc.internal"), s.Get<Setting::SessionHostLoopback>());
126 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
127 }
128
@@ -294,6 +295,7 @@ class WSLCCLISettingsUnitTests
295 " networkingMode: default\n"
296 " hostFileShareMode: default\n"
297 " dnsTunneling: default\n"
298 + " hostLoopback: default\n"
299 "experimental:\n"
300 " portRelay: default\n"
301 "credentialStore: default\n");
@@ -308,6 +310,7 @@ class WSLCCLISettingsUnitTests
310 VERIFY_ARE_EQUAL(static_cast<int>(WSLCNetworkingModeConsomme), static_cast<int>(s.Get<Setting::SessionNetworkingMode>()));
311 VERIFY_ARE_EQUAL(static_cast<int>(HostFileShareMode::VirtioFs), static_cast<int>(s.Get<Setting::SessionHostFileShareMode>()));
312 VERIFY_IS_TRUE(s.Get<Setting::SessionDnsTunneling>());
313 + VERIFY_ARE_EQUAL(std::string("host.wslc.internal"), s.Get<Setting::SessionHostLoopback>());
314 VERIFY_ARE_EQUAL(static_cast<int>(PortRelayType::VirtioNet), static_cast<int>(s.Get<Setting::SessionPortRelay>()));
315 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
316 }
@@ -548,6 +551,32 @@ class WSLCCLISettingsUnitTests
551 VERIFY_ARE_EQUAL(static_cast<int>(PortRelayType::WslRelay), static_cast<int>(s.Get<Setting::SessionPortRelay>()));
552 }
553
554 + // -----------------------------------------------------------------------
555 + // session.hostLoopback
556 + // -----------------------------------------------------------------------
557 +
558 + TEST_METHOD(Validation_HostLoopback_CustomName)
559 + {
560 + auto dir = UniqueTempDir();
561 + WriteFile(dir / L"settings.yaml", "session:\n hostLoopback: host.containers.internal\n");
562 +
563 + UserSettingsTest s{dir};
564 +
565 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
566 + VERIFY_ARE_EQUAL(std::string("host.containers.internal"), s.Get<Setting::SessionHostLoopback>());
567 + }
568 +
569 + TEST_METHOD(Validation_HostLoopback_NoneDisablesEntry)
570 + {
571 + auto dir = UniqueTempDir();
572 + WriteFile(dir / L"settings.yaml", "session:\n hostLoopback: none\n");
573 +
574 + UserSettingsTest s{dir};
575 +
576 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
577 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionHostLoopback>());
578 + }
579 +
580 // -----------------------------------------------------------------------
581 // session.defaultBindingAddress
582 // -----------------------------------------------------------------------
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+36
@@ -572,6 +572,13 @@ class WSLCE2EContainerRunTests
572 VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
573 }
574
575 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_HostLoopback)
576 + {
577 + VerifyHostLoopback("default", "host.wslc.internal", false);
578 + VerifyHostLoopback("default", "host.wslc.internal", true);
579 + VerifyHostLoopback("host.containers.internal", "host.containers.internal", false);
580 + }
581 +
582 // Verifies that 'session.defaultBindingAddress: default' resolves to the built-in
583 // loopback default (127.0.0.1) for a published port specified without an explicit host IP.
584 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_DefaultBindingAddress_Default)
@@ -1336,6 +1343,34 @@ class WSLCE2EContainerRunTests
1343 }
1344
1345 private:
1346 + void VerifyHostLoopback(std::string_view setting, std::string_view dnsName, bool forceTcp)
1347 + {
1348 + EnsureSessionIsTerminated();
1349 + auto terminateSession = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { EnsureSessionIsTerminated(); });
1350 +
1351 + const auto settingsPath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"settings.yaml";
1352 + HostFileChange settings(settingsPath, std::format("session:\n hostLoopback: \"{}\"\n", setting));
1353 +
1354 + const auto endpoint = std::format(L"http://127.0.0.1:{}/", HostLoopbackTestPort);
1355 + UniqueWebServer server(endpoint.c_str(), L"host-loopback-ok");
1356 + ExpectHttpResponse(endpoint.c_str(), HTTP_STATUS_OK, true);
1357 +
1358 + auto command = std::format(
1359 + L"python3 -c \"import http.client,socket;"
1360 + L"a=socket.getaddrinfo('{}',{},socket.AF_INET,socket.SOCK_STREAM)[0][4];"
1361 + L"c=http.client.HTTPConnection(*a,timeout=60);"
1362 + L"c.request('GET','/');"
1363 + L"assert c.getresponse().read()==b'host-loopback-ok';"
1364 + L"assert ('use-vc' in open('/etc/resolv.conf').read()) == {}\"", // Validate that the DNS setting was applied
1365 + std::string(dnsName),
1366 + HostLoopbackTestPort,
1367 + forceTcp ? "True" : "False");
1368 +
1369 + auto result = RunWslc(std::format(
1370 + L"container run {} --rm --name {} {} {}", forceTcp ? "--dns-option=use-vc" : "", WslcContainerName, PythonImage.NameAndTag(), command));
1371 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
1372 + }
1373 +
1374 // Test container name
1375 const std::wstring WslcContainerName = L"wslc-test-container";
1376 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
@@ -1358,6 +1393,7 @@ private:
1393 const uint16_t ContainerTestPort = 8080;
1394 const uint16_t HostTestPort1 = 1234;
1395 const uint16_t HostTestPort2 = 1235;
1396 + const uint16_t HostLoopbackTestPort = 1236;
1397
1398 // Test named volume
1399 const std::wstring WslcVolumeName = L"wslc-test-volume";