Files
FTL/src/database/query-table.c
T
2020-11-30 19:27:01 +01:00

554 lines
16 KiB
C

/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* Query table database routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "../FTL.h"
#include "query-table.h"
#include "common.h"
// get[Domain,ClientIP,Forward]String(), etc.
#include "../datastructure.h"
// getOverTimeID()
#include "../overTime.h"
// get_FTL_db_filesize()
#include "../files.h"
// timer_elapsed_msec()
#include "../timers.h"
// logg()
#include "../log.h"
// struct config
#include "../config.h"
// getstr()
#include "../shmem.h"
// free()
#include "../memory.h"
static bool saving_failed_before = false;
int get_number_of_queries_in_DB(void)
{
// This routine is used by the API routines.
if(!FTL_DB_avail())
{
return DB_FAILED;
}
// Count number of rows using the index timestamp is faster than select(*)
int result = db_query_int("SELECT COUNT(timestamp) FROM queries");
return result;
}
void DB_save_queries(void)
{
// Start database timer
if(config.debug & DEBUG_DATABASE)
timer_start(DATABASE_WRITE_TIMER);
unsigned int saved = 0;
bool error = false;
sqlite3_stmt* stmt = NULL;
int rc = dbquery("BEGIN TRANSACTION IMMEDIATE");
if( rc != SQLITE_OK )
{
const char *text;
if( rc == SQLITE_BUSY )
text = "WARNING";
else
{
text = "ERROR";
dbclose();
}
logg("%s: Storing queries in long-term database failed: %s", text, sqlite3_errstr(rc));
return;
}
rc = sqlite3_prepare_v2(FTL_db, "INSERT INTO queries VALUES (NULL,?,?,?,?,?,?,?)", -1, &stmt, NULL);
if( rc != SQLITE_OK )
{
const char *text, *spaces;
if( rc == SQLITE_BUSY )
{
text = "WARNING";
spaces = " ";
}
else
{
text = "ERROR";
spaces = " ";
dbclose();
}
// dbquery() above already logs the reson for why the query failed
logg("%s: Storing queries in long-term database failed: %s\n", text, sqlite3_errstr(rc));
logg("%s Keeping queries in memory for later new attempt", spaces);
saving_failed_before = true;
return;
}
// Get last ID stored in the database
long int lastID = get_max_query_ID();
int total = 0, blocked = 0;
time_t currenttimestamp = time(NULL);
time_t newlasttimestamp = 0;
long int queryID;
for(queryID = MAX(0, lastdbindex); queryID < counters->queries; queryID++)
{
queriesData* query = getQuery(queryID, true);
if(query->db != 0)
{
// Skip, already saved in database
continue;
}
if(!query->complete && query->timestamp > currenttimestamp-2)
{
// Break if a brand new query (age < 2 seconds) is not yet completed
// giving it a chance to be stored next time
break;
}
if(query->privacylevel >= PRIVACY_MAXIMUM)
{
// Skip, we never store nor count queries recorded
// while have been in maximum privacy mode in the database
continue;
}
// TIMESTAMP
sqlite3_bind_int(stmt, 1, query->timestamp);
// TYPE
sqlite3_bind_int(stmt, 2, query->type);
// STATUS
sqlite3_bind_int(stmt, 3, query->status);
// DOMAIN
const char *domain = getDomainString(query);
sqlite3_bind_text(stmt, 4, domain, -1, SQLITE_STATIC);
// CLIENT
const char *client = getClientIPString(query);
sqlite3_bind_text(stmt, 5, client, -1, SQLITE_STATIC);
// FORWARD
if(query->status == QUERY_FORWARDED && query->upstreamID > -1)
{
// Get forward pointer
const upstreamsData* upstream = getUpstream(query->upstreamID, true);
char *buffer = NULL;
if(asprintf(&buffer, "%s#%u", getstr(upstream->ippos), upstream->port) > 0)
sqlite3_bind_text(stmt, 6, buffer, -1, SQLITE_TRANSIENT);
else
sqlite3_bind_null(stmt, 6);
if(buffer != NULL)
free(buffer);
}
else
{
sqlite3_bind_null(stmt, 6);
}
// ADDITIONAL_INFO
if(query->status == QUERY_GRAVITY_CNAME ||
query->status == QUERY_REGEX_CNAME ||
query->status == QUERY_BLACKLIST_CNAME)
{
// Restore domain blocked during deep CNAME inspection if applicable
const char* cname = getCNAMEDomainString(query);
sqlite3_bind_text(stmt, 7, cname, -1, SQLITE_STATIC);
}
else if(query->status == QUERY_REGEX)
{
// Restore regex ID if applicable
const int cacheID = findCacheID(query->domainID, query->clientID, query->type);
DNSCacheData *cache = getDNSCache(cacheID, true);
if(cache != NULL)
sqlite3_bind_int(stmt, 7, cache->black_regex_idx);
else
sqlite3_bind_null(stmt, 7);
}
else
{
// Nothing to add here
sqlite3_bind_null(stmt, 7);
}
// Step and check if successful
rc = sqlite3_step(stmt);
sqlite3_clear_bindings(stmt);
sqlite3_reset(stmt);
if( rc != SQLITE_DONE )
{
logg("Encountered error while trying to store queries in long-term database: %s", sqlite3_errstr(rc));
error = true;
break;
}
saved++;
// Mark this query as saved in the database by setting the corresponding ID
query->db = ++lastID;
// Total counter information (delta computation)
total++;
if(query->status == QUERY_GRAVITY ||
query->status == QUERY_BLACKLIST ||
query->status == QUERY_REGEX ||
query->status == QUERY_EXTERNAL_BLOCKED_IP ||
query->status == QUERY_EXTERNAL_BLOCKED_NULL ||
query->status == QUERY_EXTERNAL_BLOCKED_NXRA ||
query->status == QUERY_GRAVITY_CNAME ||
query->status == QUERY_REGEX_CNAME ||
query->status == QUERY_BLACKLIST_CNAME)
blocked++;
// Update lasttimestamp variable with timestamp of the latest stored query
if(query->timestamp > newlasttimestamp)
newlasttimestamp = query->timestamp;
}
if((rc = sqlite3_finalize(stmt)) != SQLITE_OK)
{
logg("Statement finalization failed when trying to store queries to long-term database: %s",
sqlite3_errstr(rc));
if( rc == SQLITE_BUSY )
{
logg("Keeping queries in memory for later new attempt");
saving_failed_before = true;
}
else
dbclose();
return;
}
// Finish prepared statement
if((rc = dbquery("END TRANSACTION")) != SQLITE_OK)
{
// No need to log the error string here, dbquery() did that already above
logg("END TRANSACTION failed when trying to store queries to long-term database");
if( rc == SQLITE_BUSY )
{
logg("Keeping queries in memory for later new attempt");
saving_failed_before = true;
}
else
dbclose();
return;
}
// Store index for next loop interation round and update last time stamp
// in the database only if all queries have been saved successfully
if(saved > 0 && !error)
{
lastdbindex = queryID;
db_set_FTL_property(DB_LASTTIMESTAMP, newlasttimestamp);
db_update_counters(total, blocked);
}
if(config.debug & DEBUG_DATABASE || saving_failed_before)
{
logg("Notice: Queries stored in long-term database: %u (took %.1f ms, last SQLite ID %li)", saved, timer_elapsed_msec(DATABASE_WRITE_TIMER), lastID);
if(saving_failed_before)
{
logg(" Queries from earlier attempt(s) stored successfully");
saving_failed_before = false;
}
}
}
void delete_old_queries_in_DB(void)
{
// Open database
if(!FTL_DB_avail())
{
return;
}
int timestamp = time(NULL) - config.maxDBdays * 86400;
if(dbquery("DELETE FROM queries WHERE timestamp <= %i", timestamp) != SQLITE_OK)
{
logg("delete_old_queries_in_DB(): Deleting queries due to age of entries failed!");
return;
}
// Get how many rows have been affected (deleted)
const int affected = sqlite3_changes(FTL_db);
// Print final message only if there is a difference
if((config.debug & DEBUG_DATABASE) || affected)
logg("Notice: Database size is %.2f MB, deleted %i rows", 1e-6*get_FTL_db_filesize(), affected);
}
// Get most recent 24 hours data from long-term database
void DB_read_queries(void)
{
// Open database
if(!dbopen())
return;
// Prepare request
// Get time stamp 24 hours in the past
const time_t now = time(NULL);
const time_t mintime = now - config.maxlogage;
const char *querystr = "SELECT * FROM queries WHERE timestamp >= ?";
// Log FTL_db query string in debug mode
if(config.debug & DEBUG_DATABASE)
logg("DB_read_queries(): \"%s\" with ? = %lli", querystr, (long long)mintime);
// Prepare SQLite3 statement
sqlite3_stmt* stmt = NULL;
int rc = sqlite3_prepare_v2(FTL_db, querystr, -1, &stmt, NULL);
if( rc != SQLITE_OK ){
logg("DB_read_queries() - SQL error prepare: %s", sqlite3_errstr(rc));
dbclose();
return;
}
// Bind limit
if((rc = sqlite3_bind_int(stmt, 1, mintime)) != SQLITE_OK)
{
logg("DB_read_queries() - Failed to bind type mintime: %s", sqlite3_errstr(rc));
dbclose();
return;
}
// Loop through returned database rows
while((rc = sqlite3_step(stmt)) == SQLITE_ROW)
{
const sqlite3_int64 dbid = sqlite3_column_int64(stmt, 0);
const time_t queryTimeStamp = sqlite3_column_int(stmt, 1);
// 1483228800 = 01/01/2017 @ 12:00am (UTC)
if(queryTimeStamp < 1483228800)
{
logg("FTL_db warn: TIMESTAMP should be larger than 01/01/2017 but is %lli", (long long)queryTimeStamp);
continue;
}
if(queryTimeStamp > now)
{
if(config.debug & DEBUG_DATABASE) logg("FTL_db warn: Skipping query logged in the future (%lli)", (long long)queryTimeStamp);
continue;
}
const int type = sqlite3_column_int(stmt, 2);
if(type < TYPE_A || type >= TYPE_MAX)
{
logg("FTL_db warn: TYPE should not be %i", type);
continue;
}
// Don't import AAAA queries from database if the user set
// AAAA_QUERY_ANALYSIS=no in pihole-FTL.conf
if(type == TYPE_AAAA && !config.analyze_AAAA)
{
continue;
}
const int status = sqlite3_column_int(stmt, 3);
if(status < QUERY_UNKNOWN || status >= QUERY_STATUS_MAX)
{
logg("FTL_db warn: STATUS should be within [%i,%i] but is %i", QUERY_UNKNOWN, QUERY_STATUS_MAX-1, status);
continue;
}
const char * domainname = (const char *)sqlite3_column_text(stmt, 4);
if(domainname == NULL)
{
logg("FTL_db warn: DOMAIN should never be NULL, %lli", (long long)queryTimeStamp);
continue;
}
const char * clientIP = (const char *)sqlite3_column_text(stmt, 5);
if(clientIP == NULL)
{
logg("FTL_db warn: CLIENT should never be NULL, %lli", (long long)queryTimeStamp);
continue;
}
// Check if user wants to skip queries coming from localhost
if(config.ignore_localhost &&
(strcmp(clientIP, "127.0.0.1") == 0 || strcmp(clientIP, "::1") == 0))
{
continue;
}
const char *upstream = (const char *)sqlite3_column_text(stmt, 6);
int upstreamID = 0;
// Determine upstreamID only when status == 2 (forwarded) as the
// field need not to be filled for other query status types
if(status == QUERY_FORWARDED)
{
if(upstream == NULL)
{
logg("WARN (during database import): FORWARD should not be NULL with status QUERY_FORWARDED (timestamp: %lli), skipping entry",
(long long)queryTimeStamp);
continue;
}
// Get IP address and port of upstream destination
char serv_addr[INET6_ADDRSTRLEN] = { 0 };
unsigned int serv_port = 53;
// We limit the number of bytes written into the serv_addr buffer
// to prevent buffer overflows. If there is no port available in
// the database, we skip extracting them and use the default port
sscanf(upstream, "%"xstr(INET6_ADDRSTRLEN)"[^#]#%u", serv_addr, &serv_port);
serv_addr[INET6_ADDRSTRLEN-1] = '\0';
upstreamID = findUpstreamID(serv_addr, (in_port_t)serv_port, true);
}
// Obtain IDs only after filtering which queries we want to keep
const int timeidx = getOverTimeID(queryTimeStamp);
const int domainID = findDomainID(domainname, true);
const int clientID = findClientID(clientIP, true, false);
// Ensure we have enough space in the queries struct
memory_check(QUERIES);
// Set index for this query
const int queryIndex = counters->queries;
// Store this query in memory
queriesData* query = getQuery(queryIndex, false);
query->magic = MAGICBYTE;
query->timestamp = queryTimeStamp;
query->type = type;
query->status = status;
query->domainID = domainID;
query->clientID = clientID;
query->upstreamID = upstreamID;
query->timeidx = timeidx;
query->db = dbid;
query->id = 0;
query->complete = true; // Mark as all information is available
query->response = 0;
query->dnssec = DNSSEC_UNSPECIFIED;
query->reply = REPLY_UNKNOWN;
query->CNAME_domainID = -1;
// Set lastQuery timer for network table
clientsData* client = getClient(clientID, true);
client->lastQuery = queryTimeStamp;
// Handle type counters
if(type >= TYPE_A && type < TYPE_MAX)
{
counters->querytype[type-1]++;
overTime[timeidx].querytypedata[type-1]++;
}
// Update overTime data
overTime[timeidx].total++;
// Update overTime data structure with the new client
change_clientcount(client, 0, 0, timeidx, 1);
// Increase DNS queries counter
counters->queries++;
// Get additional information from the additional_info column if applicable
if(status == QUERY_GRAVITY_CNAME ||
status == QUERY_REGEX_CNAME ||
status == QUERY_BLACKLIST_CNAME)
{
// QUERY_*_CNAME: Getdomain causing the blocking
const char *CNAMEdomain = (const char *)sqlite3_column_text(stmt, 7);
if(CNAMEdomain != NULL && strlen(CNAMEdomain) > 0)
{
// Add domain to FTL's memory but do not count it. Seeing a
// domain in the middle of a CNAME trajectory does not mean
// it was queried intentionally.
const int CNAMEdomainID = findDomainID(CNAMEdomain, false);
query->CNAME_domainID = CNAMEdomainID;
}
}
else if(status == QUERY_REGEX)
{
// QUERY_REGEX: Set ID regex which was the reson for blocking
const int cacheID = findCacheID(query->domainID, query->clientID, query->type);
DNSCacheData *cache = getDNSCache(cacheID, true);
// Only load if
// a) we have a chace entry
// b) the value of additional_info is not NULL (0 bytes storage size)
if(cache != NULL && sqlite3_column_bytes(stmt, 7) != 0)
cache->black_regex_idx = sqlite3_column_int(stmt, 7);
}
// Increment status counters
switch(status)
{
case QUERY_UNKNOWN: // Unknown
counters->unknown++;
break;
case QUERY_GRAVITY: // Blocked by gravity
case QUERY_REGEX: // Blocked by regex blacklist
case QUERY_BLACKLIST: // Blocked by exact blacklist
case QUERY_EXTERNAL_BLOCKED_IP: // Blocked by external provider
case QUERY_EXTERNAL_BLOCKED_NULL: // Blocked by external provider
case QUERY_EXTERNAL_BLOCKED_NXRA: // Blocked by external provider
case QUERY_GRAVITY_CNAME: // Blocked by gravity (inside CNAME path)
case QUERY_REGEX_CNAME: // Blocked by regex blacklist (inside CNAME path)
case QUERY_BLACKLIST_CNAME: // Blocked by exact blacklist (inside CNAME path)
counters->blocked++;
// Get domain pointer
domainsData* domain = getDomain(domainID, true);
domain->blockedcount++;
change_clientcount(client, 0, 1, -1, 0);
// Update overTime data structure
overTime[timeidx].blocked++;
break;
case QUERY_FORWARDED: // Forwarded
counters->forwarded++;
// Update overTime data structure
overTime[timeidx].forwarded++;
break;
case QUERY_CACHE: // Cached or local config
counters->cached++;
// Update overTime data structure
overTime[timeidx].cached++;
break;
case QUERY_RETRIED: // Retried query
case QUERY_RETRIED_DNSSEC: // fall through
// Nothing to be done here
break;
default:
logg("Warning: Found unknown status %i in long term database!", status);
break;
}
}
logg("Imported %i queries from the long-term database", counters->queries);
// Update lastdbindex so that the next call to DB_save_queries()
// skips the queries that we just imported from the database
lastdbindex = counters->queries;
if( rc != SQLITE_DONE ){
logg("DB_read_queries() - SQL error step: %s", sqlite3_errstr(rc));
dbclose();
return;
}
// Finalize SQLite3 statement
sqlite3_finalize(stmt);
// Close database here, we have to reopen it later (after forking)
dbclose();
}