| /* | |
| * Copyright (C) 2019 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| */ | |
| #define LOG_TAG "resolv" | |
| #include "DnsStats.h" | |
| #include <android-base/format.h> | |
| #include <android-base/logging.h> | |
| namespace android::net { | |
| using netdutils::DumpWriter; | |
| using netdutils::IPAddress; | |
| using netdutils::IPSockAddr; | |
| using netdutils::ScopedIndent; | |
| using std::chrono::duration_cast; | |
| using std::chrono::microseconds; | |
| using std::chrono::milliseconds; | |
| using std::chrono::seconds; | |
| namespace { | |
| static constexpr IPAddress INVALID_IPADDRESS = IPAddress(); | |
| std::string rcodeToName(int rcode) { | |
| // clang-format off | |
| switch (rcode) { | |
| case NS_R_NO_ERROR: return "NOERROR"; | |
| case NS_R_FORMERR: return "FORMERR"; | |
| case NS_R_SERVFAIL: return "SERVFAIL"; | |
| case NS_R_NXDOMAIN: return "NXDOMAIN"; | |
| case NS_R_NOTIMPL: return "NOTIMP"; | |
| case NS_R_REFUSED: return "REFUSED"; | |
| case NS_R_YXDOMAIN: return "YXDOMAIN"; | |
| case NS_R_YXRRSET: return "YXRRSET"; | |
| case NS_R_NXRRSET: return "NXRRSET"; | |
| case NS_R_NOTAUTH: return "NOTAUTH"; | |
| case NS_R_NOTZONE: return "NOTZONE"; | |
| case NS_R_INTERNAL_ERROR: return "INTERNAL_ERROR"; | |
| case NS_R_TIMEOUT: return "TIMEOUT"; | |
| default: return fmt::format("UNKNOWN({})", rcode); | |
| } | |
| // clang-format on | |
| } | |
| bool ensureNoInvalidIp(const std::vector<IPSockAddr>& addrs) { | |
| for (const auto& addr : addrs) { | |
| if (addr.ip() == INVALID_IPADDRESS || addr.port() == 0) { | |
| LOG(WARNING) << "Invalid addr: " << addr; | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| } // namespace | |
| // The comparison ignores the last update time. | |
| bool StatsData::operator==(const StatsData& o) const { | |
| return std::tie(sockAddr, total, rcodeCounts, latencyUs) == | |
| std::tie(o.sockAddr, o.total, o.rcodeCounts, o.latencyUs); | |
| } | |
| int StatsData::averageLatencyMs() const { | |
| return (total == 0) ? 0 : duration_cast<milliseconds>(latencyUs).count() / total; | |
| } | |
| std::string StatsData::toString() const { | |
| if (total == 0) return fmt::format("{} <no data>", sockAddr.toString()); | |
| const auto now = std::chrono::steady_clock::now(); | |
| const int lastUpdateSec = duration_cast<seconds>(now - lastUpdate).count(); | |
| std::string buf; | |
| for (const auto& [rcode, counts] : rcodeCounts) { | |
| if (counts != 0) { | |
| buf += fmt::format("{}:{} ", rcodeToName(rcode), counts); | |
| } | |
| } | |
| return fmt::format("{} ({}, {}ms, [{}], {}s)", sockAddr.toString(), total, averageLatencyMs(), | |
| buf, lastUpdateSec); | |
| } | |
| StatsRecords::StatsRecords(const IPSockAddr& ipSockAddr, size_t size) | |
| : mCapacity(size), mStatsData(ipSockAddr) {} | |
| void StatsRecords::push(const Record& record) { | |
| updateStatsData(record, true); | |
| mRecords.push_back(record); | |
| if (mRecords.size() > mCapacity) { | |
| updateStatsData(mRecords.front(), false); | |
| mRecords.pop_front(); | |
| } | |
| // Update the quality factors. | |
| mSkippedCount = 0; | |
| // Because failures due to no permission can't prove that the quality of DNS server is bad, | |
| // skip the penalty update. The average latency, however, has been updated. For short-latency | |
| // servers, it will be fine. For long-latency servers, their average latency will be | |
| // decreased but the latency-based algorithm will adjust their average latency back to the | |
| // right range after few attempts when network is not restricted. | |
| // The check is synced from isNetworkRestricted() in res_send.cpp. | |
| if (record.linux_errno != EPERM) { | |
| updatePenalty(record); | |
| } | |
| } | |
| void StatsRecords::updateStatsData(const Record& record, const bool add) { | |
| const int rcode = record.rcode; | |
| if (add) { | |
| mStatsData.total += 1; | |
| mStatsData.rcodeCounts[rcode] += 1; | |
| mStatsData.latencyUs += record.latencyUs; | |
| } else { | |
| mStatsData.total -= 1; | |
| mStatsData.rcodeCounts[rcode] -= 1; | |
| mStatsData.latencyUs -= record.latencyUs; | |
| } | |
| mStatsData.lastUpdate = std::chrono::steady_clock::now(); | |
| } | |
| void StatsRecords::updatePenalty(const Record& record) { | |
| switch (record.rcode) { | |
| case NS_R_NO_ERROR: | |
| case NS_R_NXDOMAIN: | |
| case NS_R_NOTAUTH: | |
| mPenalty = 0; | |
| return; | |
| default: | |
| // NS_R_TIMEOUT and NS_R_INTERNAL_ERROR are in this case. | |
| if (mPenalty == 0) { | |
| mPenalty = 100; | |
| } else { | |
| // The evaluated quality drops more quickly when continuous failures happen. | |
| mPenalty = std::min(mPenalty * 2, kMaxQuality); | |
| } | |
| return; | |
| } | |
| } | |
| double StatsRecords::score() const { | |
| const int avgRtt = mStatsData.averageLatencyMs(); | |
| // Set the lower bound to -1 in case of "avgRtt + mPenalty < mSkippedCount" | |
| // 1) when the server doesn't have any stats yet. | |
| // 2) when the sorting has been disabled while it was enabled before. | |
| int quality = std::clamp(avgRtt + mPenalty - mSkippedCount, -1, kMaxQuality); | |
| // Normalization. | |
| return static_cast<double>(kMaxQuality - quality) * 100 / kMaxQuality; | |
| } | |
| void StatsRecords::incrementSkippedCount() { | |
| mSkippedCount = std::min(mSkippedCount + 1, kMaxQuality); | |
| } | |
| bool DnsStats::setAddrs(const std::vector<netdutils::IPSockAddr>& addrs, Protocol protocol) { | |
| if (!ensureNoInvalidIp(addrs)) return false; | |
| StatsMap& statsMap = mStats[protocol]; | |
| for (const auto& addr : addrs) { | |
| statsMap.try_emplace(addr, StatsRecords(addr, kLogSize)); | |
| } | |
| // Clean up the map to eliminate the nodes not belonging to the given list of servers. | |
| const auto cleanup = [&](StatsMap* statsMap) { | |
| StatsMap tmp; | |
| for (const auto& addr : addrs) { | |
| if (statsMap->find(addr) != statsMap->end()) { | |
| tmp.insert(statsMap->extract(addr)); | |
| } | |
| } | |
| statsMap->swap(tmp); | |
| }; | |
| cleanup(&statsMap); | |
| return true; | |
| } | |
| bool DnsStats::addStats(const IPSockAddr& ipSockAddr, const DnsQueryEvent& record) { | |
| if (ipSockAddr.ip() == INVALID_IPADDRESS) return false; | |
| bool added = false; | |
| for (auto& [sockAddr, statsRecords] : mStats[record.protocol()]) { | |
| if (sockAddr == ipSockAddr) { | |
| const StatsRecords::Record rec = { | |
| .rcode = record.rcode(), | |
| .linux_errno = record.linux_errno(), | |
| .latencyUs = microseconds(record.latency_micros()), | |
| }; | |
| statsRecords.push(rec); | |
| added = true; | |
| } else { | |
| statsRecords.incrementSkippedCount(); | |
| } | |
| } | |
| return added; | |
| } | |
| std::vector<IPSockAddr> DnsStats::getSortedServers(Protocol protocol) const { | |
| // DoT unsupported. The handshake overhead is expensive, and the connection will hang for a | |
| // while. Need to figure out if it is worth doing for DoT servers. | |
| if (protocol == PROTO_DOT) return {}; | |
| auto it = mStats.find(protocol); | |
| if (it == mStats.end()) return {}; | |
| // Sorting on insertion in decreasing order. | |
| std::multimap<double, IPSockAddr, std::greater<double>> sortedData; | |
| for (const auto& [ip, statsRecords] : it->second) { | |
| sortedData.insert({statsRecords.score(), ip}); | |
| } | |
| std::vector<IPSockAddr> ret; | |
| ret.reserve(sortedData.size()); | |
| for (auto& [_, v] : sortedData) { | |
| ret.push_back(v); // IPSockAddr is trivially-copyable. | |
| } | |
| return ret; | |
| } | |
| std::optional<microseconds> DnsStats::getAverageLatencyUs(Protocol protocol) const { | |
| const auto stats = getStats(protocol); | |
| int count = 0; | |
| microseconds sum; | |
| for (const auto& v : stats) { | |
| count += v.total; | |
| sum += v.latencyUs; | |
| } | |
| if (count == 0) return std::nullopt; | |
| return sum / count; | |
| } | |
| std::vector<StatsData> DnsStats::getStats(Protocol protocol) const { | |
| std::vector<StatsData> ret; | |
| if (mStats.find(protocol) != mStats.end()) { | |
| for (const auto& [_, statsRecords] : mStats.at(protocol)) { | |
| ret.push_back(statsRecords.getStatsData()); | |
| } | |
| } | |
| return ret; | |
| } | |
| void DnsStats::dump(DumpWriter& dw) { | |
| const auto dumpStatsMap = [&](StatsMap& statsMap) { | |
| ScopedIndent indentLog(dw); | |
| if (statsMap.size() == 0) { | |
| dw.println("<no data>"); | |
| return; | |
| } | |
| for (const auto& [_, statsRecords] : statsMap) { | |
| const StatsData& data = statsRecords.getStatsData(); | |
| std::string str = | |
| fmt::format("{} score{{{:.1f}}}", data.toString(), statsRecords.score()); | |
| dw.println("%s", str.c_str()); | |
| } | |
| }; | |
| dw.println("Server statistics: (total, RTT avg, {rcode:counts}, last update)"); | |
| ScopedIndent indentStats(dw); | |
| dw.println("over UDP"); | |
| dumpStatsMap(mStats[PROTO_UDP]); | |
| dw.println("over DOH"); | |
| dumpStatsMap(mStats[PROTO_DOH]); | |
| dw.println("over TLS"); | |
| dumpStatsMap(mStats[PROTO_DOT]); | |
| dw.println("over TCP"); | |
| dumpStatsMap(mStats[PROTO_TCP]); | |
| dw.println("over MDNS"); | |
| dumpStatsMap(mStats[PROTO_MDNS]); | |
| } | |
| } // namespace android::net |