master
cpp 741 lines 28.5 KB
Raw
1 // Copyright (C) Microsoft Corporation. All rights reserved.
2
3 #include <filesystem>
4 #include <optional>
5 #include <iostream>
6 #include <linux/audit.h> /* Definition of AUDIT_* constants */
7 #include <linux/sock_diag.h>
8 #include <linux/inet_diag.h>
9 #include <linux/net.h>
10 #include <sys/syscall.h>
11 #include <sys/xattr.h>
12 #include "common.h" // Needs to be included before sal.h before of __reserved macro
13 #include "NetlinkTransactionError.h"
14 #include "GnsPortTracker.h"
15 #include "lxinitshared.h"
16 #include "seccomp_defs.h"
17
18 // TODO: Include <sys/pidfd.h> and remove this once musl provides it.
19 // <linux/pidfd.h> cannot be used because it conflicts with musl's <fcntl.h>.
20 #ifndef PIDFD_THREAD
21 #define PIDFD_THREAD O_EXCL
22 #endif
23
24 constexpr size_t c_bind_timeout_seconds = 60;
25 constexpr auto c_sock_diag_refresh_delay = std::chrono::milliseconds(500);
26 constexpr auto c_sock_diag_poll_timeout = std::chrono::milliseconds(10);
27 constexpr auto c_bpf_poll_timeout = std::chrono::milliseconds(500);
28
29 GnsPortTracker::GnsPortTracker(
30 std::shared_ptr<wsl::shared::SocketChannel> hvSocketChannel,
31 NetlinkChannel&& netlinkChannel,
32 std::shared_ptr<SecCompDispatcher> seccompDispatcher,
33 LX_MINI_INIT_NETWORKING_MODE networkingMode) :
34 m_hvSocketChannel(std::move(hvSocketChannel)),
35 m_channel(std::move(netlinkChannel)),
36 m_seccompDispatcher(seccompDispatcher),
37 m_networkingMode(networkingMode)
38 {
39 m_networkNamespace = std::filesystem::read_symlink("/proc/self/ns/net").string();
40 GNS_LOG_INFO("GnsPortTracker initialized with networking mode ({})", static_cast<int>(m_networkingMode));
41 }
42
43 void GnsPortTracker::RunPortRefresh()
44 {
45 UtilSetThreadName("GnsPortTracker");
46
47 // The polling of bound sockets is done in a separate thread because
48 // sock_diag sometimes fails with EBUSY when a bind() is in progress.
49 // Doing this in a separate thread allows the main thread not to be delayed
50 // because of transient sock_diag failures
51
52 for (;;)
53 {
54 // Netlink will sometimes return EBUSY. Don't fail for that
55 try
56 {
57 std::promise<void> resume;
58 auto result = PortRefreshResult{ListAllocatedPorts(), time(nullptr), std::bind(&std::promise<void>::set_value, &resume)};
59 m_allocatedPortsRefresh.set_value(result);
60
61 resume.get_future().wait();
62 }
63 catch (const NetlinkTransactionError& e)
64 {
65 if (e.Error().value_or(0) != -EBUSY)
66 {
67 std::cerr << "Failed to refresh allocated ports, " << e.what() << std::endl;
68 }
69 }
70
71 std::this_thread::sleep_for(c_sock_diag_refresh_delay);
72 }
73 }
74
75 int GnsPortTracker::ProcessSecCompNotification(seccomp_notif* notification)
76 {
77 seccomp_notif notificationCopy = *notification;
78 m_request.post(notificationCopy);
79 return m_reply.get();
80 }
81
82 void GnsPortTracker::Run()
83 {
84 // This method consumes seccomp notifications and allows / disallows port allocations
85 // depending on wsl core's response.
86 // After dealing with a notification it also looks at the bound ports list to check
87 // for port deallocation
88
89 std::thread{std::bind(&GnsPortTracker::RunPortRefresh, this)}.detach();
90
91 auto future = std::make_optional(m_allocatedPortsRefresh.get_future());
92 std::optional<PortRefreshResult> refreshResult;
93
94 for (;;)
95 {
96 std::optional<BindCall> bindCall;
97 try
98 {
99 bindCall = ReadNextRequest();
100 }
101 catch (const std::exception& e)
102 {
103 GNS_LOG_ERROR("Failed to read bind request, {}", e.what());
104 }
105
106 if (bindCall.has_value())
107 {
108 int result = 0;
109 if (bindCall->Request.has_value())
110 {
111 PortAllocation& allocationRequest = bindCall->Request.value();
112 result = HandleRequest(allocationRequest);
113 if (result == 0)
114 {
115 TrackPort(allocationRequest);
116 GNS_LOG_INFO(
117 "Tracking bind call: family ({}) port ({}) protocol ({})",
118 allocationRequest.Family,
119 allocationRequest.Port,
120 allocationRequest.Protocol);
121 }
122 }
123
124 try
125 {
126 CompleteRequest(bindCall->CallId, result);
127 }
128 catch (const std::exception& e)
129 {
130 GNS_LOG_ERROR("Failed to complete bind request, {}", e.what());
131 }
132
133 if (bindCall->PortZeroBind.has_value())
134 {
135 try
136 {
137 auto allocation = ResolvePortZeroBind(std::move(bindCall->PortZeroBind.value()));
138 if (allocation.has_value())
139 {
140 const auto portResult = HandleRequest(allocation.value());
141 if (portResult == 0)
142 {
143 TrackPort(std::move(allocation.value()));
144 }
145 else
146 {
147 GNS_LOG_ERROR(
148 "Failed to register resolved port-0 bind: family ({}) port ({}) protocol ({}), error {}",
149 allocation->Family,
150 allocation->Port,
151 allocation->Protocol,
152 portResult);
153 }
154 }
155 }
156 catch (const std::exception& e)
157 {
158 GNS_LOG_ERROR("Failed to resolve port-0 bind, {}", e.what());
159 }
160 }
161 }
162
163 // If bindCall is empty, then the read() timed out. Look for any closed port
164 if (future.has_value() && future->wait_for(c_sock_diag_poll_timeout) == std::future_status::ready)
165 {
166 refreshResult.emplace(future->get());
167 future.reset();
168 m_allocatedPortsRefresh = {};
169
170 // If this loop's iteration had a bind call, it's possible that RefreshAllocatedPort
171 // was called before the bind called was processed. Make sure that the port list
172 // is up to date (If this is called, the next block will schedule another refresh)
173 if (!bindCall.has_value())
174 {
175 OnRefreshAllocatedPorts(refreshResult->Ports, refreshResult->Timestamp);
176 }
177 }
178
179 // Only look at bound ports if there's something to deallocate to avoid wasting cycles
180 if (refreshResult.has_value())
181 {
182 if (!m_allocatedPorts.empty())
183 {
184 future = m_allocatedPortsRefresh.get_future();
185 refreshResult->Resume(); // This will resume the sock_diag thread
186 refreshResult.reset();
187 }
188 }
189 }
190 }
191
192 GnsPortTracker::ActivePorts GnsPortTracker::ListAllocatedPorts()
193 {
194 ActivePorts ports;
195
196 inet_diag_req_v2 message{};
197 message.sdiag_family = AF_INET;
198 message.sdiag_protocol = IPPROTO_TCP;
199 message.idiag_states = ~0;
200
201 auto onMessage = [&](const NetlinkResponse& response) {
202 for (const auto& e : response.Messages<inet_diag_msg>(SOCK_DIAG_BY_FAMILY))
203 {
204 const auto* payload = e.Payload();
205 ports.PortProtocolPairs.emplace(ntohs(payload->id.idiag_sport), static_cast<int>(message.sdiag_protocol));
206
207 in6_addr address = {};
208 if (payload->idiag_family == AF_INET6)
209 {
210 static_assert(sizeof(address.s6_addr32) == 16);
211 static_assert(sizeof(address.s6_addr32) == sizeof(payload->id.idiag_src));
212 memcpy(address.s6_addr32, payload->id.idiag_src, sizeof(address.s6_addr32));
213 }
214 else
215 {
216 address.s6_addr32[0] = payload->id.idiag_src[0];
217 }
218
219 ports.FullAllocations.emplace(
220 ntohs(payload->id.idiag_sport), static_cast<int>(payload->idiag_family), static_cast<int>(message.sdiag_protocol), address);
221 }
222 };
223
224 {
225 auto transaction = m_channel.CreateTransaction(message, SOCK_DIAG_BY_FAMILY, NLM_F_DUMP);
226 transaction.Execute(onMessage);
227 }
228
229 message.sdiag_family = AF_INET6;
230 {
231 auto transaction = m_channel.CreateTransaction(message, SOCK_DIAG_BY_FAMILY, NLM_F_DUMP);
232 transaction.Execute(onMessage);
233 }
234
235 message.sdiag_protocol = IPPROTO_UDP;
236 message.sdiag_family = AF_INET;
237 {
238 auto transaction = m_channel.CreateTransaction(message, SOCK_DIAG_BY_FAMILY, NLM_F_DUMP);
239 transaction.Execute(onMessage);
240 }
241
242 message.sdiag_family = AF_INET6;
243 {
244 auto transaction = m_channel.CreateTransaction(message, SOCK_DIAG_BY_FAMILY, NLM_F_DUMP);
245 transaction.Execute(onMessage);
246 }
247
248 return ports;
249 }
250
251 void GnsPortTracker::OnRefreshAllocatedPorts(const ActivePorts& Ports, time_t Timestamp)
252 {
253 // Because there's no way to get notified when the bind() call actually completes, it' possible
254 // that this method is called before the bind() completion and so the port allocation may not be visible yet.
255 // To avoid deallocating ports that simply haven't been done allocating yet, m_allocatedPorts stores a timeout
256 // that prevents deallocating the port unless:
257 //
258 // - The port has been seen to be allocated (if so, then the timeout is empty)
259 // - The timeout has expired
260
261 for (auto it = m_allocatedPorts.begin(); it != m_allocatedPorts.end();)
262 {
263 bool portStillActive = false;
264 if (IsMirroredMode())
265 {
266 // In mirrored mode, match by port+protocol only. Sockets can use a source port even though an explicit
267 // bind() was not made for that port. As long as there is a socket using the source port, we should not
268 // deallocate it yet.
269 //
270 // For example, a listening socket on port X and a connection socket accepted from that listening socket
271 // will both use port X. Even if the listening socket is closed the connection socket may still be using
272 // the same port.
273 portStillActive = Ports.PortProtocolPairs.contains({it->first.Port, it->first.Protocol});
274 }
275 else
276 {
277 // In other modes, match by full allocation (port+family+protocol+address).
278 portStillActive = Ports.FullAllocations.contains(it->first);
279 }
280
281 if (!portStillActive)
282 {
283 if (!it->second.has_value() || it->second.value() < Timestamp)
284 {
285 auto result = RequestPort(it->first, false);
286 if (result != 0)
287 {
288 std::cerr << "GnsPortTracker: Failed to deallocate port " << it->first << ", " << result << std::endl;
289 }
290
291 GNS_LOG_INFO(
292 "No longer tracking bind call: family ({}) port ({}) protocol ({})",
293 it->first.Family,
294 it->first.Port,
295 it->first.Protocol);
296
297 it = m_allocatedPorts.erase(it);
298 continue;
299 }
300 }
301 else
302 {
303 it->second.reset(); // The port is known to be allocated, remove the timeout
304 }
305
306 ++it;
307 }
308 }
309
310 int GnsPortTracker::RequestPort(const PortAllocation& Port, bool allocate)
311 {
312 LX_GNS_PORT_ALLOCATION_REQUEST request{};
313 request.Header.MessageType = LxGnsMessagePortMappingRequest;
314 request.Header.MessageSize = sizeof(request);
315 request.Af = Port.Family;
316 request.Protocol = Port.Protocol;
317 request.Port = Port.Port;
318 request.Allocate = allocate;
319 static_assert(sizeof(request.Address32) == 16);
320 static_assert(sizeof(request.Address32) == sizeof(Port.Address.s6_addr32));
321 memcpy(request.Address32, Port.Address.s6_addr32, sizeof(request.Address32));
322
323 const auto& response = m_hvSocketChannel->Transaction(request);
324
325 return response.Result;
326 }
327
328 int GnsPortTracker::HandleRequest(const PortAllocation& Port)
329 {
330 // If the port is already allocated, let the call go through and the kernel will
331 // decide if bind() should succeed or not
332 // Note: Returning 0 will also cause the port's timeout to be updated
333
334 if (m_allocatedPorts.contains(Port))
335 {
336 GNS_LOG_INFO("Request for a port that's already reserved (family {}, port {}, protocol {})", Port.Family, Port.Port, Port.Protocol);
337 return 0;
338 }
339
340 // Ask the host for this port otherwise
341 const auto error = RequestPort(Port, true);
342 GNS_LOG_INFO(
343 "Requested the host for port allocation on port (family {}, port {}, protocol {}) - returned {}", Port.Family, Port.Port, Port.Protocol, error);
344 return error;
345 }
346
347 std::optional<GnsPortTracker::BindCall> GnsPortTracker::ReadNextRequest()
348 {
349 // Read the call information
350 auto request_value = m_request.try_get(c_bpf_poll_timeout);
351 if (!request_value.has_value())
352 {
353 return {};
354 }
355
356 auto callInfo = request_value.value();
357
358 // This logic needs to be defensive because the calling process is blocked until
359 // CompleteRequest() is called, so if the call information can't be processed because
360 // the caller has done something wrong (bad pointer, fd, or protocol), just let it go through
361 // and the kernel will fail it
362
363 try
364 {
365 return GetCallInfo(callInfo.id, callInfo.pid, callInfo.data.arch, callInfo.data.nr, gsl::make_span(callInfo.data.args));
366 }
367 catch (const std::exception& e)
368 {
369 GNS_LOG_ERROR("Failed to read bind() call info with ID {} for pid {}, {}", callInfo.id, callInfo.pid, e.what());
370 return {{{}, {}, callInfo.id}};
371 }
372 }
373
374 std::optional<GnsPortTracker::BindCall> GnsPortTracker::GetCallInfo(
375 uint64_t CallId, pid_t Pid, int Arch, int SysCallNumber, const gsl::span<unsigned long long>& Arguments)
376 {
377 auto ParseSocket = [&](int Socket, size_t AddressPtr, size_t AddressLength) -> std::optional<BindCall> {
378 if (AddressLength < sizeof(sockaddr) || AddressLength > sizeof(sockaddr_storage))
379 {
380 return {{{}, {}, CallId}}; // Invalid sockaddr. Let it go through.
381 }
382
383 auto networkNamespace = std::filesystem::read_symlink(std::format("/proc/{}/ns/net", Pid)).string();
384 if (networkNamespace != m_networkNamespace)
385 {
386 GNS_LOG_INFO("Skipping bind() call for pid {} in network namespace {}", Pid, networkNamespace.c_str());
387 return {{{}, {}, CallId}}; // Different network namespace. Let it go through.
388 }
389
390 auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, AddressPtr, AddressLength);
391 if (!processMemory.has_value())
392 {
393 throw RuntimeErrorWithSourceLocation("Failed to read process memory");
394 }
395
396 sockaddr& address = *reinterpret_cast<sockaddr*>(processMemory->data());
397
398 if ((address.sa_family != AF_INET && address.sa_family != AF_INET6) ||
399 (address.sa_family == AF_INET6 && AddressLength < sizeof(sockaddr_in6)))
400 {
401 return {{{}, {}, CallId}}; // This is a non IP call, or invalid sockaddr_in6. Let it go through
402 }
403
404 // Read the port. The port *happens* to be in the same spot in memory for both sockaddr_in
405 // and sockaddr_in6. To avoid a second memory read, we take advantage of this fact to fetch
406 // the port from the currently read memory, regardless of the address family.
407 static_assert(sizeof(sockaddr_in) <= sizeof(sockaddr));
408
409 const auto* inAddr = reinterpret_cast<sockaddr_in*>(&address);
410 in_port_t port = ntohs(inAddr->sin_port);
411 if (port == 0)
412 {
413 // Port 0 means the kernel will assign an ephemeral port. We can't know
414 // the port until after the bind() completes, so duplicate the socket fd
415 // now (while the process is still stopped by seccomp) and defer the
416 // getsockname() lookup to after CompleteRequest() unblocks it.
417 try
418 {
419 const int protocol = GetSocketProtocol(Pid, Socket);
420 auto dupFd = DuplicateSocketFd(Pid, Socket);
421 if (!dupFd)
422 {
423 return {{{}, {}, CallId}};
424 }
425 if (!m_seccompDispatcher->ValidateCookie(CallId))
426 {
427 return {{{}, {}, CallId}};
428 }
429 return {{{}, DeferredPortLookup{Pid, std::move(dupFd), protocol}, CallId}};
430 }
431 catch (const std::exception&)
432 {
433 return {{{}, {}, CallId}}; // Can't determine protocol, just let it through
434 }
435 }
436
437 in6_addr storedAddress = {};
438
439 if (address.sa_family == AF_INET)
440 {
441 storedAddress.s6_addr32[0] = inAddr->sin_addr.s_addr;
442 }
443 else
444 {
445 const auto* inAddr6 = reinterpret_cast<sockaddr_in6*>(&address);
446 memcpy(storedAddress.s6_addr32, inAddr6->sin6_addr.s6_addr32, sizeof(storedAddress.s6_addr32));
447 }
448
449 // It's possible that the calling process lied and passed a sockaddr that
450 // doesn't match the underlying socket family or a bad fd. If that's the case,
451 // then GetSocketProtocol() will throw
452 const int protocol = GetSocketProtocol(Pid, Socket);
453
454 // As GetSocketProtocol interacts with /proc/<pid>, to avoid TOCTOU races we need to
455 // verify that call is still valid (call id is the same thing as cookie)
456 if (!m_seccompDispatcher->ValidateCookie(CallId))
457 {
458 throw RuntimeErrorWithSourceLocation(std::format("Invalid call id {}", CallId));
459 }
460
461 return {{{PortAllocation(port, address.sa_family, protocol, storedAddress)}, {}, CallId}};
462 };
463
464 // listen() can trigger an implicit autobind (assigning an ephemeral port) on a socket that
465 // was never explicitly bind()'d. There's no sockaddr to inspect here (listen() only takes a
466 // socket fd and a backlog), and we can't tell in advance whether the socket is already bound,
467 // so duplicate the fd and check getsockname() immediately: if it's already bound (the common
468 // bind()+listen() case), resolve the port synchronously here. Otherwise defer resolution to
469 // ResolvePortZeroBind(), the same as a bind(port=0) call, since the port isn't assigned until
470 // the listen() syscall (which performs the implicit autobind) actually completes in-kernel.
471 auto ParseListen = [&](int Socket) -> std::optional<BindCall> {
472 try
473 {
474 auto networkNamespace = std::filesystem::read_symlink(std::format("/proc/{}/ns/net", Pid)).string();
475 if (networkNamespace != m_networkNamespace)
476 {
477 GNS_LOG_INFO("Skipping listen() call for pid {} in network namespace {}", Pid, networkNamespace.c_str());
478 return {{{}, {}, CallId}}; // Different network namespace. Let it go through.
479 }
480
481 const int protocol = GetSocketProtocol(Pid, Socket);
482 auto dupFd = DuplicateSocketFd(Pid, Socket);
483 if (!dupFd)
484 {
485 return {{{}, {}, CallId}};
486 }
487 if (!m_seccompDispatcher->ValidateCookie(CallId))
488 {
489 return {{{}, {}, CallId}};
490 }
491
492 // If the socket was already explicitly bind()'d - the common case of a normal
493 // bind() followed by listen() - its port is already known right now, before the
494 // listen() syscall even runs, so resolve it immediately instead of deferring
495 // through the post-completion polling path. That path is reserved for the case
496 // where listen() itself is what triggers the kernel's implicit autobind (i.e. no
497 // prior bind() call), which can only be observed after listen() has completed.
498 sockaddr_storage storage{};
499 socklen_t addressLength = sizeof(storage);
500 if (getsockname(dupFd.get(), reinterpret_cast<sockaddr*>(&storage), &addressLength) == 0)
501 {
502 in_port_t port = 0;
503 in6_addr address = {};
504 if (storage.ss_family == AF_INET)
505 {
506 const auto* sin = reinterpret_cast<const sockaddr_in*>(&storage);
507 port = ntohs(sin->sin_port);
508 address.s6_addr32[0] = sin->sin_addr.s_addr;
509 }
510 else if (storage.ss_family == AF_INET6)
511 {
512 const auto* sin6 = reinterpret_cast<const sockaddr_in6*>(&storage);
513 port = ntohs(sin6->sin6_port);
514 memcpy(address.s6_addr32, sin6->sin6_addr.s6_addr32, sizeof(address.s6_addr32));
515 }
516
517 if (port != 0)
518 {
519 return {{{PortAllocation(port, static_cast<int>(storage.ss_family), protocol, address)}, {}, CallId}};
520 }
521 }
522
523 return {{{}, DeferredPortLookup{Pid, std::move(dupFd), protocol}, CallId}};
524 }
525 catch (const std::exception&)
526 {
527 return {{{}, {}, CallId}}; // Not an IP socket (or can't determine its protocol), just let it through
528 }
529 };
530
531 #ifdef __x86_64__
532 if (Arch & __AUDIT_ARCH_64BIT)
533 {
534 if (SysCallNumber == __NR_listen)
535 {
536 return ParseListen(Arguments[0]);
537 }
538
539 return ParseSocket(Arguments[0], Arguments[1], Arguments[2]);
540 }
541 // Note: 32bit on x86_64 uses the __NR_socketcall with the first argument
542 // set to SYS_BIND/SYS_LISTEN to make bind()/listen() system calls and the
543 // second argument is a pointer to a block of memory containing the original arguments.
544 else
545 {
546 if (Arguments[0] == SYS_LISTEN)
547 {
548 // Grab the first parameter (the socket fd).
549 auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, Arguments[1], sizeof(uint32_t));
550 if (!processMemory.has_value())
551 {
552 throw RuntimeErrorWithSourceLocation("Failed to read process memory");
553 }
554
555 const uint32_t* CopiedArguments = reinterpret_cast<uint32_t*>(processMemory->data());
556 return ParseListen(CopiedArguments[0]);
557 }
558
559 if (Arguments[0] != SYS_BIND)
560 {
561 return {{{}, {}, CallId}}; // Not a bind or listen call, just let the call go through
562 }
563 // Grab the first 3 parameters
564 auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, Arguments[1], sizeof(uint32_t) * 3);
565 if (!processMemory.has_value())
566 {
567 throw RuntimeErrorWithSourceLocation("Failed to read process memory");
568 }
569
570 uint32_t* CopiedArguments = reinterpret_cast<uint32_t*>(processMemory->data());
571 return ParseSocket(CopiedArguments[0], CopiedArguments[1], CopiedArguments[2]);
572 }
573 #else
574 // Both native 64-bit listen() (trapped via __NR_listen, e.g. on aarch64) and 32-bit ARM
575 // compat listen() (trapped via the hardcoded ARMV7_NR_listen syscall number) land here, so
576 // both syscall numbers must be checked.
577 if (SysCallNumber == __NR_listen || SysCallNumber == ARMV7_NR_listen)
578 {
579 return ParseListen(Arguments[0]);
580 }
581
582 return ParseSocket(Arguments[0], Arguments[1], Arguments[2]);
583 #endif
584 }
585
586 void GnsPortTracker::CompleteRequest(uint64_t id, int result)
587 {
588 m_reply.post(result);
589 }
590
591 int GnsPortTracker::GetSocketProtocol(int pid, int fd)
592 {
593 const auto path = std::format("/proc/{}/fd/{}", pid, fd);
594
595 // Because there's a race between the time where the buffer size is determined
596 // and the actual getxattr() call, retry until the buffer size is big enough
597 std::string protocol;
598 int result = -1;
599 do
600 {
601 int bufferSize = Syscall(getxattr, path.c_str(), "system.sockprotoname", nullptr, 0);
602 protocol.resize(std::max(0, bufferSize - 1));
603
604 result = getxattr(path.c_str(), "system.sockprotoname", protocol.data(), bufferSize);
605 } while (result < 0 && errno == ERANGE);
606
607 if (result < 0)
608 {
609 throw RuntimeErrorWithSourceLocation(std::format("Failed to read protocol for socket: {}, {}", path, errno));
610 }
611
612 // In case the size of the attribute shrunk between the two getxattr calls
613 protocol.resize(std::max(0, result - 1));
614
615 if (protocol == "TCP" || protocol == "TCPv6")
616 {
617 return IPPROTO_TCP;
618 }
619 else if (protocol == "UDP" || protocol == "UDPv6")
620 {
621 return IPPROTO_UDP;
622 }
623
624 throw RuntimeErrorWithSourceLocation(std::format("Unexpected IP socket protocol: {}", protocol));
625 }
626
627 wil::unique_fd GnsPortTracker::DuplicateSocketFd(pid_t Pid, int SocketFd)
628 {
629 // Duplicate the socket fd from the target process into our address space.
630 // We cannot use open("/proc/pid/fd/N") for sockets because the symlink target
631 // (socket:[inode]) is not a valid filesystem path. Use pidfd_getfd() instead.
632 // PIDFD_THREAD requires kernel >= 6.9. Fallback to process only if not supported.
633 int pidFdResult = static_cast<int>(syscall(SYS_pidfd_open, Pid, PIDFD_THREAD));
634 if (pidFdResult < 0 && errno == EINVAL)
635 {
636 pidFdResult = static_cast<int>(syscall(SYS_pidfd_open, Pid, 0u));
637 }
638
639 wil::unique_fd pidFd(pidFdResult);
640 if (!pidFd)
641 {
642 GNS_LOG_INFO("Port-0 bind: pidfd_open failed for pid {} (errno {})", Pid, errno);
643 return {};
644 }
645
646 wil::unique_fd dupFd(static_cast<int>(syscall(SYS_pidfd_getfd, pidFd.get(), SocketFd, 0u)));
647 if (!dupFd)
648 {
649 GNS_LOG_INFO("Port-0 bind: pidfd_getfd failed for pid {} fd {} (errno {})", Pid, SocketFd, errno);
650 }
651
652 return dupFd;
653 }
654
655 void GnsPortTracker::TrackPort(PortAllocation allocation)
656 try
657 {
658 // Use insert_or_assign so the deallocation timeout is refreshed if the same
659 // port key is already present (emplace would silently keep the old entry).
660 m_allocatedPorts.insert_or_assign(std::move(allocation), std::make_optional(time(nullptr) + c_bind_timeout_seconds));
661 }
662 catch (const std::exception& e)
663 {
664 GNS_LOG_ERROR("Failed to track port allocation, {}", e.what());
665 }
666
667 std::optional<GnsPortTracker::PortAllocation> GnsPortTracker::ResolvePortZeroBind(DeferredPortLookup lookup)
668 {
669 // This resolves the port for both an explicit bind(port=0) and an implicit
670 // autobind triggered by listen(), since neither can be known until after the
671 // syscall has actually completed in-kernel.
672 //
673 // The socket fd was already duplicated (via pidfd_getfd) while the target process
674 // was stopped by seccomp, so it remains valid even if the process has closed or
675 // reused the original fd number.
676
677 // The bind() syscall has been completed (CompleteRequest() already unblocked the
678 // caller). Poll getsockname() briefly until the kernel assigns a port.
679 constexpr int maxRetries = 25;
680 constexpr auto retryDelay = std::chrono::milliseconds(10);
681
682 in_port_t port = 0;
683 in6_addr address = {};
684 int resolvedFamily = 0;
685
686 for (int attempt = 0; attempt < maxRetries; ++attempt)
687 {
688 if (attempt > 0)
689 {
690 std::this_thread::sleep_for(retryDelay);
691 }
692
693 sockaddr_storage storage{};
694 socklen_t addrLen = sizeof(storage);
695 if (getsockname(lookup.DuplicatedSocketFd.get(), reinterpret_cast<sockaddr*>(&storage), &addrLen) != 0)
696 {
697 GNS_LOG_ERROR("Port-0 bind: getsockname failed for pid {} (errno {})", lookup.Pid, errno);
698 return {};
699 }
700
701 resolvedFamily = static_cast<int>(storage.ss_family);
702
703 if (storage.ss_family == AF_INET)
704 {
705 const auto* sin = reinterpret_cast<const sockaddr_in*>(&storage);
706 port = ntohs(sin->sin_port);
707 address.s6_addr32[0] = sin->sin_addr.s_addr;
708 }
709 else if (storage.ss_family == AF_INET6)
710 {
711 const auto* sin6 = reinterpret_cast<const sockaddr_in6*>(&storage);
712 port = ntohs(sin6->sin6_port);
713 memcpy(address.s6_addr32, sin6->sin6_addr.s6_addr32, sizeof(address.s6_addr32));
714 }
715 else
716 {
717 GNS_LOG_ERROR("Port-0 bind: unexpected address family ({}) for pid {}", resolvedFamily, lookup.Pid);
718 return {};
719 }
720
721 if (port != 0)
722 {
723 break;
724 }
725 }
726
727 if (port == 0)
728 {
729 GNS_LOG_ERROR("Port-0 bind: kernel did not assign a port for pid {} after retries", lookup.Pid);
730 return {};
731 }
732
733 GNS_LOG_INFO(
734 "Port-0 bind resolved: family ({}) port ({}) protocol ({}) for pid {}", resolvedFamily, port, lookup.Protocol, lookup.Pid);
735 return PortAllocation(port, resolvedFamily, lookup.Protocol, address);
736 }
737
738 std::ostream& operator<<(std::ostream& out, const GnsPortTracker::PortAllocation& entry)
739 {
740 return out << "Port=" << entry.Port << ", Family=" << entry.Family << ", Protocol=" << entry.Protocol;
741 }