Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions app/src/main/java/eu/faircode/netguard/DatabaseHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -1166,25 +1166,28 @@ public Cursor getQAName(int uid, String ip, boolean alive) {
SQLiteDatabase db = readableDb;
String escapedIp = ip.replace("'", "''");
String aliveFilter = alive
? " AND (%1$s.time IS NULL OR %1$s.time + %1$s.ttl >= " + now + ")"
? " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"
: "";
// There is a segmented index on resource. A shared IP can carry
// DNS evidence for several qnames; keep only the most recently
// observed row per qname (dedup) and order qnames by recency, so
// the freshest resolution — most likely tied to the connection
// that's actually being made now — is attributed first instead
// of an alphabetically-first but possibly stale one.
String query = "SELECT d.qname, d.aname, d.time, d.ttl" +
//
// The dedup deliberately uses a single MAX(time) aggregate: with
// exactly one min/max aggregate, SQLite takes the bare columns
// from the row that supplied the maximum, so this is one index
// range scan over the IP's rows. A correlated per-row subquery
// here re-scans the IP's rows once per candidate row — O(n²) —
// and this query runs for every new connection (log() and
// blockKnownTracker()), where it grows with DNS history until
// it shows up as battery drain and heat.
String query = "SELECT d.qname, d.aname, d.time, d.ttl, MAX(d.time)" +
" FROM dns AS d" +
" WHERE d.resource = '" + escapedIp + "'" +
String.format(aliveFilter, "d") +
" AND d.ID = (" +
" SELECT d2.ID FROM dns AS d2" +
" WHERE d2.resource = d.resource AND d2.qname = d.qname" +
String.format(aliveFilter, "d2") +
" ORDER BY d2.time DESC, d2.ID DESC" +
" LIMIT 1" +
" )" +
aliveFilter +
" GROUP BY d.qname" +
" ORDER BY d.time DESC, d.ID DESC";
return db.rawQuery(query, new String[] {});
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ public void repeatedObservationsOfSameQnameCollapseToFreshestRow() {
}
}

@Test
public void aliveFilterAppliesBeforeDedup() {
DatabaseHelper dh = DatabaseHelper.getInstance(RuntimeEnvironment.getApplication());
dh.clearDns();

String ip = "203.0.113.30";
// The freshest observation has already expired; an older one is still
// alive. With alive=true the expired row must not shadow the alive one.
// Rows are inserted directly because insertDns() clamps the TTL to the
// "ttl" preference floor, which would keep the fresh row alive.
long now = System.currentTimeMillis();
dh.getWritableDatabase().execSQL(
"INSERT INTO dns (time, qname, aname, resource, ttl) VALUES ("
+ (now - 10_000) + ", 't2.example.com', 'alive-cname.example.com', '"
+ ip + "', 3600000)");
dh.getWritableDatabase().execSQL(
"INSERT INTO dns (time, qname, aname, resource, ttl) VALUES ("
+ (now - 5_000) + ", 't2.example.com', 'expired-cname.example.com', '"
+ ip + "', 1000)");

try (Cursor c = dh.getQAName(-1, ip, true)) {
assertEquals(1, c.getCount());
assertTrue(c.moveToFirst());
assertEquals("alive-cname.example.com", c.getString(c.getColumnIndexOrThrow("aname")));
}
}

private static ResourceRecord rr(long time, String qname, String aname, String resource, int ttl) {
ResourceRecord rr = new ResourceRecord();
rr.Time = time;
Expand Down