Merge branch 'development-v6' into tweak/api_network_info
Build, Test, Deploy / smoke-tests (push) Has been cancelled
Codespell / spell-check (push) Has been cancelled
Check for merge conflicts / merge-conflict (push) Has been cancelled
API validation / Node (push) Has been cancelled
Build, Test, Deploy / gha (pihole-FTL-386, , linux/386) (push) Has been cancelled
Build, Test, Deploy / gha (pihole-FTL-amd64, , linux/amd64) (push) Has been cancelled
Build, Test, Deploy / gha (pihole-FTL-amd64-clang, clang, linux/amd64) (push) Has been cancelled
Build, Test, Deploy / gha (pihole-FTL-riscv64, , linux/riscv64) (push) Has been cancelled
Build, Test, Deploy / self-hosted (pihole-FTL-arm64, linux/arm64/v8) (push) Has been cancelled
Build, Test, Deploy / self-hosted (pihole-FTL-armv6, linux/arm/v6) (push) Has been cancelled
Build, Test, Deploy / self-hosted (pihole-FTL-armv7, linux/arm/v7) (push) Has been cancelled

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER
2024-07-19 09:47:18 +02:00
37 changed files with 718 additions and 473 deletions
+4 -4
View File
@@ -5,13 +5,13 @@ WORKDIR /app
COPY . /app
ARG CI_ARCH="linux/amd64"
ENV CI_ARCH ${CI_ARCH}
ENV CI_ARCH=${CI_ARCH}
ARG GIT_BRANCH="test"
ENV GIT_BRANCH ${GIT_BRANCH}
ENV GIT_BRANCH=${GIT_BRANCH}
ARG GIT_TAG="test"
ENV GIT_TAG ${GIT_TAG}
ENV GIT_TAG=${GIT_TAG}
ARG BUILD_OPTS=""
ENV BUILD_OPTS ${BUILD_OPTS}
ENV BUILD_OPTS=${BUILD_OPTS}
# Build FTL
# Remove possible old build files
+6
View File
@@ -33,6 +33,12 @@ int api_stats_upstreams(struct ftl_conn *api);
int api_stats_top_domains(struct ftl_conn *api);
int api_stats_top_clients(struct ftl_conn *api);
int api_stats_recentblocked(struct ftl_conn *api);
cJSON *get_top_domains(struct ftl_conn *api, const int count,
const bool blocked, const bool domains_only);
cJSON *get_top_clients(struct ftl_conn *api, const int count,
const bool blocked, const bool clients_only,
const bool names_only);
cJSON *get_top_upstreams(struct ftl_conn *api, const bool upstreams_only);
// History methods
int api_history(struct ftl_conn *api);
+1 -1
View File
@@ -100,7 +100,7 @@ static int set_blocking(struct ftl_conn *api)
// The blocking status does not need to be changed
// Delete a possibly running timer
set_blockingmode_timer(-1.0, true);
set_blockingmode_timer(timer, true);
log_debug(DEBUG_API, "No change in blocking mode, resetting timer");
}
+16 -13
View File
@@ -348,21 +348,23 @@ components:
sync:
type: object
properties:
active:
type: boolean
server:
type: string
interval:
type: integer
count:
type: integer
rtc:
type: object
properties:
set:
type: boolean
device:
type: string
utc:
type: boolean
rtc:
type: object
properties:
set:
type: boolean
device:
type: string
utc:
type: boolean
resolver:
type: object
properties:
@@ -708,13 +710,14 @@ components:
active: true
address: ""
sync:
active: true
server: "pool.ntp.org"
interval: 3600
count: 8
rtc:
set: true
device: ""
utc: true
rtc:
set: true
device: ""
utc: true
resolver:
resolveIPv4: true
resolveIPv6: true
+45 -44
View File
@@ -22,7 +22,8 @@
// dbopen(false, ), dbclose()
#include "database/common.h"
static int add_strings_to_array(struct ftl_conn *api, cJSON *array, const char *querystr, const int max_count)
#if 0
static int add_strings_to_array(struct ftl_conn *api, cJSON *array1, cJSON *array2, const char *querystr, const int max_count)
{
sqlite3 *memdb = get_memdb();
@@ -44,11 +45,24 @@ static int add_strings_to_array(struct ftl_conn *api, cJSON *array, const char *
sqlite3_errstr(rc));
}
// Loop through returned rows
// Loop through returned rows and add them to the array
int counter = 0;
while((rc = sqlite3_step(stmt)) == SQLITE_ROW &&
(max_count < 0 || ++counter < max_count))
JSON_COPY_STR_TO_ARRAY(array, (const char*)sqlite3_column_text(stmt, 0));
(max_count < 0 || ++counter <= max_count))
{
const char *array1_str = (const char*)sqlite3_column_text(stmt, 0);
if(array1_str != NULL && array1_str[0] != '\0')
// Only add non-empty strings
JSON_COPY_STR_TO_ARRAY(array1, array1_str);
if(array2 != NULL)
{
// We have a second array to fill (second column in the query)
const char *array2_str = (const char*)sqlite3_column_text(stmt, 1);
if(array2_str != NULL && array2_str[0] != '\0')
// Only add non-empty strings
JSON_COPY_STR_TO_ARRAY(array2, array2_str);
}
}
// Acceptable return codes are either
// - SQLITE_DONE: We read all lines, or
@@ -67,60 +81,47 @@ static int add_strings_to_array(struct ftl_conn *api, cJSON *array, const char *
return 0;
}
#endif
int api_queries_suggestions(struct ftl_conn *api)
{
int rc;
// Does the user request a custom number of records to be included?
int count = 30;
get_int_var(api->request->query_string, "count", &count);
// Get domains
cJSON *domain = JSON_NEW_ARRAY();
rc = add_strings_to_array(api, domain, "SELECT domain FROM domain_by_id", count);
if(rc != 0)
cJSON *domain = get_top_domains(api, count, false, true);
cJSON *blocked = get_top_domains(api, count, true, true);
// Add domains from both arrays, avoiding duplicates
cJSON *entry = NULL;
cJSON_ArrayForEach(entry, blocked)
{
log_err("Cannot read domains from database");
cJSON_Delete(domain);
return rc;
// Check if the domain is already in the list
bool found = false;
cJSON *entry2 = NULL;
cJSON_ArrayForEach(entry2, domain)
{
if(strcmp(cJSON_GetStringValue(entry), cJSON_GetStringValue(entry2)) == 0)
{
found = true;
break;
}
}
if(!found)
JSON_ADD_ITEM_TO_ARRAY(domain, cJSON_Duplicate(entry, true));
}
// Free the blocked list
cJSON_Delete(blocked);
// Get clients, both by IP and names
// We have to call DISTINCT() here as multiple IPs can map to and name and
// vice versa
cJSON *client_ip = JSON_NEW_ARRAY();
rc = add_strings_to_array(api, client_ip, "SELECT DISTINCT(ip) FROM client_by_id", count);
if(rc != 0)
{
log_err("Cannot read client IPs from database");
cJSON_Delete(domain);
cJSON_Delete(client_ip);
return rc;
}
cJSON *client_name = JSON_NEW_ARRAY();
rc = add_strings_to_array(api, client_name, "SELECT DISTINCT(name) FROM client_by_id", count);
if(rc != 0)
{
log_err("Cannot read client names from database");
cJSON_Delete(domain);
cJSON_Delete(client_ip);
cJSON_Delete(client_name);
return rc;
}
cJSON *client_ip = get_top_clients(api, count, false, true, false);
cJSON *client_name = get_top_clients(api, count, false, true, true);
// Delete duplicate entries from client_name
cJSON_unique_array(client_name);
// Get upstreams
cJSON *upstream = JSON_NEW_ARRAY();
rc = add_strings_to_array(api, upstream, "SELECT forward FROM forward_by_id", count);
if(rc != 0)
{
log_err("Cannot read forward from database");
cJSON_Delete(domain);
cJSON_Delete(client_ip);
cJSON_Delete(client_name);
cJSON_Delete(upstream);
return rc;
}
cJSON *upstream = get_top_upstreams(api, true);
// Get types
cJSON *type = JSON_NEW_ARRAY();
queriesData query = { 0 };
+336 -222
View File
@@ -14,8 +14,6 @@
#include "api/api.h"
#include "shmem.h"
#include "datastructure.h"
// read_setupVarsconf()
#include "config/setupVars.h"
// logging routines
#include "log.h"
// config struct
@@ -27,6 +25,17 @@
// sqrt()
#include <math.h>
struct top_entries {
int count;
unsigned int responses;
in_port_t port;
size_t namepos;
size_t ippos;
double rtime;
double rtuncertainty;
};
/* qsort comparison function (count field), sort ASC
static int __attribute__((pure)) cmpasc(const void *a, const void *b)
{
@@ -55,6 +64,20 @@ int __attribute__((pure)) cmpdesc(const void *a, const void *b)
return 0;
}
// qsort subroutine, sort DESC
static int __attribute__((pure)) cmpdesc_te(const void *a, const void *b)
{
const struct top_entries *elem1 = (struct top_entries*)a;
const struct top_entries *elem2 = (struct top_entries*)b;
if (elem1->count > elem2->count)
return -1;
else if (elem1->count < elem2->count)
return 1;
else
return 0;
}
static int get_query_types_obj(struct ftl_conn *api, cJSON *types)
{
for(unsigned int i = TYPE_A; i < TYPE_MAX; i++)
@@ -128,14 +151,18 @@ int api_stats_summary(struct ftl_conn *api)
cJSON *gravity = JSON_NEW_OBJECT();
JSON_ADD_NUMBER_TO_OBJECT(gravity, "domains_being_blocked", counters->database.gravity);
// Unlock shared memory
unlock_shm();
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "queries", queries);
JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
JSON_ADD_ITEM_TO_OBJECT(json, "gravity", gravity);
JSON_SEND_OBJECT_UNLOCK(json);
JSON_SEND_OBJECT(json);
}
int api_stats_top_domains(struct ftl_conn *api)
cJSON *get_top_domains(struct ftl_conn *api, const int count,
const bool blocked, const bool domains_only)
{
// Exit before processing any data if requested via config setting
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS)
@@ -145,24 +172,150 @@ int api_stats_top_domains(struct ftl_conn *api)
// Minimum structure is
// {"top_domains":[]}
cJSON *json = JSON_NEW_OBJECT();
cJSON *top_domains = JSON_NEW_ARRAY();
JSON_ADD_ITEM_TO_OBJECT(json, "top_domains", top_domains);
JSON_SEND_OBJECT(json);
if(domains_only)
return cJSON_CreateArray();
cJSON *json = cJSON_CreateObject();
cJSON_AddItemToObject(json, "domains", cJSON_CreateArray());
cJSON_AddNumberToObject(json, "total_queries", -1);
cJSON_AddNumberToObject(json, "blocked_queries", -1);
return json;
}
// Get domains which the user doesn't want to see
regex_t *regex_domains = NULL;
unsigned int N_regex_domains = 0;
compile_filter_regex(api, "webserver.api.excludeDomains",
config.webserver.api.excludeDomains.v.json,
&regex_domains, &N_regex_domains);
// Lock shared memory
lock_shm();
// Allocate memory
const int domains = counters->domains;
int *temparray = calloc(2*domains, sizeof(int));
if(temparray == NULL)
const int total_queries = counters->queries;
const int blocked_count = get_blocked_count();
struct top_entries *top_domains = calloc(domains, sizeof(struct top_entries));
if(top_domains == NULL)
{
log_err("Memory allocation failed in %s()", __FUNCTION__);
return 0;
return NULL;
}
unsigned int added_domains = 0u;
for(int domainID = 0; domainID < domains; domainID++)
{
// Get domain pointer
const domainsData* domain = getDomain(domainID, true);
if(domain == NULL)
continue;
const char *domain_name = getstr(domain->domainpos);
// Hidden domain, probably due to privacy level. Skip this in the top lists
if(strcmp(domain_name, HIDDEN_DOMAIN) == 0)
continue;
// Use either blocked or total count based on request string
top_domains[added_domains].count = blocked ? domain->blockedcount : domain->count - domain->blockedcount;
// Get domain name
top_domains[added_domains].namepos = domain->domainpos;
// Increment counter
added_domains++;
}
// Unlock shared memory
unlock_shm();
// Sort temporary array
qsort(top_domains, added_domains, sizeof(*top_domains), cmpdesc_te);
int n = 0;
cJSON *jtop_domains = cJSON_CreateArray();
// Lock shared memory
lock_shm();
for(unsigned int i = 0; i < added_domains; i++)
{
// Skip e.g. recycled domains
if(top_domains[i].namepos == 0)
continue;
const char *domain = getstr(top_domains[i].namepos);
// Skip this client if there is a filter on it
bool skip_domain = false;
if(N_regex_domains > 0)
{
// Iterate over all regex filters
for(unsigned int j = 0; j < N_regex_domains; j++)
{
// Check if the domain matches the regex
if(regexec(&regex_domains[j], domain, 0, NULL, 0) == 0)
{
// Domain matches
skip_domain = true;
break;
}
}
}
if(skip_domain || top_domains[i].count < 1)
continue;
if(domains_only)
{
cJSON_AddStringToArray(jtop_domains, domain);
}
else
{
cJSON *domain_item = cJSON_CreateObject();
cJSON_AddStringToObject(domain_item, "domain", domain);
cJSON_AddNumberToObject(domain_item, "count", top_domains[i].count);
cJSON_AddItemToArray(jtop_domains, domain_item);
}
// Only count entries that are actually sent and return when we have send enough data
if(++n >= count)
break;
}
// Unlock shared memory
unlock_shm();
// Free temporary array
free(top_domains);
// Free regexes
if(N_regex_domains > 0)
{
// Free individual regexes
for(unsigned int i = 0; i < N_regex_domains; i++)
regfree(&regex_domains[i]);
// Free array of regex pointers
free(regex_domains);
}
if(domains_only)
{
// Return the array of domains only
return jtop_domains;
}
// else: Build and return full object
cJSON *json = cJSON_CreateObject();
cJSON_AddItemToObject(json, "domains", jtop_domains);
cJSON_AddNumberToObject(json, "total_queries", total_queries);
cJSON_AddNumberToObject(json, "blocked_queries", blocked_count);
return json;
}
int api_stats_top_domains(struct ftl_conn *api)
{
bool blocked = false; // Can be overwritten by query string
int count = 10;
// /api/stats/top_domains?blocked=true
@@ -176,121 +329,14 @@ int api_stats_top_domains(struct ftl_conn *api)
get_int_var(api->request->query_string, "count", &count);
}
unsigned int added_domains = 0u;
for(int domainID = 0; domainID < domains; domainID++)
{
// Get domain pointer
const domainsData* domain = getDomain(domainID, true);
if(domain == NULL)
continue;
// Add domain ID
temparray[2*added_domains + 0] = domainID;
// Use either blocked or total count based on request string
temparray[2*added_domains + 1] = blocked ? domain->blockedcount : domain->count - domain->blockedcount;
added_domains++;
}
// Sort temporary array
qsort(temparray, added_domains, sizeof(int[2]), cmpdesc);
// Get domains which the user doesn't want to see
regex_t *regex_domains = NULL;
unsigned int N_regex_domains = 0;
compile_filter_regex(api, "webserver.api.excludeDomains",
config.webserver.api.excludeDomains.v.json,
&regex_domains, &N_regex_domains);
int n = 0;
cJSON *top_domains = JSON_NEW_ARRAY();
for(unsigned int i = 0; i < added_domains; i++)
{
// Get sorted index
const int domainID = temparray[2*i + 0];
// Get domain pointer
const domainsData* domain = getDomain(domainID, true);
if(domain == NULL)
continue;
// Get domain name
const char *domain_name = getstr(domain->domainpos);
// Hidden domain, probably due to privacy level. Skip this in the top lists
if(strcmp(domain_name, HIDDEN_DOMAIN) == 0)
continue;
// Skip this client if there is a filter on it
bool skip_domain = false;
if(N_regex_domains > 0)
{
// Iterate over all regex filters
for(unsigned int j = 0; j < N_regex_domains; j++)
{
// Check if the domain matches the regex
if(regexec(&regex_domains[j], domain_name, 0, NULL, 0) == 0)
{
// Domain matches
skip_domain = true;
break;
}
}
}
if(skip_domain)
continue;
int domain_count = -1;
if(blocked && domain->blockedcount > 0)
{
domain_count = domain->blockedcount;
n++;
}
else if(!blocked && (domain->count - domain->blockedcount) > 0)
{
domain_count = domain->count - domain->blockedcount;
n++;
}
if(domain_count > -1)
{
cJSON *domain_item = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(domain_item, "domain", domain_name);
JSON_ADD_NUMBER_TO_OBJECT(domain_item, "count", domain_count);
JSON_ADD_ITEM_TO_ARRAY(top_domains, domain_item);
}
// Only count entries that are actually sent and return when we have send enough data
if(n >= count)
break;
}
free(temparray);
// Free regexes
if(N_regex_domains > 0)
{
// Free individual regexes
for(unsigned int i = 0; i < N_regex_domains; i++)
regfree(&regex_domains[i]);
// Free array of regex pointers
free(regex_domains);
}
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "domains", top_domains);
const int blocked_count = get_blocked_count();
JSON_ADD_NUMBER_TO_OBJECT(json, "total_queries", counters->queries);
JSON_ADD_NUMBER_TO_OBJECT(json, "blocked_queries", blocked_count);
JSON_SEND_OBJECT_UNLOCK(json);
cJSON *json = get_top_domains(api, count, blocked, false);
JSON_SEND_OBJECT(json);
}
int api_stats_top_clients(struct ftl_conn *api)
cJSON *get_top_clients(struct ftl_conn *api, const int count,
const bool blocked, const bool clients_only,
const bool names_only)
{
int count = 10;
// Exit before processing any data if requested via config setting
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS_CLIENTS)
{
@@ -299,31 +345,26 @@ int api_stats_top_clients(struct ftl_conn *api)
// Minimum structure is
// {"top_clients":[]}
cJSON *json = JSON_NEW_OBJECT();
cJSON *top_clients = JSON_NEW_ARRAY();
JSON_ADD_ITEM_TO_OBJECT(json, "top_clients", top_clients);
JSON_SEND_OBJECT(json);
}
if(clients_only)
return cJSON_CreateArray();
bool blocked = false; // /api/stats/top_clients?blocked=true
if(api->request->query_string != NULL)
{
// Should blocked clients be shown?
get_bool_var(api->request->query_string, "blocked", &blocked);
// Does the user request a non-default number of replies?
// Note: We do not accept zero query requests here
get_int_var(api->request->query_string, "count", &count);
cJSON *json = cJSON_CreateObject();
cJSON_AddItemToObject(json, "clients", cJSON_CreateArray());
cJSON_AddNumberToObject(json, "total_queries", -1);
cJSON_AddNumberToObject(json, "blocked_queries", -1);
return json;
}
// Lock shared memory
lock_shm();
int clients = counters->clients;
int *temparray = calloc(2*clients, sizeof(int));
if(temparray == NULL)
const int total_queries = counters->queries;
const int blocked_count = get_blocked_count();
struct top_entries *top_clients = calloc(clients, sizeof(struct top_entries));
if(top_clients == NULL)
{
log_err("Memory allocation failed in api_stats_top_clients()");
log_err("Memory allocation failed in %s()", __FUNCTION__);
return 0;
}
@@ -337,15 +378,26 @@ int api_stats_top_clients(struct ftl_conn *api)
if(client == NULL || (!client->flags.aliasclient && client->aliasclient_id >= 0))
continue;
temparray[2*added_clients + 0] = clientID;
const char *client_ip = getstr(client->ippos);
// Hidden client, probably due to privacy level. Skip this in the top lists
if(strcmp(client_ip, HIDDEN_CLIENT) == 0)
continue;
// Use either blocked or total count based on request string
temparray[2*added_clients + 1] = blocked ? client->blockedcount : client->count;
top_clients[added_clients].count = blocked ? client->blockedcount : client->count;
// Get client name and IP
top_clients[added_clients].ippos = client->ippos;
top_clients[added_clients].namepos = client->namepos;
added_clients++;
}
// Unlock shared memory
unlock_shm();
// Sort temporary array
qsort(temparray, added_clients, sizeof(int[2]), cmpdesc);
qsort(top_clients, added_clients, sizeof(*top_clients), cmpdesc_te);
// Get clients which the user doesn't want to see
regex_t *regex_clients = NULL;
@@ -355,24 +407,19 @@ int api_stats_top_clients(struct ftl_conn *api)
&regex_clients, &N_regex_clients);
int n = 0;
cJSON *top_clients = JSON_NEW_ARRAY();
cJSON *jtop_clients = JSON_NEW_ARRAY();
// Lock shared memory
lock_shm();
for(unsigned int i = 0; i < added_clients; i++)
{
// Get sorted indices and counter values (may be either total or blocked count)
const int clientID = temparray[2*i + 0];
const int client_count = temparray[2*i + 1];
// Get client pointer
const clientsData* client = getClient(clientID, true);
if(client == NULL)
// Skip e.g. recycled clients
if(top_clients[i].namepos == 0)
continue;
// Get IP and host name of client
const char *client_ip = getstr(client->ippos);
const char *client_name = getstr(client->namepos);
// Hidden client, probably due to privacy level. Skip this in the top lists
if(strcmp(client_ip, HIDDEN_CLIENT) == 0)
continue;
const char *client_ip = getstr(top_clients[i].ippos);
const char *client_name = getstr(top_clients[i].namepos);
// Skip this client if there is a filter on it
bool skip_client = false;
@@ -397,26 +444,37 @@ int api_stats_top_clients(struct ftl_conn *api)
}
}
if(skip_client)
if(skip_client || top_clients[i].count < 1)
continue;
// Return this client if the client made at least one query
// within the most recent 24 hours
if(client_count > 0)
if(clients_only)
{
cJSON *client_item = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(client_item, "name", client_name);
JSON_REF_STR_IN_OBJECT(client_item, "ip", client_ip);
JSON_ADD_NUMBER_TO_OBJECT(client_item, "count", client_count);
JSON_ADD_ITEM_TO_ARRAY(top_clients, client_item);
n++;
if(names_only)
{
if(strlen(client_name) > 0)
cJSON_AddStringToArray(jtop_clients, client_name);
}
else
cJSON_AddStringToArray(jtop_clients, client_ip);
}
else
{
cJSON *client_item = cJSON_CreateObject();
cJSON_AddStringToObject(client_item, "name", client_name);
cJSON_AddStringToObject(client_item, "ip", client_ip);
cJSON_AddNumberToObject(client_item, "count", top_clients[i].count);
cJSON_AddItemToArray(jtop_clients, client_item);
}
if(n == count)
if(++n == count)
break;
}
// Unlock shared memory
unlock_shm();
// Free temporary array
free(temparray);
free(top_clients);
// Free regexes
if(N_regex_clients > 0)
@@ -429,21 +487,46 @@ int api_stats_top_clients(struct ftl_conn *api)
free(regex_clients);
}
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "clients", top_clients);
if(clients_only)
{
// Return the array of clients only
return jtop_clients;
}
const int blocked_count = get_blocked_count();
JSON_ADD_NUMBER_TO_OBJECT(json, "blocked_queries", blocked_count);
JSON_ADD_NUMBER_TO_OBJECT(json, "total_queries", counters->queries);
JSON_SEND_OBJECT_UNLOCK(json);
// else: Build and return full object
cJSON *json = cJSON_CreateObject();
cJSON_AddItemToObject(json, "clients", jtop_clients);
cJSON_AddNumberToObject(json, "total_queries", total_queries);
cJSON_AddNumberToObject(json, "blocked_queries", blocked_count);
return json;
}
int api_stats_top_clients(struct ftl_conn *api)
{
bool blocked = false; // Can be overwritten by query string
int count = 10;
// /api/stats/top_clients?blocked=true
if(api->request->query_string != NULL)
{
// Should blocked clients be shown?
get_bool_var(api->request->query_string, "blocked", &blocked);
int api_stats_upstreams(struct ftl_conn *api)
// Does the user request a non-default number of replies?
// Note: We do not accept zero query requests here
get_int_var(api->request->query_string, "count", &count);
}
cJSON *json = get_top_clients(api, count, blocked, false, false);
JSON_SEND_OBJECT(json);
}
cJSON *get_top_upstreams(struct ftl_conn *api, const bool upstreams_only)
{
const int upstreams = counters->upstreams;
int *temparray = calloc(2*upstreams, sizeof(int));
if(temparray == NULL)
const int forwarded_count = get_forwarded_count();
const int total_queries = counters->queries;
struct top_entries *top_upstreams = calloc(upstreams, sizeof(struct top_entries));
if(top_upstreams == NULL)
{
log_err("Memory allocation failed in api_stats_upstreams()");
return 0;
@@ -460,22 +543,34 @@ int api_stats_upstreams(struct ftl_conn *api)
if(upstream == NULL)
continue;
temparray[2*added_upstreams + 0] = upstreamID;
temparray[2*added_upstreams + 1] = upstream->count;
top_upstreams[added_upstreams].count = upstream->count;
top_upstreams[added_upstreams].ippos = upstream->ippos;
top_upstreams[added_upstreams].namepos = upstream->namepos;
top_upstreams[added_upstreams].port = upstream->port;
top_upstreams[added_upstreams].responses = upstream->responses;
top_upstreams[added_upstreams].rtime = upstream->rtime;
top_upstreams[added_upstreams].rtuncertainty = upstream->rtuncertainty;
added_upstreams++;
}
// Unlock shared memory
unlock_shm();
// Sort temporary array in descending order
qsort(temparray, upstreams, sizeof(int[2]), cmpdesc);
qsort(top_upstreams, added_upstreams, sizeof(*top_upstreams), cmpdesc);
// Loop over available forward destinations
cJSON *top_upstreams = JSON_NEW_ARRAY();
cJSON *jtop_upstreams = JSON_NEW_ARRAY();
// Lock shared memory
lock_shm();
for(int i = -2; i < (int)added_upstreams; i++)
{
int count = 0;
const char* ip, *name;
int port = -1;
in_port_t port = -1;
double responsetime = 0.0, uncertainty = 0.0;
if(i == -2)
@@ -495,67 +590,80 @@ int api_stats_upstreams(struct ftl_conn *api)
else
{
// Regular upstream destination
// Get sorted indices
const int upstreamID = temparray[2*i + 0];
// Get upstream pointer
const upstreamsData *upstream = getUpstream(upstreamID, true);
if(upstream == NULL)
continue;
// Get IP and host name of upstream destination if available
ip = getstr(upstream->ippos);
name = getstr(upstream->namepos);
port = upstream->port;
// Get percentage
count = upstream->count;
ip = getstr(top_upstreams[i].ippos);
name = getstr(top_upstreams[i].namepos);
port = top_upstreams[i].port;
count = top_upstreams[i].count;
// Compute average response time and uncertainty (unit: seconds)
if(upstream->responses > 0)
if(top_upstreams[i].responses > 0)
{
// Simple average of the response times
responsetime = upstream->rtime / upstream->responses;
responsetime = top_upstreams[i].rtime / top_upstreams[i].responses;
}
if(upstream->responses > 1)
if(top_upstreams[i].responses > 1)
{
// The actual value will be somewhere in a neighborhood around the mean value.
// This neighborhood of values is the uncertainty in the mean.
uncertainty = sqrt(upstream->rtuncertainty / upstream->responses / (upstream->responses-1));
uncertainty = sqrt(top_upstreams[i].rtuncertainty / top_upstreams[i].responses / (top_upstreams[i].responses-1));
}
}
// Send data:
// - always if i < 0 (special upstreams: blocklist and cache)
// - only if there are any queries for all others (i > 0)
if(count > 0 || i < 0)
if(count < 1 && i >= 0)
continue;
if(upstreams_only)
{
cJSON_AddStringToArray(jtop_upstreams, name);
}
else
{
cJSON *upstream = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(upstream, "ip", ip);
JSON_REF_STR_IN_OBJECT(upstream, "name", name);
JSON_ADD_NUMBER_TO_OBJECT(upstream, "port", port);
JSON_ADD_NUMBER_TO_OBJECT(upstream, "count", count);
cJSON_AddStringToObject(upstream, "ip", ip);
cJSON_AddStringToObject(upstream, "name", name);
cJSON_AddNumberToObject(upstream, "port", port);
cJSON_AddNumberToObject(upstream, "count", count);
cJSON *statistics = JSON_NEW_OBJECT();
JSON_ADD_NUMBER_TO_OBJECT(statistics, "response", responsetime);
JSON_ADD_NUMBER_TO_OBJECT(statistics, "variance", uncertainty);
JSON_ADD_ITEM_TO_OBJECT(upstream, "statistics", statistics);
JSON_ADD_ITEM_TO_ARRAY(top_upstreams, upstream);
cJSON_AddNumberToObject(statistics, "response", responsetime);
cJSON_AddNumberToObject(statistics, "variance", uncertainty);
cJSON_AddItemToObject(upstream, "statistics", statistics);
cJSON_AddItemToArray(jtop_upstreams, upstream);
}
}
// Free temporary array
free(temparray);
// Unlock shared memory
unlock_shm();
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "upstreams", top_upstreams);
const int forwarded_count = get_forwarded_count();
JSON_ADD_NUMBER_TO_OBJECT(json, "forwarded_queries", forwarded_count);
JSON_ADD_NUMBER_TO_OBJECT(json, "total_queries", counters->queries);
JSON_SEND_OBJECT_UNLOCK(json);
// Free temporary array
free(top_upstreams);
if(upstreams_only)
{
// Return the array of upstreams only
return jtop_upstreams;
}
// else: Build and return full object
cJSON *json = cJSON_CreateObject();
cJSON_AddItemToObject(json, "upstreams", jtop_upstreams);
cJSON_AddNumberToObject(json, "total_queries", total_queries);
cJSON_AddNumberToObject(json, "forwarded_queries", forwarded_count);
return json;
}
int api_stats_upstreams(struct ftl_conn *api)
{
cJSON *json = get_top_upstreams(api, false);
JSON_SEND_OBJECT(json);
}
int api_stats_query_types(struct ftl_conn *api)
{
// Lock shared memory
lock_shm();
cJSON *types = JSON_NEW_OBJECT();
@@ -566,11 +674,14 @@ int api_stats_query_types(struct ftl_conn *api)
return ret;
}
// Unlock shared memory
unlock_shm();
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "types", types);
// Send response
JSON_SEND_OBJECT_UNLOCK(json);
JSON_SEND_OBJECT(json);
}
int api_stats_recentblocked(struct ftl_conn *api)
@@ -624,7 +735,10 @@ int api_stats_recentblocked(struct ftl_conn *api)
break;
}
// Unlock shared memory
unlock_shm();
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "blocked", blocked);
JSON_SEND_OBJECT_UNLOCK(json);
JSON_SEND_OBJECT(json);
}
+23 -16
View File
@@ -814,6 +814,13 @@ void initConfig(struct config *conf)
memset(&conf->ntp.ipv6.address.d.in6_addr, 0, sizeof(struct in6_addr));
conf->ntp.ipv6.address.c = validate_stub; // Only type-based checking
conf->ntp.sync.active.k = "ntp.sync.active";
conf->ntp.sync.active.h = "Should FTL try to synchronize the system time with an upstream NTP server?";
conf->ntp.sync.active.t = CONF_BOOL;
conf->ntp.sync.active.f = FLAG_RESTART_FTL;
conf->ntp.sync.active.d.b = true;
conf->ntp.sync.active.c = validate_stub; // Only type-based checking
conf->ntp.sync.server.k = "ntp.sync.server";
conf->ntp.sync.server.h = "NTP upstream server to sync with, e.g., \"pool.ntp.org\". Note that the NTP server should be located as close as possible to you in order to minimize the time offset possibly introduced by different routing paths.";
conf->ntp.sync.server.a = cJSON_CreateStringReference("valid NTP upstream server");
@@ -833,24 +840,24 @@ void initConfig(struct config *conf)
conf->ntp.sync.count.d.ui = 8;
conf->ntp.sync.count.c = validate_stub; // Only type-based checking
conf->ntp.rtc.set.k = "ntp.rtc.set";
conf->ntp.rtc.set.h = "Should FTL update a real-time clock (RTC) if available?";
conf->ntp.rtc.set.t = CONF_BOOL;
conf->ntp.rtc.set.d.b = true;
conf->ntp.rtc.set.c = validate_stub; // Only type-based checking
conf->ntp.sync.rtc.set.k = "ntp.sync.rtc.set";
conf->ntp.sync.rtc.set.h = "Should FTL update a real-time clock (RTC) if available?";
conf->ntp.sync.rtc.set.t = CONF_BOOL;
conf->ntp.sync.rtc.set.d.b = true;
conf->ntp.sync.rtc.set.c = validate_stub; // Only type-based checking
conf->ntp.rtc.device.k = "ntp.rtc.device";
conf->ntp.rtc.device.h = "Path to the RTC device to update. Leave empty for auto-discovery";
conf->ntp.rtc.device.a = cJSON_CreateStringReference("Path to the RTC device, e.g., \"/dev/rtc0\"");
conf->ntp.rtc.device.t = CONF_STRING;
conf->ntp.rtc.device.d.s = (char*)"";
conf->ntp.rtc.device.c = validate_stub; // Only type-based checking
conf->ntp.sync.rtc.device.k = "ntp.sync.rtc.device";
conf->ntp.sync.rtc.device.h = "Path to the RTC device to update. Leave empty for auto-discovery";
conf->ntp.sync.rtc.device.a = cJSON_CreateStringReference("Path to the RTC device, e.g., \"/dev/rtc0\"");
conf->ntp.sync.rtc.device.t = CONF_STRING;
conf->ntp.sync.rtc.device.d.s = (char*)"";
conf->ntp.sync.rtc.device.c = validate_stub; // Only type-based checking
conf->ntp.rtc.utc.k = "ntp.rtc.utc";
conf->ntp.rtc.utc.h = "Should the RTC be set to UTC?";
conf->ntp.rtc.utc.t = CONF_BOOL;
conf->ntp.rtc.utc.d.b = true;
conf->ntp.rtc.utc.c = validate_stub; // Only type-based checking
conf->ntp.sync.rtc.utc.k = "ntp.sync.rtc.utc";
conf->ntp.sync.rtc.utc.h = "Should the RTC be set to UTC?";
conf->ntp.sync.rtc.utc.t = CONF_BOOL;
conf->ntp.sync.rtc.utc.d.b = true;
conf->ntp.sync.rtc.utc.c = validate_stub; // Only type-based checking
// struct resolver
+6 -5
View File
@@ -201,15 +201,16 @@ struct config {
struct conf_item address;
} ipv6;
struct {
struct conf_item active;
struct conf_item server;
struct conf_item interval;
struct conf_item count;
struct {
struct conf_item set;
struct conf_item device;
struct conf_item utc;
} rtc;
} sync;
struct {
struct conf_item set;
struct conf_item device;
struct conf_item utc;
} rtc;
} ntp;
struct {
+6
View File
@@ -732,6 +732,12 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
{
log_warn("New dnsmasq configuration is not valid (%s), config remains unchanged", errbuf);
if(debug_flags[DEBUG_ANY])
{
log_debug(DEBUG_ANY, "Temporary dnsmasq config file left in place for debugging purposes");
return false;
}
// Remove temporary config file
if(remove(DNSMASQ_TEMP_CONF) != 0)
{
+4 -20
View File
@@ -23,6 +23,8 @@
#include <limits.h>
// escape_json()
#include "webserver/http-common.h"
// chown_pihole()
#include "files.h"
// Open the TOML file for reading or writing
FILE * __attribute((malloc)) __attribute((nonnull(1))) openFTLtoml(const char *mode, const unsigned int version)
@@ -96,26 +98,8 @@ void closeFTLtoml(FILE *fp)
// Chown file if we are root
if(geteuid() == 0)
{
// Get UID and GID of user with name "pihole"
struct passwd *pwd = getpwnam("pihole");
if(pwd == NULL)
{
log_warn("Cannot get UID and GID of user pihole: %s", strerror(errno));
}
else
{
const uid_t pihole_uid = pwd->pw_uid;
const gid_t pihole_gid = pwd->pw_gid;
// Chown file to pihole user
if(chown(GLOBALTOMLPATH, pihole_uid, pihole_gid) != 0)
log_warn("Cannot chown "GLOBALTOMLPATH" to pihole:pihole (%u:%u): %s",
(unsigned int)pihole_uid, (unsigned int)pihole_gid, strerror(errno));
else
log_debug(DEBUG_CONFIG, "Chown-ed "GLOBALTOMLPATH" to pihole:pihole (%u:%u)",
(unsigned int)pihole_uid, (unsigned int)pihole_gid);
}
}
chown_pihole(GLOBALTOMLPATH, NULL);
return;
}
+26 -7
View File
@@ -330,13 +330,32 @@ void set_nice(void)
// Set nice value
const int ret = setpriority(which, pid, config.misc.nice.v.i);
if(ret == -1)
// ERROR EPERM: The calling process attempted to increase its priority
// by supplying a negative value but has insufficient privileges.
// On Linux, the RLIMIT_NICE resource limit can be used to define a limit to
// which an unprivileged process's nice value can be raised. We are not
// affected by this limit when pihole-FTL is running with CAP_SYS_NICE
log_warn("Cannot set process priority to %d: %s. Process priority remains at %d",
config.misc.nice.v.i, strerror(errno), priority);
{
if(errno == EACCES || errno == EPERM)
{
// from man 2 setpriority:
//
// ERRORS
// [...]
// EACCES The caller attempted to set a lower nice value (i.e., a higher
// process priority), but did not have the required privilege (on
// Linux: did not have the CAP_SYS_NICE capability).
//
// EPERM A process was located, but its effective user ID did not match
// either the effective or the real user ID of the caller, and was
// not privileged (on Linux: did not have the CAP_SYS_NICE capabil‐
// ity).
// [...]
log_warn("Insufficient permissions to set process priority to %d (CAP_SYS_NICE required), process priority remains at %d",
config.misc.nice.v.i, priority);
}
else
{
// Other error
log_warn("Cannot set process priority to %d: %s. Process priority remains at %d",
config.misc.nice.v.i, strerror(errno), priority);
}
}
}
}
-2
View File
@@ -83,7 +83,6 @@ static bool analyze_database(sqlite3 *db)
void *DB_thread(void *val)
{
// Set thread name
thread_running[DB] = true;
prctl(PR_SET_NAME, thread_names[DB], 0, 0, 0);
// Save timestamp as we do not want to store immediately
@@ -241,6 +240,5 @@ void *DB_thread(void *val)
dbclose(&db);
log_info("Terminating database thread");
thread_running[DB] = false;
return NULL;
}
+4 -4
View File
@@ -293,6 +293,10 @@ static int _add_message(const enum message_type type,
static int _add_message(const enum message_type type,
const char *message, const size_t count,...)
{
// Log to database only if not in CLI mode
if(cli_mode)
return -1;
int rowid = -1;
// Return early if database is known to be broken
if(FTLDBerror())
@@ -1237,10 +1241,6 @@ void logg_regex_warning(const char *type, const char *warning, const int dbindex
// Log to FTL.log
log_warn("%s", buf);
// Log to database only if not in CLI mode
if(cli_mode)
return;
// Add to database
add_message(REGEX_MESSAGE, regex, type, warning, dbindex);
}
+6 -1
View File
@@ -41,6 +41,9 @@ static sqlite3_stmt **stmts[] = { &query_stmt,
&forward_stmt,
&addinfo_stmt };
// Private prototypes
static void load_queries_from_disk(void);
// Return the maximum ID of the in-memory database
unsigned long __attribute__((pure)) get_max_db_idx(void)
{
@@ -220,6 +223,8 @@ bool init_memory_database(void)
return false;
}
load_queries_from_disk();
// Everything went well
return true;
}
@@ -1635,7 +1640,7 @@ bool queries_to_database(void)
return true;
}
void load_queries_from_disk(void)
static void load_queries_from_disk(void)
{
// Compensate for possible jumps in time
runGC(time(NULL), NULL, false);
-1
View File
@@ -119,7 +119,6 @@ bool add_additional_info_column(sqlite3 *db);
void DB_read_queries(void);
void update_disk_db_idx(void);
bool queries_to_database(void);
void load_queries_from_disk(void);
bool optimize_queries_table(sqlite3 *db);
bool create_addinfo_table(sqlite3 *db);
+4 -5
View File
@@ -1193,10 +1193,10 @@ static char *domain_rev4(int from_file, char *server, struct in_addr *addr4, int
return _("error");
}
if (sdetails.orig_hostinfo)
freeaddrinfo(sdetails.orig_hostinfo);
}
}
if (sdetails.orig_hostinfo)
freeaddrinfo(sdetails.orig_hostinfo);
return NULL;
}
@@ -1280,11 +1280,10 @@ static char *domain_rev6(int from_file, char *server, struct in6_addr *addr6, in
if (!add_update_server(flags, &serv_addr, &source_addr, interface, domain, NULL))
return _("error");
}
if (sdetails.orig_hostinfo)
freeaddrinfo(sdetails.orig_hostinfo);
}
}
if (sdetails.orig_hostinfo)
freeaddrinfo(sdetails.orig_hostinfo);
return NULL;
}
+9 -30
View File
@@ -1803,7 +1803,7 @@ void FTL_dnsmasq_reload(void)
// This function is called by the dnsmasq code on receive of SIGHUP
// *before* clearing the cache and re-reading the lists
if(reload++ > 0)
log_info("Received SIGHUP, flushing cache and re-reading config");
log_info("Flushing cache and re-reading config");
// Gravity database updates
// - (Re-)open gravity database connection
@@ -2912,17 +2912,12 @@ void FTL_fork_and_bind_sockets(struct passwd *ent_pw, bool dnsmasq_start)
// so they will not listen to real-time signals
handle_realtime_signals();
// We will use the attributes object later to start all threads in
// detached mode
pthread_attr_t attr;
// Initialize thread attributes object with default attribute values
// Do NOT detach threads as we want to join them during shutdown with a
// fixed timeout to give them time to clean up and finish their work
pthread_attr_t attr;
pthread_attr_init(&attr);
// Initialize NTP server
ntp_server_start(&attr);
// Start NTP sync thread
ntp_start_sync_thread(&attr);
@@ -2968,26 +2963,16 @@ void FTL_fork_and_bind_sockets(struct passwd *ent_pw, bool dnsmasq_start)
// we're actually dropping root (user/group may be set to root)
if(ent_pw != NULL && ent_pw->pw_uid != 0)
{
log_info("FTL is going to drop from root to user %s (UID %u)",
ent_pw->pw_name, ent_pw->pw_uid);
log_info("FTL is going to drop from root to user pihole");
// Change ownership of shared memory objects
chown_all_shmem(ent_pw);
// Configured FTL log file
if(chown(config.files.log.ftl.v.s, ent_pw->pw_uid, ent_pw->pw_gid) == -1)
{
log_warn("Setting ownership (%u:%u) of %s failed: %s (%i)",
ent_pw->pw_uid, ent_pw->pw_gid, config.files.log.ftl.v.s, strerror(errno), errno);
}
chown_pihole(config.files.log.ftl.v.s, ent_pw);
// Configured FTL database file
if(chown(config.files.database.v.s, ent_pw->pw_uid, ent_pw->pw_gid) == -1)
{
log_warn("Setting ownership (%u:%u) of %s failed: %s (%i)",
ent_pw->pw_uid, ent_pw->pw_gid, config.files.database.v.s, strerror(errno), errno);
}
chown_pihole(config.files.database.v.s, ent_pw);
// Check if auxiliary files exist and change ownership
char *extrafile = calloc(strlen(config.files.database.v.s) + 5, sizeof(char));
@@ -3000,20 +2985,14 @@ void FTL_fork_and_bind_sockets(struct passwd *ent_pw, bool dnsmasq_start)
// Check <database>-wal file (write-ahead log)
strcpy(extrafile, config.files.database.v.s);
strcat(extrafile, "-wal");
if(file_exists(extrafile) && chown(extrafile, ent_pw->pw_uid, ent_pw->pw_gid) == -1)
{
log_warn("Setting ownership (%u:%u) of %s failed: %s (%i)",
ent_pw->pw_uid, ent_pw->pw_gid, extrafile, strerror(errno), errno);
}
if(file_exists(extrafile))
chown_pihole(extrafile, ent_pw);
// Check <database>-shm file (mmapped shared memory)
strcpy(extrafile, config.files.database.v.s);
strcat(extrafile, "-shm");
if(file_exists(extrafile) && chown(extrafile, ent_pw->pw_uid, ent_pw->pw_gid) == -1)
{
log_warn("Setting ownership (%u:%u) of %s failed: %s (%i)",
ent_pw->pw_uid, ent_pw->pw_gid, extrafile, strerror(errno), errno);
}
if(file_exists(extrafile))
chown_pihole(extrafile, ent_pw);
// Free allocated memory
free(extrafile);
+3 -1
View File
@@ -250,7 +250,9 @@ enum thread_types {
GC,
DNSclient,
TIMER,
NTP,
NTP_CLIENT,
NTP_SERVER4,
NTP_SERVER6,
THREADS_MAX
} __attribute__ ((packed));
+22 -18
View File
@@ -16,8 +16,6 @@
// opendir(), readdir()
#include <dirent.h>
// getpwuid()
#include <pwd.h>
// getgrgid()
#include <grp.h>
// NAME_MAX
@@ -434,29 +432,35 @@ static int copy_file(const char *source, const char *destination)
}
// Change ownership of file to pihole user
static bool chown_pihole(const char *path)
bool chown_pihole(const char *path, struct passwd *pwd)
{
// Get pihole user's uid and gid
struct passwd *pwd = getpwnam("pihole");
// Get pihole user's UID and GID if not provided
if(pwd == NULL)
{
log_warn("chown_pihole(): Failed to get pihole user's uid: %s", strerror(errno));
return false;
pwd = getpwnam("pihole");
if(pwd == NULL)
{
log_warn("chown_pihole(): Failed to get pihole user's UID/GID: %s", strerror(errno));
return false;
}
}
struct group *grp = getgrnam("pihole");
if(grp == NULL)
// Get group name
struct group *grp = getgrgid(pwd->pw_gid);
const char *grp_name = grp != NULL ? grp->gr_name : "<unknown>";
// Change ownership of file to pihole user
if(chown(path, pwd->pw_uid, pwd->pw_gid) < 0)
{
log_warn("chown_pihole(): Failed to get pihole user's gid: %s", strerror(errno));
log_warn("Failed to change ownership of \"%s\" to %s:%s (%u:%u): %s",
path, pwd->pw_name, grp_name, pwd->pw_uid, pwd->pw_gid,
errno == EPERM ? "Insufficient permissions (CAP_CHOWN required)" : strerror(errno));
return false;
}
// Change ownership of file to pihole user
if(chown(path, pwd->pw_uid, grp->gr_gid) < 0)
{
log_warn("chown_pihole(): Failed to change ownership of \"%s\" to %u:%u: %s",
path, pwd->pw_uid, grp->gr_gid, strerror(errno));
return false;
}
log_debug(DEBUG_INOTIFY, "Changed ownership of \"%s\" to %s:%s (%u:%u)",
path, pwd->pw_name, grp_name, pwd->pw_uid, pwd->pw_gid);
return true;
}
@@ -533,7 +537,7 @@ void rotate_files(const char *path, char **first_file)
}
// Change ownership of file to pihole user
chown_pihole(new_path);
chown_pihole(new_path, NULL);
}
// Free memory
+3
View File
@@ -16,6 +16,8 @@
#include <mntent.h>
// SHA256_DIGEST_SIZE
#include <nettle/sha2.h>
// getpwuid()
#include <pwd.h>
#define MAX_ROTATIONS 15
#define BACKUP_DIR "/etc/pihole/config_backups"
@@ -31,6 +33,7 @@ void ls_dir(const char* path);
unsigned int get_path_usage(const char *path, char buffer[64]);
struct mntent *get_filesystem_details(const char *path);
bool directory_exists(const char *path);
bool chown_pihole(const char *path, struct passwd *pwd);
void rotate_files(const char *path, char **first_file);
bool files_different(const char *pathA, const char* pathB, unsigned int from);
bool sha256sum(const char *path, uint8_t checksum[SHA256_DIGEST_SIZE]);
-2
View File
@@ -481,7 +481,6 @@ static bool check_files_on_same_device(const char *path1, const char *path2)
void *GC_thread(void *val)
{
// Set thread name
thread_running[GC] = true;
prctl(PR_SET_NAME, thread_names[GC], 0, 0, 0);
// Remember when we last ran the actions
@@ -567,6 +566,5 @@ void *GC_thread(void *val)
watch_config(false);
log_info("Terminating GC thread");
thread_running[GC] = false;
return NULL;
}
+56 -17
View File
@@ -39,8 +39,9 @@
#include <sys/timex.h>
// log_ntp_message()
#include "database/message-table.h"
// load_queries_from_disk()
#include "database/query-table.h"
// check_capability()
#include "capabilities.h"
struct ntp_sync
{
bool valid;
@@ -576,7 +577,7 @@ bool ntp_client(const char *server, const bool settime, const bool print)
ntp_root_dispersion = D2FP(theta_stdev);
// Finally, adjust RTC if configured
if(config.ntp.rtc.set.v.b)
if(config.ntp.sync.rtc.set.v.b)
ntp_sync_rtc();
}
@@ -588,32 +589,57 @@ bool ntp_client(const char *server, const bool settime, const bool print)
static void *ntp_client_thread(void *arg)
{
// Set thread name
thread_running[NTP] = true;
prctl(PR_SET_NAME, thread_names[NTP], 0, 0, 0);
prctl(PR_SET_NAME, thread_names[NTP_CLIENT], 0, 0, 0);
// Run NTP client
bool ntp_server_started = false;
bool first_run = true;
while(!killed)
{
// Run NTP client
ntp_client(config.ntp.sync.server.v.s, true, false);
// Get time before NTP sync
const double before = double_time();
// Load queries from database after first NTP synchronization
if(first_run)
// Run NTP client
const bool success = ntp_client(config.ntp.sync.server.v.s, true, false);
// Get time after NTP sync
const double after = double_time();
// If the time was updated by more than a certain amount,
// restart FTL to import recent data. This is relevant when the
// system time was set to an incorrect value (e.g., due to a
// dead CMOS battery or overall missing RTC) and the time was
// off.
double time_delta = fabs(after - before);
if(first_run && time_delta > GCinterval)
{
load_queries_from_disk();
first_run = false;
log_info("System time was updated by %.1f seconds, restarting FTL to import recent data",
time_delta);
// Set the restart flag to true
exit_code = RESTART_FTL_CODE;
// Send SIGTERM to FTL
kill(main_pid(), SIGTERM);
}
// Set first run to false
first_run = false;
if(success && !ntp_server_started)
{
// Initialize NTP server only after first NTP
// synchronization to ensure that the time is set
// correctly
ntp_server_started = ntp_server_start();
}
// Intermediate cancellation-point
BREAK_IF_KILLED();
// Sleep before retrying
thread_sleepms(NTP, 1000 * config.ntp.sync.interval.v.ui);
thread_sleepms(NTP_CLIENT, 1000 * config.ntp.sync.interval.v.ui);
}
log_info("Terminating NTP thread");
thread_running[NTP] = false;
return NULL;
}
@@ -621,19 +647,32 @@ static void *ntp_client_thread(void *arg)
bool ntp_start_sync_thread(pthread_attr_t *attr)
{
// Return early if NTP client is disabled
if(config.ntp.sync.server.v.s == NULL ||
if(config.ntp.sync.active.v.b == false ||
config.ntp.sync.server.v.s == NULL ||
strlen(config.ntp.sync.server.v.s) == 0 ||
config.ntp.sync.interval.v.ui == 0)
{
load_queries_from_disk();
log_info("NTP sync is disabled");
ntp_server_start();
return false;
}
// Check if we have the ambient capabilities to set the system time.
// Without CAP_SYS_TIME, we cannot set the system time and the NTP
// client will not be able to synchronize the time so there is no point
// in starting the thread.
if(!check_capability(CAP_SYS_TIME))
{
log_warn("Insufficient permissions to set system time (CAP_SYS_TIME required), NTP client not available");
ntp_server_start();
return false;
}
// Create thread
if(pthread_create(&threads[NTP], attr, ntp_client_thread, NULL) != 0)
if(pthread_create(&threads[NTP_CLIENT], attr, ntp_client_thread, NULL) != 0)
{
log_err("Cannot create NTP client thread");
load_queries_from_disk();
ntp_server_start();
return false;
}
+1 -1
View File
@@ -27,7 +27,7 @@ uint64_t gettime64(void);
void print_debug_time(const char *label, const uint32_t *u32p, const uint64_t ntp_time);
// Start NTP server
bool ntp_server_start(pthread_attr_t *attr);
bool ntp_server_start(void);
// Start NTP client
bool ntp_client(const char *server, const bool settime, const bool print);
+20 -16
View File
@@ -51,15 +51,15 @@ static int open_rtc(void)
const gid_t gid = getgid();
// If the user has specified an RTC device, try to open it
if(config.ntp.rtc.device.v.s != NULL &&
strlen(config.ntp.rtc.device.v.s) > 0)
if(config.ntp.sync.rtc.device.v.s != NULL &&
strlen(config.ntp.sync.rtc.device.v.s) > 0)
{
// Open the RTC device
rtc_fd = open(config.ntp.rtc.device.v.s, O_RDONLY);
rtc_fd = open(config.ntp.sync.rtc.device.v.s, O_RDONLY);
if (rtc_fd != -1)
{
log_debug(DEBUG_NTP, "Successfully opened RTC at \"%s\"",
config.ntp.rtc.device.v.s);
config.ntp.sync.rtc.device.v.s);
return rtc_fd;
}
@@ -72,32 +72,34 @@ static int open_rtc(void)
{
// Get current owner of the device
struct stat st = { 0 };
if(stat(config.ntp.rtc.device.v.s, &st) == -1)
if(stat(config.ntp.sync.rtc.device.v.s, &st) == -1)
{
log_debug(DEBUG_NTP, "stat(\"%s\") failed: %s",
config.ntp.rtc.device.v.s, strerror(errno));
config.ntp.sync.rtc.device.v.s, strerror(errno));
return -1;
}
if(chown(config.ntp.rtc.device.v.s, uid, gid) == -1)
if(chown(config.ntp.sync.rtc.device.v.s, uid, gid) == -1)
{
log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s",
config.ntp.rtc.device.v.s, uid, gid, strerror(errno));
config.ntp.sync.rtc.device.v.s, uid, gid,
errno == EPERM ? "Insufficient permissions (CAP_CHOWN required)" : strerror(errno));
return -1;
}
rtc_fd = open(config.ntp.rtc.device.v.s, O_RDONLY);
rtc_fd = open(config.ntp.sync.rtc.device.v.s, O_RDONLY);
if (rtc_fd != -1)
{
log_debug(DEBUG_NTP, "Successfully opened RTC at \"%s\"",
config.ntp.rtc.device.v.s);
config.ntp.sync.rtc.device.v.s);
}
// Chown the device back to the original owner
if(chown(config.ntp.rtc.device.v.s, st.st_uid, st.st_gid) == -1)
if(chown(config.ntp.sync.rtc.device.v.s, st.st_uid, st.st_gid) == -1)
{
log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s",
config.ntp.rtc.device.v.s, st.st_uid, st.st_gid, strerror(errno));
config.ntp.sync.rtc.device.v.s, st.st_uid, st.st_gid,
errno == EPERM ? "Insufficient permissions (CAP_CHOWN required)" : strerror(errno));
return -1;
}
@@ -106,7 +108,7 @@ static int open_rtc(void)
}
log_debug(DEBUG_NTP, "Failed to open RTC at \"%s\": %s",
config.ntp.rtc.device.v.s, strerror(errno));
config.ntp.sync.rtc.device.v.s, strerror(errno));
return -1;
}
@@ -139,7 +141,8 @@ static int open_rtc(void)
if(chown(rtc_devices[i], uid, gid) == -1)
{
log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s",
rtc_devices[i], uid, gid, strerror(errno));
rtc_devices[i], uid, gid,
errno == EPERM ? "Insufficient permissions (CAP_CHOWN required)" : strerror(errno));
return -1;
}
@@ -154,7 +157,8 @@ static int open_rtc(void)
if(chown(rtc_devices[i], st.st_uid, st.st_gid) == -1)
{
log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s",
rtc_devices[i], st.st_uid, st.st_gid, strerror(errno));
rtc_devices[i], st.st_uid, st.st_gid,
errno == EPERM ? "Insufficient permissions (CAP_CHOWN required)" : strerror(errno));
return -1;
}
@@ -255,7 +259,7 @@ bool ntp_sync_rtc(void)
// Time to which we will set Hardware Clock, in broken down format
struct tm new_time = { 0 };
const time_t newtime = time(NULL);
if(config.ntp.rtc.utc.v.b)
if(config.ntp.sync.rtc.utc.v.b)
// UTC
gmtime_r(&newtime, &new_time);
else
+14 -9
View File
@@ -39,6 +39,10 @@
#include <inttypes.h>
// log_ntp_message()
#include "database/message-table.h"
// NTP_SERVER_IPV4,6
#include "enums.h"
// threads
#include "signals.h"
uint64_t ntp_last_sync = 0u;
uint32_t ntp_root_delay = 0u;
@@ -224,7 +228,7 @@ static bool ntp_reply(const int socket_fd, const struct sockaddr *saddr_p, const
}
// Process incoming NTP requests
static void request_process_loop(int fd, const char *ipstr, const int protocol)
static void request_process_loop(const int fd, const char *ipstr, const int protocol)
{
log_info("NTP server listening on %s:123 (%s)", ipstr, protocol == AF_INET ? "IPv4" : "IPv6");
while (true)
@@ -280,9 +284,12 @@ static void request_process_loop(int fd, const char *ipstr, const int protocol)
// Start the NTP server
static void *ntp_bind_and_listen(void *param)
{
const int protocol = param == 0 ? AF_INET : AF_INET6;
// Set thread name
const unsigned int thread_id = param == 0 ? NTP_SERVER4 : NTP_SERVER6;
prctl(PR_SET_NAME, thread_names[thread_id], 0, 0, 0);
// Create a socket
const int protocol = param == 0 ? AF_INET : AF_INET6;
errno = 0;
const int s = socket(protocol, SOCK_DGRAM, IPPROTO_UDP);
if(s == -1)
@@ -301,8 +308,7 @@ static void *ntp_bind_and_listen(void *param)
memset(ipstr, 0, sizeof(ipstr));
if(protocol == AF_INET)
{
// IPv4 - set thread name
prctl(PR_SET_NAME, "NTP (IPv4)", 0, 0, 0);
// IPv4 NTP server
// Prepare the bind address
struct sockaddr_in bind_addr;
@@ -327,8 +333,7 @@ static void *ntp_bind_and_listen(void *param)
}
else
{
// IPv6 - set thread name
prctl(PR_SET_NAME, "NTP (IPv6)", 0, 0, 0);
// IPv6 NTP server
// Set socket options to allow IPv6 only, otherwise it will bind
// to both IPv4 and IPv6 and show IPv4 addresses as
@@ -373,7 +378,7 @@ static void *ntp_bind_and_listen(void *param)
}
// Start the NTP server
bool ntp_server_start(pthread_attr_t *attr)
bool ntp_server_start(void)
{
// Spawn two pthreads, one for IPv4 and one for IPv6
@@ -382,7 +387,7 @@ bool ntp_server_start(pthread_attr_t *attr)
{
// Create a thread for the IPv4 NTP server
pthread_t thread;
if (pthread_create(&thread, attr, ntp_bind_and_listen, (void *)0) != 0)
if (pthread_create(&thread, NULL, ntp_bind_and_listen, (void *)0) != 0)
{
log_ntp_message(true, true, "Cannot create NTP server thread for IPv4");
return false;
@@ -394,7 +399,7 @@ bool ntp_server_start(pthread_attr_t *attr)
{
// Create a thread for the IPv6 NTP server
pthread_t thread;
if (pthread_create(&thread, attr, ntp_bind_and_listen, (void *)1) != 0)
if (pthread_create(&thread, NULL, ntp_bind_and_listen, (void *)1) != 0)
{
log_ntp_message(true, true, "Cannot create NTP server thread for IPv6");
return false;
-3
View File
@@ -1053,14 +1053,12 @@ static void resolveUpstreams(const bool onlynew)
void *DNSclient_thread(void *val)
{
// Set thread name
thread_running[DNSclient] = true;
prctl(PR_SET_NAME, thread_names[DNSclient], 0, 0, 0);
// Test struct sizes
if(!check_struct_sizes())
{
log_err("Struct sizes do not match expected sizes, aborting resolver thread");
thread_running[DNSclient] = false;
return NULL;
}
@@ -1124,6 +1122,5 @@ void *DNSclient_thread(void *val)
}
log_info("Terminating resolver thread");
thread_running[DNSclient] = false;
return NULL;
}
+7 -4
View File
@@ -190,17 +190,20 @@ static bool chown_shmem(SharedMemory *sharedMemory, struct passwd *ent_pw)
// Open shared memory object
const int fd = shm_open(sharedMemory->name, O_RDWR, S_IRUSR | S_IWUSR);
log_debug(DEBUG_SHMEM, "Changing %s (%d) to %u:%u", sharedMemory->name, fd, ent_pw->pw_uid, ent_pw->pw_gid);
if(fd == -1)
{
log_crit("chown_shmem(): Failed to open shared memory object \"%s\": %s",
log_crit("Failed to open shared memory object \"%s\" for chown: %s",
sharedMemory->name, strerror(errno));
exit(EXIT_FAILURE);
}
if(fchown(fd, ent_pw->pw_uid, ent_pw->pw_gid) == -1)
{
log_warn("chown_shmem(%d, %u, %u): failed for %s: %s (%d)",
fd, ent_pw->pw_uid, ent_pw->pw_gid, sharedMemory->name,
strerror(errno), errno);
log_crit("Failed to change ownership of shared memory object \"%s\": %s",
sharedMemory->name,
errno == EPERM ? "Insufficient permissions (CAP_CHOWN required)" : strerror(errno));
return false;
}
+3 -2
View File
@@ -34,13 +34,14 @@ static time_t FTLstarttime = 0;
volatile int exit_code = EXIT_SUCCESS;
volatile sig_atomic_t thread_cancellable[THREADS_MAX] = { false };
volatile sig_atomic_t thread_running[THREADS_MAX] = { false };
const char * const thread_names[THREADS_MAX] = {
"database",
"housekeeper",
"dns-client",
"timer",
"ntp-client"
"ntp-client",
"ntp-server4",
"ntp-server6",
};
// Return the (null-terminated) name of the calling thread
-1
View File
@@ -29,7 +29,6 @@ extern volatile sig_atomic_t want_to_reimport_aliasclients;
extern volatile sig_atomic_t want_to_reload_lists;
extern volatile sig_atomic_t thread_cancellable[THREADS_MAX];
extern volatile sig_atomic_t thread_running[THREADS_MAX];
extern const char * const thread_names[THREADS_MAX];
#define BREAK_IF_KILLED() { if(killed) break; }
-2
View File
@@ -84,7 +84,6 @@ void get_blockingmode_timer(double *delay, bool *target_status)
void *timer(void *val)
{
// Set thread name
thread_running[GC] = true;
prctl(PR_SET_NAME, thread_names[TIMER], 0, 0, 0);
// Save timestamp as we do not want to store immediately
@@ -110,7 +109,6 @@ void *timer(void *val)
}
log_info("Terminating timer thread");
thread_running[GC] = false;
return NULL;
}
-2
View File
@@ -616,8 +616,6 @@ int run_arp_scan(const bool scan_all, const bool extreme_mode)
pthread_attr_t attr;
// Initialize thread attributes object with default attribute values
pthread_attr_init(&attr);
// Set thread attributes to detached mode
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
struct ifaddrs *addrs, *tmp;
getifaddrs(&addrs);
-2
View File
@@ -725,8 +725,6 @@ int run_dhcp_discover(void)
pthread_attr_t attr;
// Initialize thread attributes object with default attribute values
pthread_attr_init(&attr);
// Set thread attributes to detached mode
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// Create processing/printfing lock
pthread_mutexattr_t lock_attr;
+43
View File
@@ -662,3 +662,46 @@ char *__attribute__((malloc)) escape_json(const char *string)
// Return the JSON escaped string
return namep;
}
// Remove duplicates from a cJSON array
// This function uses the less efficient cJSON_GetArraySize() function compared
// to cJSON_ArrayForEach() as we are going to modify the array in-place while
// iterating over it
void cJSON_unique_array(cJSON *array)
{
// Check if the array is an array
if(!cJSON_IsArray(array))
return;
for(int oi = 0; oi < cJSON_GetArraySize(array); oi++)
{
// Get the outer item
cJSON *outer_item = cJSON_GetArrayItem(array, oi);
// Check if the item is a string
if (!cJSON_IsString(outer_item))
continue;
// Check for duplicates in the remainder of the array
for(int ii = oi + 1; ii < cJSON_GetArraySize(array); ii++)
{
// Get the inner item
cJSON *inner_item = cJSON_GetArrayItem(array, ii);
// Check if the inner item is a string
if (!cJSON_IsString(inner_item))
continue;
// Compare the two strings
if(strcmp(outer_item->valuestring, inner_item->valuestring) == 0)
{
// Remove the duplicate item, this is safe as we are
// at least one item ahead of the outer item
cJSON_DeleteItemFromArray(array, ii);
// Compensate for removed item (the for loop
// will increment ii for the next step, thus
// we need to decrement it here)
ii--;
continue;
}
}
}
}
+1
View File
@@ -103,5 +103,6 @@ char * __attribute__((malloc)) escape_html(const char *string);
int check_json_payload(struct ftl_conn *api);
int parse_groupIDs(struct ftl_conn *api, tablerow *table, cJSON *row);
char * __attribute__((malloc)) escape_json(const char *string);
void cJSON_unique_array(cJSON *array);
#endif // HTTP_H
+26
View File
@@ -57,6 +57,29 @@
cJSON_AddItemToObject(object, key, string_item); \
})
// Hand over allocated string to cJSON - it will thereafter take care of freeing
// it when the cJSON object is deleted
#define JSON_GIVE_STR_TO_OBJECT(object, key, string)({ \
cJSON *string_item = NULL; \
if(string != NULL) \
{ \
string_item = cJSON_CreateStringReference((const char*)(string)); \
string_item->type &= ~cJSON_IsReference; \
} \
else \
{ \
string_item = cJSON_CreateNull(); \
} \
if(string_item == NULL) \
{ \
cJSON_Delete(object); \
send_http_internal_error(api); \
log_err("JSON_GIVE_STR_TO_OBJECT FAILED (key: \"%s\", string: \"%s\")!", key, string); \
return 500; \
} \
cJSON_AddItemToObject(object, key, string_item); \
})
#define JSON_ADD_NUMBER_TO_OBJECT(object, key, num)({ \
const double number = num; \
if(cJSON_AddNumberToObject(object, key, number) == NULL) \
@@ -269,3 +292,6 @@
#define cJSON_AddNumberToArray(array, num) \
cJSON_AddItemToArray(array, cJSON_CreateNumber(num))
#define cJSON_AddStringToArray(array, string) \
cJSON_AddItemToArray(array, cJSON_CreateString(string))
+14 -11
View File
@@ -481,6 +481,9 @@
address = ""
[ntp.sync]
# Should FTL try to synchronize the system time with an upstream NTP server?
active = true
# NTP upstream server to sync with, e.g., "pool.ntp.org". Note that the NTP server
# should be located as close as possible to you in order to minimize the time offset
# possibly introduced by different routing paths.
@@ -495,18 +498,18 @@
# Number of NTP syncs to perform and average before updating the system time
count = 8
[ntp.rtc]
# Should FTL update a real-time clock (RTC) if available?
set = true
[ntp.sync.rtc]
# Should FTL update a real-time clock (RTC) if available?
set = true
# Path to the RTC device to update. Leave empty for auto-discovery
#
# Possible values are:
# Path to the RTC device, e.g., "/dev/rtc0"
device = ""
# Path to the RTC device to update. Leave empty for auto-discovery
#
# Possible values are:
# Path to the RTC device, e.g., "/dev/rtc0"
device = ""
# Should the RTC be set to UTC?
utc = true
# Should the RTC be set to UTC?
utc = true
[resolver]
# Should FTL try to resolve IPv4 addresses to hostnames?
@@ -1102,7 +1105,7 @@
all = true ### CHANGED, default = false
# Configuration statistics:
# 148 total entries out of which 93 entries are default
# 149 total entries out of which 94 entries are default
# --> 55 entries are modified
# 2 entries are forced through environment:
# - misc.nice
+9 -7
View File
@@ -475,7 +475,7 @@
}
@test "No WARNING messages in FTL.log (besides known warnings)" {
run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|CAP_SYS_TIME|(Cannot set process priority)|FTLCONF_"'
run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|CAP_SYS_TIME|FTLCONF_"'
printf "%s\n" "${lines[@]}"
[[ "${lines[@]}" == "" ]]
}
@@ -1347,12 +1347,6 @@
[[ ${lines[0]} == '{"error":{"key":"bad_request","message":"Config items set via environment variables cannot be changed via the API","hint":"misc.nice"},"took":'*'}' ]]
}
@test "Check NTP server is broadcasting correct time" {
run bash -c './pihole-FTL ntp 127.0.0.1 --dry-run'
printf "%s\n" "${lines[@]}"
[[ $status == 0 ]]
}
# We cannot easily test IPv6 as it may not be available in docker (CI)
@test "API domain search: Non-existing domain returns expected JSON" {
@@ -1811,3 +1805,11 @@
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "3" ]]
}
@test "Check NTP server is broadcasting correct time" {
# Run this test at the very end of the test suite
# to ensure the NTP server has been started
run bash -c './pihole-FTL ntp 127.0.0.1'
printf "%s\n" "${lines[@]}"
[[ $status == 0 ]]
}