diff --git a/.github/Dockerfile b/.github/Dockerfile index 046e45f6..40d7a614 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -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 diff --git a/src/api/api.h b/src/api/api.h index 5c9bf8c8..793da3c6 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -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); diff --git a/src/api/dns.c b/src/api/dns.c index 10e90605..489ed7df 100644 --- a/src/api/dns.c +++ b/src/api/dns.c @@ -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"); } diff --git a/src/api/docs/content/specs/config.yaml b/src/api/docs/content/specs/config.yaml index af0edbfb..cdab29c6 100644 --- a/src/api/docs/content/specs/config.yaml +++ b/src/api/docs/content/specs/config.yaml @@ -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 diff --git a/src/api/queries.c b/src/api/queries.c index eb16529f..bc8dac1d 100644 --- a/src/api/queries.c +++ b/src/api/queries.c @@ -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 }; diff --git a/src/api/stats.c b/src/api/stats.c index 1569fb6b..18227e3c 100644 --- a/src/api/stats.c +++ b/src/api/stats.c @@ -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 +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, + ®ex_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(®ex_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(®ex_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, - ®ex_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(®ex_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(®ex_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) ®ex_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); } diff --git a/src/config/config.c b/src/config/config.c index 8054e83d..eee5a8c0 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -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 diff --git a/src/config/config.h b/src/config/config.h index 069217d3..81d4a15f 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -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 { diff --git a/src/config/dnsmasq_config.c b/src/config/dnsmasq_config.c index 6b8cbc82..1dd54d35 100644 --- a/src/config/dnsmasq_config.c +++ b/src/config/dnsmasq_config.c @@ -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) { diff --git a/src/config/toml_helper.c b/src/config/toml_helper.c index 26c6b1dd..22b85dd0 100644 --- a/src/config/toml_helper.c +++ b/src/config/toml_helper.c @@ -23,6 +23,8 @@ #include // 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; } diff --git a/src/daemon.c b/src/daemon.c index 5e7cf2db..a85b0ed0 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -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); + } + } } } diff --git a/src/database/database-thread.c b/src/database/database-thread.c index f8768902..3a83e425 100644 --- a/src/database/database-thread.c +++ b/src/database/database-thread.c @@ -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; } diff --git a/src/database/message-table.c b/src/database/message-table.c index 031d7fb8..409d151b 100644 --- a/src/database/message-table.c +++ b/src/database/message-table.c @@ -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); } diff --git a/src/database/query-table.c b/src/database/query-table.c index 435f560a..a2a98a00 100644 --- a/src/database/query-table.c +++ b/src/database/query-table.c @@ -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); diff --git a/src/database/query-table.h b/src/database/query-table.h index df0a8dd0..1a5b8b5a 100644 --- a/src/database/query-table.h +++ b/src/database/query-table.h @@ -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); diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 8e7377dc..d075ad0a 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -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; } diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index 200e37e9..8a5f15af 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -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 -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 -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); diff --git a/src/enums.h b/src/enums.h index 2df1a0eb..66ba915a 100644 --- a/src/enums.h +++ b/src/enums.h @@ -250,7 +250,9 @@ enum thread_types { GC, DNSclient, TIMER, - NTP, + NTP_CLIENT, + NTP_SERVER4, + NTP_SERVER6, THREADS_MAX } __attribute__ ((packed)); diff --git a/src/files.c b/src/files.c index cb5799fd..8158f60a 100644 --- a/src/files.c +++ b/src/files.c @@ -16,8 +16,6 @@ // opendir(), readdir() #include -// getpwuid() -#include // getgrgid() #include // 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 : ""; + + // 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 diff --git a/src/files.h b/src/files.h index 742e8d9a..fc10ad23 100644 --- a/src/files.h +++ b/src/files.h @@ -16,6 +16,8 @@ #include // SHA256_DIGEST_SIZE #include +// getpwuid() +#include #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]); diff --git a/src/gc.c b/src/gc.c index 3d4f0962..4b6a42f5 100644 --- a/src/gc.c +++ b/src/gc.c @@ -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; } diff --git a/src/ntp/client.c b/src/ntp/client.c index 894d9690..b0e634e3 100644 --- a/src/ntp/client.c +++ b/src/ntp/client.c @@ -39,8 +39,9 @@ #include // 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; } diff --git a/src/ntp/ntp.h b/src/ntp/ntp.h index 72445423..7adbe8ad 100644 --- a/src/ntp/ntp.h +++ b/src/ntp/ntp.h @@ -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); diff --git a/src/ntp/rtc.c b/src/ntp/rtc.c index abeb457e..07232711 100644 --- a/src/ntp/rtc.c +++ b/src/ntp/rtc.c @@ -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 diff --git a/src/ntp/server.c b/src/ntp/server.c index 652c9525..ae9eed9c 100644 --- a/src/ntp/server.c +++ b/src/ntp/server.c @@ -39,6 +39,10 @@ #include // 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; diff --git a/src/resolve.c b/src/resolve.c index 453e477e..a99c93ac 100644 --- a/src/resolve.c +++ b/src/resolve.c @@ -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; } diff --git a/src/shmem.c b/src/shmem.c index 44f72eb8..83e7da5c 100644 --- a/src/shmem.c +++ b/src/shmem.c @@ -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; } diff --git a/src/signals.c b/src/signals.c index 8e14cfe2..34f6b52c 100644 --- a/src/signals.c +++ b/src/signals.c @@ -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 diff --git a/src/signals.h b/src/signals.h index 76664887..4e388dd9 100644 --- a/src/signals.h +++ b/src/signals.h @@ -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; } diff --git a/src/timers.c b/src/timers.c index 17b16704..b841d3be 100644 --- a/src/timers.c +++ b/src/timers.c @@ -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; } diff --git a/src/tools/arp-scan.c b/src/tools/arp-scan.c index c5ac6ec3..923625a3 100644 --- a/src/tools/arp-scan.c +++ b/src/tools/arp-scan.c @@ -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); diff --git a/src/tools/dhcp-discover.c b/src/tools/dhcp-discover.c index 045c8c14..c68a74f9 100644 --- a/src/tools/dhcp-discover.c +++ b/src/tools/dhcp-discover.c @@ -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; diff --git a/src/webserver/http-common.c b/src/webserver/http-common.c index 2154f0f6..75e687ba 100644 --- a/src/webserver/http-common.c +++ b/src/webserver/http-common.c @@ -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; + } + } + } +} diff --git a/src/webserver/http-common.h b/src/webserver/http-common.h index d8bfe1af..bde500e5 100644 --- a/src/webserver/http-common.h +++ b/src/webserver/http-common.h @@ -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 diff --git a/src/webserver/json_macros.h b/src/webserver/json_macros.h index 2644e075..6c12c650 100644 --- a/src/webserver/json_macros.h +++ b/src/webserver/json_macros.h @@ -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)) diff --git a/test/pihole.toml b/test/pihole.toml index 0b433266..4dbe9889 100644 --- a/test/pihole.toml +++ b/test/pihole.toml @@ -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 diff --git a/test/test_suite.bats b/test/test_suite.bats index ed73f557..ca07dbf9 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -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 ]] +}