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()