Clean up localhost relay implementation to not rely on procfs parsing. (#13836)

* Clean up localhost relay implementation to not rely on procfs parsing. * pr feedback --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

Ben Hillis committed Dec 12, 2025 at 14:18 UTC f1e20b21c9c81442e440d1be55de8c807ab3dd19
2 files changed +65 -110
src/linux/init/localhost.cpp
+55 -103
@@ -12,6 +12,8 @@
12 #include <netinet/ip.h>
13 #include <sys/syscall.h>
14 #include <linux/unistd.h>
15 +#include <linux/sock_diag.h>
16 +#include <linux/inet_diag.h>
17 #include <lxwil.h>
18 #include <linux/if_tun.h>
19
@@ -21,6 +23,8 @@
23 #include "SecCompDispatcher.h"
24 #include "seccomp_defs.h"
25 #include "CommandLine.h"
26 +#include "NetlinkChannel.h"
27 +#include "NetlinkTransactionError.h"
28
29 #define TCP_LISTEN 10
30
@@ -145,80 +149,60 @@ void ListenThread(sockaddr_vm hvSocketAddress, int listenSocket)
149 return;
150 }
151
148 -std::vector<sockaddr_storage> ParseTcpFile(int family, FILE* file)
152 +std::vector<sockaddr_storage> QueryListeningSockets(NetlinkChannel& channel)
153 {
150 - char* line = nullptr;
151 - auto freeLine = wil::scope_exit([&line]() { free(line); });
152 -
153 - // Skip the first line which contains a header.
154 - size_t lineLength = 0;
155 - auto bytesRead = getline(&line, &lineLength, file);
156 - THROW_LAST_ERROR_IF(bytesRead < 0);
157 -
158 - // Each line contains information about TCP sockets on the system, the fields
159 - // we are interested are for sockets that are or have been listening:
160 - // 1: Socket address and port number
161 - // 3: Socket status
154 std::vector<sockaddr_storage> sockets{};
163 - while ((bytesRead = getline(&line, &lineLength, file)) != -1)
155 + try
156 {
165 - sockaddr_storage sock{};
166 - int index = 0;
167 - int status = 0;
168 - for (char *sp, *field = strtok_r(line, " \n", &sp); field != nullptr; field = strtok_r(NULL, " \n", &sp))
169 - {
170 - if (index == 1)
157 + inet_diag_req_v2 message{};
158 + message.sdiag_protocol = IPPROTO_TCP;
159 + message.idiag_states = (1 << TCP_LISTEN);
160 +
161 + auto onMessage = [&](const NetlinkResponse& response) {
162 + for (const auto& e : response.Messages<inet_diag_msg>(SOCK_DIAG_BY_FAMILY))
163 {
172 - int port;
173 - const char* portString = strchr(field, ':');
174 - if (portString == nullptr)
175 - {
176 - break;
177 - }
178 - portString += 1;
179 - port = static_cast<int>(strtol(portString, nullptr, 16));
180 - if (port == 0)
181 - {
182 - break;
183 - }
164 + const auto* payload = e.Payload();
165 + sockaddr_storage sock{};
166
185 - if (family == AF_INET)
167 + if (payload->idiag_family == AF_INET)
168 {
187 - sockaddr_in ipv4Sock{};
188 - ipv4Sock.sin_family = family;
189 - ipv4Sock.sin_addr.s_addr = strtol(field, nullptr, 16);
190 - ipv4Sock.sin_port = port;
191 - memcpy(&sock, &ipv4Sock, sizeof(ipv4Sock));
169 + auto* ipv4 = reinterpret_cast<sockaddr_in*>(&sock);
170 + ipv4->sin_family = AF_INET;
171 + ipv4->sin_addr.s_addr = payload->id.idiag_src[0];
172 + ipv4->sin_port = payload->id.idiag_sport;
173 }
193 - else if (family == AF_INET6)
174 + else if (payload->idiag_family == AF_INET6)
175 {
195 - sockaddr_in6 ipv6Sock{};
196 - ipv6Sock.sin6_family = family;
197 - ipv6Sock.sin6_port = port;
198 - for (int part = 0; part < 4; ++part)
199 - {
200 - char next[5];
201 - next[4] = 0;
202 - memcpy(next, field + part * 4, 4);
203 - ipv6Sock.sin6_addr.__in6_union.__s6_addr32[part] = strtol(next, nullptr, 16);
204 - }
205 - memcpy(&sock, &ipv6Sock, sizeof(ipv6Sock));
176 + auto* ipv6 = reinterpret_cast<sockaddr_in6*>(&sock);
177 + ipv6->sin6_family = AF_INET6;
178 + static_assert(sizeof(ipv6->sin6_addr.s6_addr32) == sizeof(payload->id.idiag_src));
179 + memcpy(ipv6->sin6_addr.s6_addr32, payload->id.idiag_src, sizeof(ipv6->sin6_addr.s6_addr32));
180 + ipv6->sin6_port = payload->id.idiag_sport;
181 }
182 +
183 + sockets.emplace_back(sock);
184 }
208 - else if (index == 3)
209 - {
210 - status = static_cast<int>(strtol(field, nullptr, 16));
211 - break;
212 - }
185 + };
186
214 - index += 1;
187 + // Query IPv4 listening sockets.
188 + {
189 + message.sdiag_family = AF_INET;
190 + auto transaction = channel.CreateTransaction(message, SOCK_DIAG_BY_FAMILY, NLM_F_DUMP);
191 + transaction.Execute(onMessage);
192 }
193
217 - if ((status == TCP_LISTEN) && (sock.ss_family != 0))
194 + // Query IPv6 listening sockets.
195 {
219 - sockets.emplace_back(sock);
196 + message.sdiag_family = AF_INET6;
197 + auto transaction = channel.CreateTransaction(message, SOCK_DIAG_BY_FAMILY, NLM_F_DUMP);
198 + transaction.Execute(onMessage);
199 }
200 }
201 + catch (const NetlinkTransactionError& e)
202 + {
203 + // Log but don't fail - network state might be temporarily unavailable
204 + LOG_ERROR("Failed to query listening sockets via sock_diag: {}", e.what());
205 + }
206
207 return sockets;
208 }
@@ -246,12 +230,12 @@ LX_GNS_PORT_LISTENER_RELAY SockToRelayMessage(const sockaddr_storage& sock)
230 {
231 auto ipv4 = reinterpret_cast<const sockaddr_in*>(&sock);
232 message.Address[0] = ipv4->sin_addr.s_addr;
249 - message.Port = ipv4->sin_port;
233 + message.Port = ntohs(ipv4->sin_port);
234 }
235 else if (sock.ss_family == AF_INET6)
236 {
237 auto ipv6 = reinterpret_cast<const sockaddr_in6*>(&sock);
254 - message.Port = ipv6->sin6_port;
238 + message.Port = ntohs(ipv6->sin6_port);
239 memcpy(message.Address, ipv6->sin6_addr.__in6_union.__s6_addr, sizeof(message.Address));
240 }
241 return message;
@@ -296,53 +280,23 @@ bool IsSameSockAddr(const sockaddr_storage& left, const sockaddr_storage& right)
280 {
281 auto leftIpv6 = reinterpret_cast<const sockaddr_in6*>(&left);
282 auto rightIpv6 = reinterpret_cast<const sockaddr_in6*>(&right);
299 - if (leftIpv6->sin6_port != rightIpv6->sin6_port)
300 - {
301 - return false;
302 - }
303 - for (int part = 0; part < 4; ++part)
304 - {
305 - if (leftIpv6->sin6_addr.__in6_union.__s6_addr32[part] != rightIpv6->sin6_addr.__in6_union.__s6_addr32[part])
306 - {
307 - return false;
308 - }
309 - }
310 - return true;
311 - }
312 - else
313 - {
314 - FATAL_ERROR("Unrecognized socket family {}", left.ss_family);
315 - return false;
283 + return (leftIpv6->sin6_port == rightIpv6->sin6_port && memcmp(&leftIpv6->sin6_addr, &rightIpv6->sin6_addr, sizeof(in6_addr)) == 0);
284 }
285 +
286 + FATAL_ERROR("Unrecognized socket family {}", left.ss_family);
287 + return false;
288 }
289
319 -// Start looking for ports bound to localhost or wildcard.
320 -int ScanProcNetTCP(wsl::shared::SocketChannel& channel)
290 +// Monitor listening TCP sockets using sock_diag netlink interface.
291 +int MonitorListeningSockets(wsl::shared::SocketChannel& channel)
292 {
322 - // Periodically scan procfs for listening TCP sockets.
293 + NetlinkChannel netlinkChannel(SOCK_RAW, NETLINK_SOCK_DIAG);
294 std::vector<sockaddr_storage> relays{};
295 int result = 0;
296 +
297 for (;;)
298 {
327 - std::vector<sockaddr_storage> sockets;
328 - wil::unique_file tcp4File{fopen("/proc/net/tcp", "r")};
329 - if (tcp4File)
330 - {
331 - sockets = ParseTcpFile(AF_INET, tcp4File.get());
332 - }
333 -
334 - wil::unique_file tcp6File{fopen("/proc/net/tcp6", "r")};
335 - if (tcp6File)
336 - {
337 - auto ipv6Sockets = ParseTcpFile(AF_INET6, tcp6File.get());
338 - sockets.insert(sockets.end(), ipv6Sockets.begin(), ipv6Sockets.end());
339 - }
340 -
341 - if (!tcp4File && !tcp6File)
342 - {
343 - LOG_ERROR("Failed to open /proc/net/tcp and /proc/net/tcp6, closing port relay");
344 - return 1;
345 - }
299 + auto sockets = QueryListeningSockets(netlinkChannel);
300
301 // Stop any relays that no longer match listening ports.
302 std::erase_if(relays, [&](const auto& entry) {
@@ -386,9 +340,7 @@ int ScanProcNetTCP(wsl::shared::SocketChannel& channel)
340 }
341
342 // Sleep before scanning again.
389 - //
390 - // TODO: Investigate using EBPF notifications instead of a sleep.
391 - sleep(1);
343 + std::this_thread::sleep_for(std::chrono::seconds(1));
344 }
345
346 return result;
@@ -432,7 +384,7 @@ try
384
385 if (ScanForPorts)
386 {
435 - return ScanProcNetTCP(channel);
387 + return MonitorListeningSockets(channel);
388 }
389
390 return 0;
test/windows/NetworkTests.cpp
+10 -7
@@ -2095,20 +2095,23 @@ class NetworkTests
2095 VerifyNotBoundLoopback(port, false);
2096 }
2097
2098 - static void ValidateLocalhostRelayTraffic(bool ipv6)
2098 + static void ValidateLocalhostRelayTraffic(ADDRESS_FAMILY addressFamily)
2099 {
2100 + THROW_HR_IF(E_INVALIDARG, addressFamily != AF_INET && addressFamily != AF_INET6);
2101 +
2102 // Bind a port in the guest.
2101 - auto [guestProcess, read] = BindGuestPort(ipv6 ? L"TCP6-LISTEN:1234,bind=::1" : L"TCP4-LISTEN:1234,bind=127.0.0.1", true);
2103 + auto [guestProcess, read] =
2104 + BindGuestPort(addressFamily == AF_INET6 ? L"TCP6-LISTEN:1234,bind=::1" : L"TCP4-LISTEN:1234,bind=127.0.0.1", true);
2105
2106 // Connect to the port via the localhost relay
2107 wil::unique_socket hostSocket;
2108 SOCKADDR_INET addr{};
2106 - addr.si_family = ipv6 ? AF_INET6 : AF_INET;
2109 + addr.si_family = addressFamily;
2110 INETADDR_SETLOOPBACK((PSOCKADDR)&addr);
2111 SS_PORT(&addr) = htons(1234);
2112
2113 auto pred = [&]() {
2111 - hostSocket.reset(socket(ipv6 ? AF_INET6 : AF_INET, SOCK_STREAM, IPPROTO_TCP));
2114 + hostSocket.reset(socket(addressFamily, SOCK_STREAM, IPPROTO_TCP));
2115 THROW_HR_IF(E_ABORT, !hostSocket);
2116 THROW_HR_IF(E_FAIL, connect(hostSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
2117 };
@@ -2149,8 +2152,8 @@ class NetworkTests
2152 WSL2_TEST_ONLY();
2153 WslKeepAlive keepAlive;
2154
2152 - ValidateLocalhostRelayTraffic(false);
2153 - ValidateLocalhostRelayTraffic(true);
2155 + ValidateLocalhostRelayTraffic(AF_INET);
2156 + ValidateLocalhostRelayTraffic(AF_INET6);
2157 }
2158
2159 TEST_METHOD(NatLocalhostRelayNoIpv6)
@@ -2161,7 +2164,7 @@ class NetworkTests
2164 WslKeepAlive keepAlive;
2165
2166 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -f /proc/net/tcp6"), 1L);
2164 - ValidateLocalhostRelayTraffic(false);
2167 + ValidateLocalhostRelayTraffic(AF_INET);
2168 }
2169
2170 static void TestNonRootNamespaceEphemeralBind()