Implement a new yaml setting for users to configure the default port binding address (#40885)

* Save state * Add test coverage * Increase test coverage * Format * Make the default ipv4 only * Remove TODO * Catch inet_ntop errors

Blue committed Jun 23, 2026 at 19:48 UTC 51f663ab48be757d31e169658fe0c919972eaf61
10 files changed +272 -69
src/windows/common/WSLCUserSettings.cpp
+17
@@ -47,6 +47,10 @@ static constexpr std::string_view s_DefaultSettingsTemplate =
47 " # Maximum disk image size (e.g. 500GB default: 1TB)\n"
48 " # maxStorageSize: default\n"
49 "\n"
50 + " # Default host address that published ports bind to when 'container run -p' is\n"
51 + " # used without an explicit address (default: 127.0.0.1)\n"
52 + " # defaultBindingAddress: default\n"
53 + "\n"
54 "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n"
55 "# credentialStore: wincred\n";
56
@@ -130,6 +134,19 @@ namespace details {
134 return std::nullopt;
135 }
136
137 + WSLC_VALIDATE_SETTING(SessionDefaultBindingAddress)
138 + {
139 + // The default binding address only applies to IPv4 ports (IPv6 bindings are always
140 + // explicit), so it must parse as a valid IPv4 literal.
141 + in_addr address{};
142 + if (inet_pton(AF_INET, value.c_str(), &address) != 1)
143 + {
144 + return std::nullopt;
145 + }
146 +
147 + return value;
148 + }
149 +
150 WSLC_VALIDATE_SETTING(CredentialStore)
151 {
152 if (value == "wincred")
src/windows/common/WSLCUserSettings.h
+2
@@ -43,6 +43,7 @@ enum class Setting : size_t
43 SessionDnsTunneling,
44 CredentialStore,
45 SessionPortRelay,
46 + SessionDefaultBindingAddress,
47
48 Max
49 };
@@ -97,6 +98,7 @@ namespace details {
98 DEFINE_SETTING_MAPPING(SessionDnsTunneling, bool, bool, true, "session.dnsTunneling")
99 DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore")
100 DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay")
101 + DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress")
102
103 #undef DEFINE_SETTING_MAPPING
104 // clang-format on
src/windows/inc/wslc_schema.h
+1 -2
@@ -22,8 +22,7 @@ using wsl::shared::EmptyObject;
22
23 struct InspectPortBinding
24 {
25 - // WSLC always binds to localhost. Included for Docker API compatibility.
26 - std::string HostIp = "127.0.0.1";
25 + std::string HostIp;
26 std::string HostPort;
27
28 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectPortBinding, HostIp, HostPort);
src/windows/wslc/services/ContainerService.cpp
+9
@@ -23,6 +23,7 @@ Abstract:
23 #include <WSLCProcessLauncher.h>
24 #include <ConsoleState.h>
25 #include <CommandLine.h>
26 +#include <WSLCUserSettings.h>
27 #include <filesystem>
28 #include <unordered_map>
29 #include <wslc.h>
@@ -81,6 +82,8 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
82 }
83 }
84
85 + const auto defaultBindingAddress = settings::User().Get<settings::Setting::SessionDefaultBindingAddress>();
86 +
87 // Set port options if provided
88 for (const auto& port : options.Ports)
89 {
@@ -93,6 +96,12 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
96 {
97 bindAddress = portMapping.HostIP()->IP();
98 }
99 + else if (!defaultBindingAddress.empty())
100 + {
101 + // No explicit host IP: apply the configured default binding address (IPv4 only,
102 + // since IPv6 bindings are always explicit). When unset, AddPort falls back to loopback.
103 + bindAddress = defaultBindingAddress;
104 + }
105
106 auto containerPort = portMapping.ContainerPort();
107 for (uint16_t i = 0; i < containerPort.Count(); ++i)
src/windows/wslcsession/WSLCContainer.cpp
+2 -3
@@ -1306,14 +1306,13 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1306 wslcInspect.Config.User = dockerInspect.Config.User;
1307 wslcInspect.Config.WorkingDir = dockerInspect.Config.WorkingDir;
1308
1309 - // Map WSLC port mappings (Windows host ports only). HostIp is not set here and will use
1310 - // the default value ("127.0.0.1") defined in the InspectPortBinding schema.
1309 + // Map WSLC port mappings (Windows host ports only).
1310 for (const auto& e : m_mappedPorts)
1311 {
1313 - // TODO: ipv6 support.
1312 auto portKey = std::format("{}/{}", e.ContainerPort, e.ProtocolString());
1313
1314 wslc_schema::InspectPortBinding portBinding{};
1315 + portBinding.HostIp = e.VmMapping.BindingAddressString();
1316 portBinding.HostPort = std::to_string(e.VmMapping.HostPort());
1317
1318 wslcInspect.Ports[portKey].push_back(std::move(portBinding));
test/windows/Common.cpp
+49
@@ -3041,6 +3041,55 @@ void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry
3041 }
3042 }
3043
3044 +std::optional<std::string> GetHostAdapterIpv4()
3045 +{
3046 + ULONG bufferSize = 0;
3047 + constexpr ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
3048 + auto result = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &bufferSize);
3049 + if (result != ERROR_BUFFER_OVERFLOW)
3050 + {
3051 + return std::nullopt;
3052 + }
3053 +
3054 + std::vector<BYTE> buffer(bufferSize);
3055 + auto* adapters = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
3056 + result = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &bufferSize);
3057 + if (result != ERROR_SUCCESS)
3058 + {
3059 + return std::nullopt;
3060 + }
3061 +
3062 + for (auto* adapter = adapters; adapter != nullptr; adapter = adapter->Next)
3063 + {
3064 + if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK || adapter->IfType == IF_TYPE_TUNNEL)
3065 + {
3066 + continue;
3067 + }
3068 +
3069 + for (auto* addr = adapter->FirstUnicastAddress; addr != nullptr; addr = addr->Next)
3070 + {
3071 + if (addr->Address.lpSockaddr->sa_family != AF_INET)
3072 + {
3073 + continue;
3074 + }
3075 +
3076 + auto& ipv4 = reinterpret_cast<sockaddr_in*>(addr->Address.lpSockaddr)->sin_addr;
3077 +
3078 + // Skip APIPA (169.254.x.x) addresses.
3079 + if ((ntohl(ipv4.s_addr) & 0xFFFF0000) == 0xA9FE0000)
3080 + {
3081 + continue;
3082 + }
3083 +
3084 + char buf[INET_ADDRSTRLEN];
3085 + VERIFY_IS_NOT_NULL(inet_ntop(AF_INET, &ipv4, buf, sizeof(buf)));
3086 + return std::string(buf);
3087 + }
3088 + }
3089 +
3090 + return std::nullopt;
3091 +}
3092 +
3093 void SetPathAccess(const std::filesystem::path& path, DWORD Permissions, ACCESS_MODE Mode)
3094 {
3095 auto [everyoneSid, everyoneSidBuffer] = wsl::windows::common::security::CreateSid(SECURITY_WORLD_SID_AUTHORITY, SECURITY_WORLD_RID);
test/windows/Common.h
+2
@@ -655,6 +655,8 @@ void LoadTestImage(IWSLCSession& session, std::string_view imageName);
655
656 void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry = false);
657
658 +std::optional<std::string> GetHostAdapterIpv4();
659 +
660 template <typename T>
661 void VerifyAreEqualUnordered(const std::vector<T>& expected, const std::vector<T>& actual, const std::source_location& source = std::source_location::current())
662 {
test/windows/WSLCTests.cpp
+39 -64
@@ -7982,56 +7982,7 @@ class WSLCTests
7982 {
7983 auto [restore, session] = SetupPortMappingsTest(WSLCNetworkingModeConsomme);
7984
7985 - // Helper to resolve the first non-loopback IPv4 address on an active host adapter.
7986 - auto getHostAdapterIpv4 = []() -> std::optional<std::string> {
7987 - ULONG bufferSize = 0;
7988 - constexpr ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
7989 - auto result = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &bufferSize);
7990 - if (result != ERROR_BUFFER_OVERFLOW)
7991 - {
7992 - return std::nullopt;
7993 - }
7994 -
7995 - std::vector<BYTE> buffer(bufferSize);
7996 - auto* adapters = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
7997 - result = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &bufferSize);
7998 - if (result != ERROR_SUCCESS)
7999 - {
8000 - return std::nullopt;
8001 - }
8002 -
8003 - for (auto* adapter = adapters; adapter != nullptr; adapter = adapter->Next)
8004 - {
8005 - if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK || adapter->IfType == IF_TYPE_TUNNEL)
8006 - {
8007 - continue;
8008 - }
8009 -
8010 - for (auto* addr = adapter->FirstUnicastAddress; addr != nullptr; addr = addr->Next)
8011 - {
8012 - if (addr->Address.lpSockaddr->sa_family != AF_INET)
8013 - {
8014 - continue;
8015 - }
8016 -
8017 - auto& ipv4 = reinterpret_cast<sockaddr_in*>(addr->Address.lpSockaddr)->sin_addr;
8018 -
8019 - // Skip APIPA (169.254.x.x) addresses.
8020 - if ((ntohl(ipv4.s_addr) & 0xFFFF0000) == 0xA9FE0000)
8021 - {
8022 - continue;
8023 - }
8024 -
8025 - char buf[INET_ADDRSTRLEN];
8026 - inet_ntop(AF_INET, &ipv4, buf, sizeof(buf));
8027 - return std::string(buf);
8028 - }
8029 - }
8030 -
8031 - return std::nullopt;
8032 - };
8033 -
8034 - auto hostIp = getHostAdapterIpv4();
7985 + auto hostIp = GetHostAdapterIpv4();
7986
7987 struct PortMapping
7988 {
@@ -8064,15 +8015,43 @@ class WSLCTests
8015 return container;
8016 };
8017
8018 + auto validateInspectPortBinding = [&](auto& container,
8019 + uint16_t containerPort,
8020 + int protocol,
8021 + const std::string& expectedHostIp,
8022 + std::optional<uint16_t> expectedHostPort) -> std::string {
8023 + auto inspectData = container.Inspect();
8024 +
8025 + auto portKey = std::format("{}/{}", containerPort, protocol == IPPROTO_UDP ? "udp" : "tcp");
8026 + VERIFY_IS_TRUE(inspectData.Ports.contains(portKey));
8027 +
8028 + auto& bindings = inspectData.Ports[portKey];
8029 + VERIFY_ARE_EQUAL(1u, bindings.size());
8030 + VERIFY_ARE_EQUAL(expectedHostIp, bindings[0].HostIp);
8031 +
8032 + if (expectedHostPort.has_value())
8033 + {
8034 + VERIFY_ARE_EQUAL(std::to_string(expectedHostPort.value()), bindings[0].HostPort);
8035 + }
8036 + else
8037 + {
8038 + VERIFY_IS_TRUE(std::stoi(bindings[0].HostPort) > 0);
8039 + }
8040 +
8041 + return bindings[0].HostPort;
8042 + };
8043 +
8044 // Explicit localhost (127.0.0.1) binding.
8045 {
8046 auto container = createTcpContainer({{1260, 8000, AF_INET, IPPROTO_TCP, "127.0.0.1"}});
8047 + validateInspectPortBinding(container, 8000, IPPROTO_TCP, "127.0.0.1", 1260);
8048 ExpectHttpResponse(L"http://127.0.0.1:1260", 200);
8049 }
8050
8051 // 0.0.0.0 (all interfaces) binding.
8052 {
8053 auto container = createTcpContainer({{1261, 8000, AF_INET, IPPROTO_TCP, "0.0.0.0"}});
8054 + validateInspectPortBinding(container, 8000, IPPROTO_TCP, "0.0.0.0", 1261);
8055
8056 // Verify reachable via loopback.
8057 ExpectHttpResponse(L"http://127.0.0.1:1261", 200);
@@ -8094,6 +8073,7 @@ class WSLCTests
8073 if (hostIp.has_value())
8074 {
8075 auto container = createTcpContainer({{1262, 8000, AF_INET, IPPROTO_TCP, hostIp.value()}});
8076 + validateInspectPortBinding(container, 8000, IPPROTO_TCP, hostIp.value(), 1262);
8077
8078 auto url = std::format(L"http://{}:1262", wsl::shared::string::MultiByteToWide(hostIp.value()));
8079 ExpectHttpResponse(url.c_str(), 200);
@@ -8107,14 +8087,9 @@ class WSLCTests
8087 // Anonymous bind on localhost (ephemeral host port).
8088 {
8089 auto container = createTcpContainer({{WSLC_EPHEMERAL_PORT, 8000, AF_INET, IPPROTO_TCP, "127.0.0.1"}});
8090 + auto hostPort = validateInspectPortBinding(container, 8000, IPPROTO_TCP, "127.0.0.1", std::nullopt);
8091
8111 - auto inspectData = container.Inspect();
8112 - VERIFY_IS_TRUE(inspectData.Ports.contains("8000/tcp"));
8113 -
8114 - auto& bindings = inspectData.Ports["8000/tcp"];
8115 - VERIFY_ARE_EQUAL(1u, bindings.size());
8116 -
8117 - ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", bindings[0].HostPort).c_str(), 200);
8092 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", hostPort).c_str(), 200);
8093 }
8094
8095 // Anonymous bind on host ip (ephemeral host port).
@@ -8122,14 +8097,9 @@ class WSLCTests
8097 if (hostIp.has_value())
8098 {
8099 auto container = createTcpContainer({{WSLC_EPHEMERAL_PORT, 8000, AF_INET, IPPROTO_TCP, hostIp.value()}});
8100 + auto hostPort = validateInspectPortBinding(container, 8000, IPPROTO_TCP, hostIp.value(), std::nullopt);
8101
8126 - auto inspectData = container.Inspect();
8127 - VERIFY_IS_TRUE(inspectData.Ports.contains("8000/tcp"));
8128 -
8129 - auto& bindings = inspectData.Ports["8000/tcp"];
8130 - VERIFY_ARE_EQUAL(1u, bindings.size());
8131 -
8132 - ExpectHttpResponse(std::format(L"http://{}:{}", hostIp.value(), bindings[0].HostPort).c_str(), 200);
8102 + ExpectHttpResponse(std::format(L"http://{}:{}", hostIp.value(), hostPort).c_str(), 200);
8103 }
8104 else
8105 {
@@ -8140,12 +8110,14 @@ class WSLCTests
8110 // IPv6 loopback (::1) binding.
8111 {
8112 auto container = createTcpContainer({{1263, 8000, AF_INET6, IPPROTO_TCP, "::1"}});
8113 + validateInspectPortBinding(container, 8000, IPPROTO_TCP, "::1", 1263);
8114 ExpectHttpResponse(L"http://[::1]:1263", 200);
8115 }
8116
8117 // IPv6 wildcard (::) binding.
8118 {
8119 auto container = createTcpContainer({{1264, 8000, AF_INET6, IPPROTO_TCP, "::"}});
8120 + validateInspectPortBinding(container, 8000, IPPROTO_TCP, "::", 1264);
8121 ExpectHttpResponse(L"http://[::1]:1264", 200);
8122 }
8123
@@ -8173,6 +8145,7 @@ class WSLCTests
8145
8146 auto container = launcher.Launch(*session);
8147 WaitForOutput(container.GetInitProcess().GetStdHandle(1), "UDP listening");
8148 + validateInspectPortBinding(container, 9000, IPPROTO_UDP, "127.0.0.1", 1265);
8149
8150 WSLCE2ETests::SendUdpAndReceive(1265, "hello", "HELLO");
8151 }
@@ -8185,6 +8158,8 @@ class WSLCTests
8158 launcher.AddPort(1265, 8000, AF_INET, IPPROTO_TCP, "1.1.1.1");
8159
8160 auto container = launcher.Create(*session);
8161 + validateInspectPortBinding(container, 8000, IPPROTO_TCP, "1.1.1.1", 1265);
8162 +
8163 VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr), HRESULT_FROM_WIN32(WSAEADDRNOTAVAIL));
8164 ValidateCOMErrorMessage(
8165 L"Failed to map port '1.1.1.1:1265/tcp', The requested address is not valid in its context. ");
test/windows/wslc/WSLCCLISettingsUnitTests.cpp
+83
@@ -546,6 +546,89 @@ class WSLCCLISettingsUnitTests
546 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
547 VERIFY_ARE_EQUAL(static_cast<int>(PortRelayType::WslRelay), static_cast<int>(s.Get<Setting::SessionPortRelay>()));
548 }
549 +
550 + // -----------------------------------------------------------------------
551 + // session.defaultBindingAddress
552 + // -----------------------------------------------------------------------
553 +
554 + // When unset, the default binding address falls back to the empty built-in default.
555 + TEST_METHOD(Validation_DefaultBindingAddress_Absent_UsesEmptyDefault)
556 + {
557 + auto dir = UniqueTempDir();
558 + WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 4\n");
559 +
560 + UserSettingsTest s{dir};
561 +
562 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
563 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
564 + }
565 +
566 + // A valid IPv4 binding address loads without warnings.
567 + TEST_METHOD(Validation_DefaultBindingAddress_ValidValue)
568 + {
569 + auto dir = UniqueTempDir();
570 + WriteFile(
571 + dir / L"settings.yaml",
572 + "session:\n"
573 + " defaultBindingAddress: 0.0.0.0\n");
574 +
575 + UserSettingsTest s{dir};
576 +
577 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
578 + VERIFY_ARE_EQUAL(std::string("0.0.0.0"), s.Get<Setting::SessionDefaultBindingAddress>());
579 + }
580 +
581 + // "default" magic string uses the built-in (empty) default with no warning.
582 + TEST_METHOD(Validation_DefaultBindingAddress_DefaultString)
583 + {
584 + auto dir = UniqueTempDir();
585 + WriteFile(
586 + dir / L"settings.yaml",
587 + "session:\n"
588 + " defaultBindingAddress: default\n");
589 +
590 + UserSettingsTest s{dir};
591 +
592 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
593 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
594 + }
595 +
596 + // An invalid address is rejected: the default is used and an invalid-value warning emitted.
597 + TEST_METHOD(Validation_DefaultBindingAddress_InvalidValue_UsesDefaultAndWarns)
598 + {
599 + auto dir = UniqueTempDir();
600 + WriteFile(
601 + dir / L"settings.yaml",
602 + "session:\n"
603 + " defaultBindingAddress: not-an-ip\n");
604 +
605 + UserSettingsTest s{dir};
606 +
607 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
608 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
609 + VERIFY_ARE_EQUAL(
610 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.defaultBindingAddress", s.SettingsFilePath().wstring(), 2),
611 + s.GetWarnings().front().Message);
612 + VERIFY_ARE_EQUAL(std::wstring(L"session.defaultBindingAddress"), s.GetWarnings().front().SettingPath);
613 + }
614 +
615 + // An IPv6 literal is rejected (the default binding address is IPv4 only) and warns.
616 + TEST_METHOD(Validation_DefaultBindingAddress_IPv6Rejected_UsesDefaultAndWarns)
617 + {
618 + auto dir = UniqueTempDir();
619 + WriteFile(
620 + dir / L"settings.yaml",
621 + "session:\n"
622 + " defaultBindingAddress: \"::1\"\n");
623 +
624 + UserSettingsTest s{dir};
625 +
626 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
627 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
628 + VERIFY_ARE_EQUAL(
629 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.defaultBindingAddress", s.SettingsFilePath().wstring(), 2),
630 + s.GetWarnings().front().Message);
631 + }
632 };
633
634 } // namespace WSLCCLISettingsUnitTests
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+68
@@ -568,6 +568,74 @@ class WSLCE2EContainerRunTests
568 VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
569 }
570
571 + // Verifies that 'session.defaultBindingAddress: default' resolves to the built-in
572 + // loopback default (127.0.0.1) for a published port specified without an explicit host IP.
573 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_DefaultBindingAddress_Default)
574 + {
575 + const auto settingsPath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"settings.yaml";
576 + HostFileChange settings(settingsPath, "session:\n defaultBindingAddress: default\n");
577 +
578 + auto result = RunWslc(std::format(
579 + L"container run -d --name {} -p {}:{} {} {}",
580 + WslcContainerName,
581 + HostTestPort1,
582 + ContainerTestPort,
583 + PythonImage.NameAndTag(),
584 + GetPythonHttpServerScript(ContainerTestPort)));
585 + result.Verify({.Stderr = L"", .ExitCode = 0});
586 +
587 + WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
588 +
589 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
590 +
591 + auto inspectContainer = InspectContainer(WslcContainerName);
592 + auto portKey = std::to_string(ContainerTestPort) + "/tcp";
593 + VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
594 +
595 + auto portBindings = inspectContainer.Ports[portKey];
596 + VERIFY_ARE_EQUAL(1u, portBindings.size());
597 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
598 + VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
599 + }
600 +
601 + // Verifies that a configured 'session.defaultBindingAddress' overrides the loopback
602 + // default for a published port specified without an explicit host IP.
603 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_DefaultBindingAddress_Override)
604 + {
605 + const auto hostIp = GetHostAdapterIpv4();
606 + if (!hostIp.has_value())
607 + {
608 + WEX::Logging::Log::Comment(L"Skipping: no suitable non-loopback host IPv4 address was found.");
609 + return;
610 + }
611 +
612 + const auto settingsPath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"settings.yaml";
613 + HostFileChange settings(settingsPath, std::format("session:\n defaultBindingAddress: {}\n", *hostIp));
614 +
615 + auto result = RunWslc(std::format(
616 + L"container run -d --name {} -p {}:{} {} {}",
617 + WslcContainerName,
618 + HostTestPort1,
619 + ContainerTestPort,
620 + PythonImage.NameAndTag(),
621 + GetPythonHttpServerScript(ContainerTestPort)));
622 + result.Verify({.Stderr = L"", .ExitCode = 0});
623 +
624 + WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
625 +
626 + const auto hostIpWide = std::wstring(hostIp->begin(), hostIp->end());
627 + ExpectHttpResponse(std::format(L"http://{}:{}", hostIpWide, HostTestPort1).c_str(), HTTP_STATUS_OK, true);
628 +
629 + auto inspectContainer = InspectContainer(WslcContainerName);
630 + auto portKey = std::to_string(ContainerTestPort) + "/tcp";
631 + VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
632 +
633 + auto portBindings = inspectContainer.Ports[portKey];
634 + VERIFY_ARE_EQUAL(1u, portBindings.size());
635 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
636 + VERIFY_ARE_EQUAL(*hostIp, portBindings[0].HostIp);
637 + }
638 +
639 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_TCP)
640 {
641 // Start a container with a simple server listening on a port