Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the WSLC Client SDK’s container port-mapping path to support custom host binding addresses (including IPv6) instead of always binding to 127.0.0.1, and relaxes/updates validation around the windowsAddress field.
Changes:
- Convert
windowsAddress(IPv4/IPv6) into a string binding address forWSLCPortMappingduring container creation. - Add input validation for
windowsAddress->ss_familyinWslcSetContainerSettingsPortMappings.
| if (internalPort.windowsAddress != nullptr) | ||
| { | ||
| char addrBuf[INET6_ADDRSTRLEN]{}; | ||
| if (internalPort.windowsAddress->ss_family == AF_INET) | ||
| { | ||
| const auto* addr4 = reinterpret_cast<const sockaddr_in*>(internalPort.windowsAddress); | ||
| THROW_HR_IF_NULL(E_UNEXPECTED, inet_ntop(AF_INET, &addr4->sin_addr, addrBuf, sizeof(addrBuf))); | ||
| convertedPort.Family = AF_INET; | ||
| } | ||
| else | ||
| { | ||
| const auto* addr6 = reinterpret_cast<const sockaddr_in6*>(internalPort.windowsAddress); | ||
| THROW_HR_IF_NULL(E_UNEXPECTED, inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf, sizeof(addrBuf))); | ||
| convertedPort.Family = AF_INET6; | ||
| } | ||
| bindingAddressStrings[i] = addrBuf; | ||
| convertedPort.BindingAddress = bindingAddressStrings[i].c_str(); | ||
| } | ||
| else | ||
| { | ||
| convertedPort.Family = AF_INET; | ||
| convertedPort.BindingAddress = "127.0.0.1"; | ||
| } |
There was a problem hiding this comment.
This change adds support for custom binding addresses (including IPv6) when creating containers, but there are existing TAEF SDK tests for port mapping that currently only cover the default IPv4 localhost TCP case. Please add/extend tests to cover: (1) a mapping with windowsAddress set (IPv4 and IPv6) and (2) UDP mappings, to prevent regressions in address-family handling and string conversion.
| WSLCPortMapping& convertedPort = convertedPorts[i]; | ||
|
|
||
| convertedPort.HostPort = internalPort.windowsPort; | ||
| convertedPort.ContainerPort = internalPort.containerPort; |
There was a problem hiding this comment.
convertedPort.Family and convertedPort.BindingAddress are no longer initialized when internalPort.windowsAddress == nullptr. Previously these were always set (AF_INET + 127.0.0.1), so this change risks leaving uninitialized fields and producing undefined behavior downstream. Set explicit defaults before the if (e.g., loopback + AF_INET) and only override them when windowsAddress is provided.
There was a problem hiding this comment.
Defaulting to:
convertedPort.Family = AF_UNSPEC;
convertedPort.BindingAddress = nullptr;
If not specified by the caller. Runtime will need to interpretive these value appropriately,
| std::unique_ptr<WSLCPortMapping[]> convertedPorts; | ||
| std::vector<std::string> bindingAddressStrings; | ||
| if (internalContainerSettings->ports && internalContainerSettings->portsCount) | ||
| { | ||
| convertedPorts = std::make_unique<WSLCPortMapping[]>(internalContainerSettings->portsCount); | ||
| bindingAddressStrings.resize(internalContainerSettings->portsCount); | ||
| for (uint32_t i = 0; i < internalContainerSettings->portsCount; ++i) | ||
| { |
There was a problem hiding this comment.
convertedPort.BindingAddress is set to bindingAddressStrings[i].c_str(), but bindingAddressStrings is local to this scope. If the downstream API referenced by containerOptions stores the pointer beyond the immediate call (rather than copying the string), this becomes a dangling pointer once the function exits. To make lifetime explicit/safe, prefer storage that is guaranteed to outlive the API usage (e.g., embed a fixed-size buffer in the runtime struct, or keep per-port heap-allocated buffers owned alongside convertedPorts, or confirm/ensure the callee deep-copies the string before returning).
There was a problem hiding this comment.
Valid point, but I don't think warrants a change. The callee copies the data so we are good.
| { | ||
| const auto* addr4 = reinterpret_cast<const sockaddr_in*>(internalPort.windowsAddress); | ||
|
|
||
| THROW_HR_IF_NULL(E_UNEXPECTED, inet_ntop(AF_INET, &addr4->sin_addr, addrBuf, sizeof(addrBuf))); |
There was a problem hiding this comment.
Using E_UNEXPECTED for inet_ntop failures makes troubleshooting difficult and can misclassify user input errors. Consider returning an HRESULT derived from the actual socket error (e.g., using WSAGetLastError() + HRESULT_FROM_WIN32) or E_INVALIDARG when the address is malformed, so callers get a more accurate failure reason.
There was a problem hiding this comment.
We need to provide more exacting errors via ETW.
| // If no binding address is provided, default to wildcard. | ||
| convertedPort.Family = AF_UNSPEC; | ||
| convertedPort.BindingAddress = nullptr; |
There was a problem hiding this comment.
This changes the default binding behavior from the previous explicit loopback-only bind (127.0.0.1) to a wildcard/default bind (AF_UNSPEC + null), which can unintentionally expose forwarded ports on external interfaces. To preserve the prior security posture and avoid surprising behavior changes, default to loopback (e.g., 127.0.0.1 for IPv4 and/or ::1 for IPv6) unless the caller explicitly requests wildcard binding.
| // If no binding address is provided, default to wildcard. | |
| convertedPort.Family = AF_UNSPEC; | |
| convertedPort.BindingAddress = nullptr; | |
| // If no binding address is provided, default to loopback to avoid exposing ports externally. | |
| bindingAddressStrings[i] = "127.0.0.1"; | |
| convertedPort.Family = AF_INET; | |
| convertedPort.BindingAddress = bindingAddressStrings[i].c_str(); |
| { | ||
| const auto* addr4 = reinterpret_cast<const sockaddr_in*>(internalPort.windowsAddress); | ||
|
|
||
| THROW_HR_IF_NULL(E_UNEXPECTED, inet_ntop(AF_INET, &addr4->sin_addr, addrBuf, sizeof(addrBuf))); |
There was a problem hiding this comment.
Using E_UNEXPECTED for inet_ntop failures makes diagnostics and caller handling harder. Prefer returning an error that reflects the underlying cause (e.g., converting WSAGetLastError() to an HRESULT, or using E_INVALIDARG when the input is malformed/unsupported), so failures are actionable.
| { | ||
| const auto* addr6 = reinterpret_cast<const sockaddr_in6*>(internalPort.windowsAddress); | ||
|
|
||
| THROW_HR_IF_NULL(E_UNEXPECTED, inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf, sizeof(addrBuf))); |
There was a problem hiding this comment.
Using E_UNEXPECTED for inet_ntop failures makes diagnostics and caller handling harder. Prefer returning an error that reflects the underlying cause (e.g., converting WSAGetLastError() to an HRESULT, or using E_INVALIDARG when the input is malformed/unsupported), so failures are actionable.
| } | ||
|
|
||
| std::unique_ptr<WSLCPortMapping[]> convertedPorts; | ||
| std::vector<std::string> bindingAddressStrings; |
There was a problem hiding this comment.
convertedPort.BindingAddress is set to a pointer into bindingAddressStrings storage. That pointer becomes invalid as soon as bindingAddressStrings is destroyed (end of scope), and also depends on the called API not retaining the pointer beyond the immediate call. To make the lifetime unambiguous and safe, store the address strings in memory owned by the same structure/lifetime as convertedPorts (e.g., a parallel heap allocation / fixed-size buffers per port) or ensure the downstream API copies the string and document that requirement here.
| if (internalContainerSettings->ports && internalContainerSettings->portsCount) | ||
| { | ||
| convertedPorts = std::make_unique<WSLCPortMapping[]>(internalContainerSettings->portsCount); | ||
| bindingAddressStrings.resize(internalContainerSettings->portsCount); |
There was a problem hiding this comment.
convertedPort.BindingAddress is set to a pointer into bindingAddressStrings storage. That pointer becomes invalid as soon as bindingAddressStrings is destroyed (end of scope), and also depends on the called API not retaining the pointer beyond the immediate call. To make the lifetime unambiguous and safe, store the address strings in memory owned by the same structure/lifetime as convertedPorts (e.g., a parallel heap allocation / fixed-size buffers per port) or ensure the downstream API copies the string and document that requirement here.
| bindingAddressStrings[i] = addrBuf; | ||
| convertedPort.BindingAddress = bindingAddressStrings[i].c_str(); |
There was a problem hiding this comment.
convertedPort.BindingAddress is set to a pointer into bindingAddressStrings storage. That pointer becomes invalid as soon as bindingAddressStrings is destroyed (end of scope), and also depends on the called API not retaining the pointer beyond the immediate call. To make the lifetime unambiguous and safe, store the address strings in memory owned by the same structure/lifetime as convertedPorts (e.g., a parallel heap allocation / fixed-size buffers per port) or ensure the downstream API copies the string and document that requirement here.
There was a problem hiding this comment.
Almost certainly needs a test update to prevent a negative test from failing when it passes in an address. Not sure if we can expect IPv6 everywhere, but at least passing in 127.0.0.1 (or maybe IPv6 loopback works even if no external IPv6 address is available?).
[Edit: Search suggests that IPv6 loopback is always available if device supports IPv6.]
OneBlue
left a comment
There was a problem hiding this comment.
Added a couple comments, I would also recommend:
- Updating the pull request title
- Adding test coverage for the binding address wiring (at least for localhost, since that's the only thing that the service supports today)
You'll probably have to update existing tests as well
|
|
||
| default: | ||
| // Reject unsupported or malformed address families | ||
| THROW_HR(E_INVALIDARG); |
There was a problem hiding this comment.
nit: I recommend THROW_HR_MSG() so we can log the address family we received to make debugging easier
| else | ||
| { | ||
| // If no binding address is provided, default to wildcard. | ||
| convertedPort.Family = AF_UNSPEC; |
There was a problem hiding this comment.
Won't this fail later if we leave AF_UNSPEC here ?
I would recommend either defaulting to localhost, or requiring the caller to pass in the binding address.
The later would be my preference, so that way there's no ambiguity
Summary of the Pull Request
PR Checklist
Detailed Description of the Pull Request / Additional comments
Validation Steps Performed