net: handle multi-part netlink responses

Handle multi-part netlink responses to prevent truncated results from
large routing tables.

Previously, we only made a single recv call, which led to incomplete
results when the kernel split the message into multiple responses (which
happens frequently with NLM_F_DUMP).

Also guard against a potential hanging issue where the code would
indefinitely wait for NLMSG_DONE for non-multi-part responses by
detecting the NLM_F_MULTI flag and only continue waiting when necessary.
This commit is contained in:
willcl-ark
2025-04-01 14:15:50 +01:00
committed by will
parent 42e99ad773
commit 88db09bafe

View File

@@ -36,6 +36,9 @@ namespace {
// will fail, so we skip that. // will fail, so we skip that.
#if defined(__linux__) || (defined(__FreeBSD__) && __FreeBSD_version >= 1400000) #if defined(__linux__) || (defined(__FreeBSD__) && __FreeBSD_version >= 1400000)
// Good for responses containing ~ 10,000-15,000 routes.
static constexpr ssize_t NETLINK_MAX_RESPONSE_SIZE{1'048'576};
std::optional<CNetAddr> QueryDefaultGatewayImpl(sa_family_t family) std::optional<CNetAddr> QueryDefaultGatewayImpl(sa_family_t family)
{ {
// Create a netlink socket. // Create a netlink socket.
@@ -84,49 +87,68 @@ std::optional<CNetAddr> QueryDefaultGatewayImpl(sa_family_t family)
// Receive response. // Receive response.
char response[4096]; char response[4096];
int64_t recv_result; ssize_t total_bytes_read{0};
do { bool done{false};
recv_result = sock->Recv(response, sizeof(response), 0); while (!done) {
} while (recv_result < 0 && (errno == EINTR || errno == EAGAIN)); int64_t recv_result;
if (recv_result < 0) { do {
LogPrintLevel(BCLog::NET, BCLog::Level::Error, "recv() from netlink socket: %s\n", NetworkErrorString(errno)); recv_result = sock->Recv(response, sizeof(response), 0);
return std::nullopt; } while (recv_result < 0 && (errno == EINTR || errno == EAGAIN));
} if (recv_result < 0) {
LogPrintLevel(BCLog::NET, BCLog::Level::Error, "recv() from netlink socket: %s\n", NetworkErrorString(errno));
for (nlmsghdr* hdr = (nlmsghdr*)response; NLMSG_OK(hdr, recv_result); hdr = NLMSG_NEXT(hdr, recv_result)) { return std::nullopt;
rtmsg* r = (rtmsg*)NLMSG_DATA(hdr);
int remaining_len = RTM_PAYLOAD(hdr);
if (hdr->nlmsg_type != RTM_NEWROUTE) {
continue; // Skip non-route messages
} }
// Only consider default routes (destination prefix length of 0). total_bytes_read += recv_result;
if (r->rtm_dst_len != 0) { if (total_bytes_read > NETLINK_MAX_RESPONSE_SIZE) {
continue; LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "Netlink response exceeded size limit (%zu bytes, family=%d)\n", NETLINK_MAX_RESPONSE_SIZE, family);
return std::nullopt;
} }
// Iterate over the attributes. for (nlmsghdr* hdr = (nlmsghdr*)response; NLMSG_OK(hdr, recv_result); hdr = NLMSG_NEXT(hdr, recv_result)) {
rtattr *rta_gateway = nullptr; if (!(hdr->nlmsg_flags & NLM_F_MULTI)) {
int scope_id = 0; done = true;
for (rtattr* attr = RTM_RTA(r); RTA_OK(attr, remaining_len); attr = RTA_NEXT(attr, remaining_len)) {
if (attr->rta_type == RTA_GATEWAY) {
rta_gateway = attr;
} else if (attr->rta_type == RTA_OIF && sizeof(int) == RTA_PAYLOAD(attr)) {
std::memcpy(&scope_id, RTA_DATA(attr), sizeof(scope_id));
} }
}
// Found gateway? if (hdr->nlmsg_type == NLMSG_DONE) {
if (rta_gateway != nullptr) { done = true;
if (family == AF_INET && sizeof(in_addr) == RTA_PAYLOAD(rta_gateway)) { break;
in_addr gw; }
std::memcpy(&gw, RTA_DATA(rta_gateway), sizeof(gw));
return CNetAddr(gw); rtmsg* r = (rtmsg*)NLMSG_DATA(hdr);
} else if (family == AF_INET6 && sizeof(in6_addr) == RTA_PAYLOAD(rta_gateway)) { int remaining_len = RTM_PAYLOAD(hdr);
in6_addr gw;
std::memcpy(&gw, RTA_DATA(rta_gateway), sizeof(gw)); if (hdr->nlmsg_type != RTM_NEWROUTE) {
return CNetAddr(gw, scope_id); continue; // Skip non-route messages
}
// Only consider default routes (destination prefix length of 0).
if (r->rtm_dst_len != 0) {
continue;
}
// Iterate over the attributes.
rtattr* rta_gateway = nullptr;
int scope_id = 0;
for (rtattr* attr = RTM_RTA(r); RTA_OK(attr, remaining_len); attr = RTA_NEXT(attr, remaining_len)) {
if (attr->rta_type == RTA_GATEWAY) {
rta_gateway = attr;
} else if (attr->rta_type == RTA_OIF && sizeof(int) == RTA_PAYLOAD(attr)) {
std::memcpy(&scope_id, RTA_DATA(attr), sizeof(scope_id));
}
}
// Found gateway?
if (rta_gateway != nullptr) {
if (family == AF_INET && sizeof(in_addr) == RTA_PAYLOAD(rta_gateway)) {
in_addr gw;
std::memcpy(&gw, RTA_DATA(rta_gateway), sizeof(gw));
return CNetAddr(gw);
} else if (family == AF_INET6 && sizeof(in6_addr) == RTA_PAYLOAD(rta_gateway)) {
in6_addr gw;
std::memcpy(&gw, RTA_DATA(rta_gateway), sizeof(gw));
return CNetAddr(gw, scope_id);
}
} }
} }
} }