Merge changes from topic "querylog"
* changes:
Add unit tests for DnsQueryLog
Log DNS resolved answers
diff --git a/Android.bp b/Android.bp
index 0faf45f..28cb03e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -60,6 +60,7 @@
"util.cpp",
"Dns64Configuration.cpp",
"DnsProxyListener.cpp",
+ "DnsQueryLog.cpp",
"DnsResolver.cpp",
"DnsResolverService.cpp",
"DnsStats.cpp",
@@ -174,6 +175,7 @@
"resolv_tls_unit_test.cpp",
"resolv_unit_test.cpp",
"DnsStatsTest.cpp",
+ "DnsQueryLogTest.cpp",
],
shared_libs: [
"libbase",
diff --git a/DnsProxyListener.cpp b/DnsProxyListener.cpp
index 341cc76..4eef162 100644
--- a/DnsProxyListener.cpp
+++ b/DnsProxyListener.cpp
@@ -329,6 +329,24 @@
return (arc4random_uniform(subsampling_denom) == 0) ? subsampling_denom : 0;
}
+void maybeLogQuery(int eventType, const android_net_context& netContext,
+ const NetworkDnsEventReported& event, const std::string& query_name,
+ const std::vector<std::string>& ip_addrs) {
+ // Skip reverse queries.
+ if (eventType == INetdEventListener::EVENT_GETHOSTBYADDR) return;
+
+ for (const auto& query : event.dns_query_events().dns_query_event()) {
+ // Log it when the cache misses.
+ if (query.cache_hit() != CS_FOUND) {
+ const int timeTakenMs = event.latency_micros() / 1000;
+ DnsQueryLog::Record record(netContext.dns_netid, netContext.uid, netContext.pid,
+ query_name, ip_addrs, timeTakenMs);
+ gDnsResolv->dnsQueryLog().push(std::move(record));
+ return;
+ }
+ }
+}
+
void reportDnsEvent(int eventType, const android_net_context& netContext, int latencyUs,
int returnCode, NetworkDnsEventReported& event, const std::string& query_name,
const std::vector<std::string>& ip_addrs = {}, int total_ip_addr_count = 0) {
@@ -343,6 +361,8 @@
event.private_dns_modes(), dnsQueryBytesField, rate);
}
+ maybeLogQuery(eventType, netContext, event, query_name, ip_addrs);
+
const auto& listeners = ResolverEventReporter::getInstance().getListeners();
if (listeners.size() == 0) {
LOG(ERROR) << __func__
diff --git a/DnsQueryLog.cpp b/DnsQueryLog.cpp
new file mode 100644
index 0000000..6f0e179
--- /dev/null
+++ b/DnsQueryLog.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ *
+ */
+
+#include "DnsQueryLog.h"
+
+#include <android-base/stringprintf.h>
+
+namespace android::net {
+
+namespace {
+
+std::string maskHostname(const std::string& hostname) {
+ // Boundary issue is handled in substr().
+ return hostname.substr(0, 1) + "***";
+}
+
+// Return the string of masked addresses of the first v4 address and the first v6 address.
+std::string maskIps(const std::vector<std::string>& ips) {
+ std::string ret;
+ bool v4Found = false, v6Found = false;
+ for (const auto& ip : ips) {
+ if (auto pos = ip.find_first_of(':'); pos != ip.npos && !v6Found) {
+ ret += ip.substr(0, pos + 1) + "***, ";
+ v6Found = true;
+ } else if (auto pos = ip.find_first_of('.'); pos != ip.npos && !v4Found) {
+ ret += ip.substr(0, pos + 1) + "***, ";
+ v4Found = true;
+ }
+ if (v6Found && v4Found) break;
+ }
+ return ret.empty() ? "" : ret.substr(0, ret.length() - 2);
+}
+
+// Return the readable string format "hr:min:sec.ms".
+std::string timestampToString(const std::chrono::system_clock::time_point& ts) {
+ using std::chrono::duration_cast;
+ using std::chrono::milliseconds;
+ const auto time_sec = std::chrono::system_clock::to_time_t(ts);
+ char buf[32];
+ std::strftime(buf, sizeof(buf), "%H:%M:%S", std::localtime(&time_sec));
+ int ms = duration_cast<milliseconds>(ts.time_since_epoch()).count() % 1000;
+ return android::base::StringPrintf("%s.%03d", buf, ms);
+}
+
+} // namespace
+
+void DnsQueryLog::push(Record&& record) {
+ std::lock_guard guard(mLock);
+ mQueue.push_back(std::move(record));
+ if (mQueue.size() > mCapacity) {
+ mQueue.pop_front();
+ }
+}
+
+void DnsQueryLog::dump(netdutils::DumpWriter& dw) const {
+ dw.println("DNS query log (last %lld minutes):", (mValidityTimeMs / 60000).count());
+ netdutils::ScopedIndent indentStats(dw);
+ const auto now = std::chrono::system_clock::now();
+
+ std::lock_guard guard(mLock);
+ for (const auto& record : mQueue) {
+ if (now - record.timestamp > mValidityTimeMs) continue;
+
+ const std::string maskedHostname = maskHostname(record.hostname);
+ const std::string maskedIpsStr = maskIps(record.addrs);
+ const std::string time = timestampToString(record.timestamp);
+ dw.println("time=%s netId=%u uid=%u pid=%d hostname=%s answer=[%s] (%dms)", time.c_str(),
+ record.netId, record.uid, record.pid, maskedHostname.c_str(),
+ maskedIpsStr.c_str(), record.timeTaken);
+ }
+}
+
+} // namespace android::net
diff --git a/DnsQueryLog.h b/DnsQueryLog.h
new file mode 100644
index 0000000..c19f8db
--- /dev/null
+++ b/DnsQueryLog.h
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ *
+ */
+
+#pragma once
+
+#include <deque>
+#include <string>
+#include <vector>
+
+#include <android-base/thread_annotations.h>
+#include <netdutils/DumpWriter.h>
+
+namespace android::net {
+
+// A circular buffer based class used for query logging. It's thread-safe for concurrent access.
+class DnsQueryLog {
+ public:
+ static constexpr std::string_view DUMP_KEYWORD = "querylog";
+
+ struct Record {
+ Record(uint32_t netId, uid_t uid, pid_t pid, const std::string& hostname,
+ const std::vector<std::string>& addrs, int timeTaken)
+ : netId(netId),
+ uid(uid),
+ pid(pid),
+ hostname(hostname),
+ addrs(addrs),
+ timeTaken(timeTaken) {}
+ const uint32_t netId;
+ const uid_t uid;
+ const pid_t pid;
+ const std::chrono::system_clock::time_point timestamp = std::chrono::system_clock::now();
+ const std::string hostname;
+ const std::vector<std::string> addrs;
+ const int timeTaken;
+ };
+
+ // Allow the tests to set the capacity and the validaty time in milliseconds.
+ DnsQueryLog(size_t size = kDefaultLogSize,
+ std::chrono::milliseconds time = kDefaultValidityMinutes)
+ : mCapacity(size), mValidityTimeMs(time) {}
+
+ void push(Record&& record) EXCLUDES(mLock);
+ void dump(netdutils::DumpWriter& dw) const EXCLUDES(mLock);
+
+ private:
+ mutable std::mutex mLock;
+ std::deque<Record> mQueue GUARDED_BY(mLock);
+ const size_t mCapacity;
+ const std::chrono::milliseconds mValidityTimeMs;
+
+ // The capacity of the circular buffer.
+ static constexpr size_t kDefaultLogSize = 200;
+
+ // Limit to dump the queries within last |kDefaultValidityMinutes| minutes.
+ static constexpr std::chrono::minutes kDefaultValidityMinutes{60};
+};
+
+} // namespace android::net
diff --git a/DnsQueryLogTest.cpp b/DnsQueryLogTest.cpp
new file mode 100644
index 0000000..b652412
--- /dev/null
+++ b/DnsQueryLogTest.cpp
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+
+#include <regex>
+#include <thread>
+
+#include <android-base/strings.h>
+#include <android-base/test_utils.h>
+#include <gtest/gtest.h>
+
+#include "DnsQueryLog.h"
+
+using namespace std::chrono_literals;
+
+namespace android::net {
+
+namespace {
+
+// Dump the log to STDOUT and capture it.
+std::string captureDumpOutput(const DnsQueryLog& queryLog) {
+ netdutils::DumpWriter dw(STDOUT_FILENO);
+ CapturedStdout captured;
+ queryLog.dump(dw);
+ return captured.str();
+}
+
+// A simple check for the dump result by checking the netIds one by one.
+void verifyDumpOutput(const std::string& dumpLog, const std::vector<int>& expectedNetIds) {
+ // Capture three matches: netId, hostname, and answer (empty allowed).
+ static const std::regex pattern(
+ R"(netId=(\d+).* hostname=([\w\*]+) answer=\[([\w:,\.\*\s]*)\])");
+
+ std::string str(dumpLog);
+ std::smatch sm;
+ for (const auto& netId : expectedNetIds) {
+ SCOPED_TRACE(netId);
+ EXPECT_TRUE(std::regex_search(str, sm, pattern));
+ EXPECT_EQ(sm[1], std::to_string(netId));
+ str = sm.suffix();
+ }
+
+ // Ensure the dumpLog is exactly as expected.
+ EXPECT_FALSE(std::regex_search(str, sm, pattern));
+}
+
+} // namespace
+
+class DnsQueryLogTest : public ::testing::Test {
+ protected:
+ const std::vector<std::string> serversV4 = {"127.0.0.1", "1.2.3.4"};
+ const std::vector<std::string> serversV4V6 = {"127.0.0.1", "1.2.3.4", "2001:db8::1",
+ "fe80:1::2%testnet"};
+};
+
+TEST_F(DnsQueryLogTest, Push) {
+ std::vector<DnsQueryLog::Record> records = {
+ DnsQueryLog::Record(30, 1000, 1000, "example.com", serversV4, 10),
+ DnsQueryLog::Record(31, 1000, 1000, "", serversV4, 10), // Empty hostname.
+ DnsQueryLog::Record(32, 1000, 1000, "example.com", {}, 10), // No answer.
+ DnsQueryLog::Record(33, 1000, 1000, "example.com", serversV4V6, 10),
+ };
+ DnsQueryLog queryLog;
+ for (auto& r : records) {
+ queryLog.push(std::move(r));
+ }
+
+ std::string output = captureDumpOutput(queryLog);
+ verifyDumpOutput(output, {30, 31, 32, 33});
+}
+
+TEST_F(DnsQueryLogTest, PushStressTest) {
+ const int threadNum = 100;
+ const int pushNum = 1000;
+ const size_t size = 500;
+ DnsQueryLog queryLog(size);
+ std::vector<std::thread> threads(threadNum);
+
+ // Launch 'threadNum' threads to push the same queryLog 'pushNum' times.
+ for (auto& thread : threads) {
+ thread = std::thread([&]() {
+ for (int i = 0; i < pushNum; i++) {
+ DnsQueryLog::Record record(30, 1000, 1000, "www.example.com", serversV4, 10);
+ queryLog.push(std::move(record));
+ }
+ });
+ }
+ for (auto& thread : threads) {
+ thread.join();
+ }
+
+ // Verify there are exact 'size' records in queryLog.
+ std::string output = captureDumpOutput(queryLog);
+ verifyDumpOutput(output, std::vector(size, 30));
+}
+
+TEST_F(DnsQueryLogTest, ZeroSize) {
+ const size_t size = 0;
+ DnsQueryLog::Record r1(30, 1000, 1000, "www.example1.com", serversV4V6, 10);
+ DnsQueryLog::Record r2(31, 1000, 1000, "www.example2.com", serversV4V6, 10);
+ DnsQueryLog::Record r3(32, 1000, 1000, "www.example3.com", serversV4V6, 10);
+
+ DnsQueryLog queryLog(size);
+ queryLog.push(std::move(r1));
+ queryLog.push(std::move(r2));
+ queryLog.push(std::move(r3));
+
+ std::string output = captureDumpOutput(queryLog);
+ verifyDumpOutput(output, {});
+}
+
+TEST_F(DnsQueryLogTest, CapacityFull) {
+ const size_t size = 3;
+ DnsQueryLog::Record r1(30, 1000, 1000, "www.example1.com", serversV4V6, 10);
+ DnsQueryLog::Record r2(31, 1000, 1000, "www.example2.com", serversV4V6, 10);
+ DnsQueryLog::Record r3(32, 1000, 1000, "www.example3.com", serversV4V6, 10);
+ DnsQueryLog::Record r4(33, 1000, 1000, "www.example4.com", serversV4V6, 10);
+ const std::vector<int> expectedNetIds = {31, 32, 33};
+
+ DnsQueryLog queryLog(size);
+ queryLog.push(std::move(r1));
+ queryLog.push(std::move(r2));
+ queryLog.push(std::move(r3));
+ queryLog.push(std::move(r4));
+
+ std::string output = captureDumpOutput(queryLog);
+ verifyDumpOutput(output, expectedNetIds);
+}
+
+TEST_F(DnsQueryLogTest, ValidityTime) {
+ DnsQueryLog::Record r1(30, 1000, 1000, "www.example.com", serversV4, 10);
+ DnsQueryLog queryLog(3, 100ms);
+ queryLog.push(std::move(r1));
+
+ // Dump the output and verify the correctness by checking netId.
+ std::string output = captureDumpOutput(queryLog);
+ verifyDumpOutput(output, {30});
+
+ std::this_thread::sleep_for(150ms);
+
+ // The record is expired thus not shown in the output.
+ output = captureDumpOutput(queryLog);
+ verifyDumpOutput(output, {});
+
+ // Push another record to ensure it still works.
+ DnsQueryLog::Record r2(31, 1000, 1000, "example.com", serversV4V6, 10);
+ queryLog.push(std::move(r2));
+ output = captureDumpOutput(queryLog);
+ verifyDumpOutput(output, {31});
+}
+
+} // namespace android::net
diff --git a/DnsResolver.h b/DnsResolver.h
index 2efa341..863b096 100644
--- a/DnsResolver.h
+++ b/DnsResolver.h
@@ -18,6 +18,7 @@
#define _DNS_RESOLVER_H_
#include "DnsProxyListener.h"
+#include "DnsQueryLog.h"
#include "ResolverController.h"
#include "netd_resolv/resolv.h"
#include "netdutils/Log.h"
@@ -34,11 +35,14 @@
DnsResolver(DnsResolver const&) = delete;
void operator=(DnsResolver const&) = delete;
+ DnsQueryLog& dnsQueryLog() { return mQueryLog; }
+
ResolverController resolverCtrl;
private:
DnsResolver() {}
DnsProxyListener mDnsProxyListener;
+ DnsQueryLog mQueryLog;
};
extern DnsResolver* gDnsResolv;
diff --git a/DnsResolverService.cpp b/DnsResolverService.cpp
index a9ec8e7..c4b42bc 100644
--- a/DnsResolverService.cpp
+++ b/DnsResolverService.cpp
@@ -92,7 +92,7 @@
return STATUS_OK;
}
-binder_status_t DnsResolverService::dump(int fd, const char**, uint32_t) {
+binder_status_t DnsResolverService::dump(int fd, const char** args, uint32_t numArgs) {
auto dump_permission = checkAnyPermission({PERM_DUMP});
if (!dump_permission.isOk()) {
return STATUS_PERMISSION_DENIED;
@@ -101,6 +101,14 @@
// This method does not grab any locks. If individual classes need locking
// their dump() methods MUST handle locking appropriately.
DumpWriter dw(fd);
+
+ if (numArgs == 1 && std::string(args[0]) == DnsQueryLog::DUMP_KEYWORD) {
+ dw.blankline();
+ gDnsResolv->dnsQueryLog().dump(dw);
+ dw.blankline();
+ return STATUS_OK;
+ }
+
for (auto netId : resolv_list_caches()) {
dw.println("NetId: %u", netId);
gDnsResolv->resolverCtrl.dump(dw, netId);