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