Merge branch 'development-v6' into fix/read_rotated_toml_on_error

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER 2023-11-18 07:42:50 +01:00
commit 1a517c7358
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
46 changed files with 1460 additions and 307 deletions

View File

@ -1,5 +1,8 @@
name: Codespell
on:
push:
branches:
- '**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

View File

@ -138,7 +138,7 @@
// caused by insufficient memory or by code bugs (not properly dealing
// with NULL pointers) much easier.
#undef strdup // strdup() is a macro in itself, it needs special handling
#define free(ptr) FTLfree(ptr, __FILE__, __FUNCTION__, __LINE__)
#define free(ptr) FTLfree((void**)&ptr, __FILE__, __FUNCTION__, __LINE__)
#define strdup(str_in) FTLstrdup(str_in, __FILE__, __FUNCTION__, __LINE__)
#define calloc(numer_of_elements, element_size) FTLcalloc(numer_of_elements, element_size, __FILE__, __FUNCTION__, __LINE__)
#define realloc(ptr, new_size) FTLrealloc(ptr, new_size, __FILE__, __FUNCTION__, __LINE__)

View File

@ -198,7 +198,7 @@ static bool encode_uint8_t_array_to_base32(const uint8_t *in, const size_t in_le
}
static uint32_t last_code = 0;
bool verifyTOTP(const uint32_t incode)
enum totp_status verifyTOTP(const uint32_t incode)
{
// Decode base32 secret
uint8_t decoded_secret[RFC6238_SECRET_LEN];
@ -228,15 +228,16 @@ bool verifyTOTP(const uint32_t incode)
{
log_warn("2FA code has already been used (%i, %u), please wait %lu seconds",
i, gencode, (unsigned long)(RFC6238_X - (now % RFC6238_X)));
return false;
return TOTP_REUSED;
}
log_info("2FA code verified successfully at %i", i);
const char *which = i == -1 ? "previous" : i == 0 ? "current" : "next";
log_debug(DEBUG_API, "2FA code from %s time step is valid", which);
last_code = gencode;
return true;
return TOTP_CORRECT;
}
}
return false;
return TOTP_INVALID;
}
// Print TOTP code to stdout (for CLI use)

View File

@ -92,7 +92,12 @@ int api_auth_session_delete(struct ftl_conn *api);
bool is_local_api_user(const char *remote_addr) __attribute__((pure));
// 2FA methods
bool verifyTOTP(const uint32_t code);
enum totp_status {
TOTP_INVALID,
TOTP_CORRECT,
TOTP_REUSED,
} __attribute__ ((packed));
enum totp_status verifyTOTP(const uint32_t code);
int generateTOTP(struct ftl_conn *api);
int printTOTP(void);
int generateAppPw(struct ftl_conn *api);

View File

@ -26,7 +26,8 @@
// database session functions
#include "database/session-table.h"
static struct session auth_data[API_MAX_CLIENTS] = {{false, false, {false, false}, 0, 0, {0}, {0}, {0}, {0}}};
static uint16_t max_sessions = 0;
static struct session *auth_data = NULL;
static void add_request_info(struct ftl_conn *api, const char *csrf)
{
@ -43,13 +44,23 @@ static void add_request_info(struct ftl_conn *api, const char *csrf)
void init_api(void)
{
// Restore sessions from database
restore_db_sessions(auth_data);
max_sessions = config.webserver.api.max_sessions.v.u16;
auth_data = calloc(max_sessions, sizeof(struct session));
if(auth_data == NULL)
{
log_crit("Could not allocate memory for API sessions, check config value of webserver.api.max_sessions");
exit(EXIT_FAILURE);
}
restore_db_sessions(auth_data, max_sessions);
}
void free_api(void)
{
// Store sessions in database
backup_db_sessions(auth_data);
backup_db_sessions(auth_data, max_sessions);
max_sessions = 0;
free(auth_data);
auth_data = NULL;
}
// Is this client connecting from localhost?
@ -187,7 +198,7 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
}
}
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
for(unsigned int i = 0; i < max_sessions; i++)
{
if(auth_data[i].used &&
auth_data[i].valid_until >= now &&
@ -253,7 +264,7 @@ static int get_all_sessions(struct ftl_conn *api, cJSON *json)
{
const time_t now = time(NULL);
cJSON *sessions = JSON_NEW_ARRAY();
for(int i = 0; i < API_MAX_CLIENTS; i++)
for(int i = 0; i < max_sessions; i++)
{
if(!auth_data[i].used)
continue;
@ -316,7 +327,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
static void delete_session(const int user_id)
{
// Skip if nothing to be done here
if(user_id < 0 || user_id >= API_MAX_CLIENTS)
if(user_id < 0 || user_id >= max_sessions)
return;
// Zero out this session (also sets valid to false == 0)
@ -326,7 +337,7 @@ static void delete_session(const int user_id)
void delete_all_sessions(void)
{
// Zero out all sessions without looping
memset(auth_data, 0, sizeof(auth_data));
memset(auth_data, 0, max_sessions*sizeof(*auth_data));
}
static int send_api_auth_status(struct ftl_conn *api, const int user_id, const time_t now)
@ -516,18 +527,27 @@ int api_auth(struct ftl_conn *api)
NULL);
}
if(!verifyTOTP(json_totp->valueint))
enum totp_status totp = verifyTOTP(json_totp->valueint);
if(totp == TOTP_REUSED)
{
// 2FA token has been reused
return send_json_error(api, 401,
"unauthorized",
"Reused 2FA token",
"wait for new token");
}
else if(totp != TOTP_CORRECT)
{
// 2FA token is invalid
return send_json_error(api, 401,
"unauthorized",
"Invalid 2FA token",
NULL);
"unauthorized",
"Invalid 2FA token",
NULL);
}
}
// Find unused authentication slot
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
for(unsigned int i = 0; i < max_sessions; i++)
{
// Expired slow, mark as unused
if(auth_data[i].used &&
@ -585,16 +605,22 @@ int api_auth(struct ftl_conn *api)
}
if(user_id == API_AUTH_UNAUTHORIZED)
{
log_warn("No free API seats available, not authenticating client");
log_warn("No free API seats available (webserver.api.max_sessions = %u), not authenticating client",
max_sessions);
return send_json_error(api, 429,
"api_seats_exceeded",
"API seats exceeded",
"increase webserver.api.max_sessions");
}
}
else if(result == PASSWORD_RATE_LIMITED)
{
// Rate limited
return send_json_error(api, 429,
"too_many_requests",
"Too many requests",
"login rate limiting");
"rate_limiting",
"Rate-limiting login attempts",
NULL);
}
else
{
@ -621,7 +647,7 @@ int api_auth_session_delete(struct ftl_conn *api)
return send_json_error(api, 400, "bad_request", "Missing or invalid session ID", NULL);
// Check if session ID is valid
if(uid <= API_AUTH_UNAUTHORIZED || uid >= API_MAX_CLIENTS)
if(uid <= API_AUTH_UNAUTHORIZED || uid >= max_sessions)
return send_json_error(api, 400, "bad_request", "Session ID out of bounds", NULL);
// Check if session is used

View File

@ -11,9 +11,6 @@
#ifndef AUTH_H
#define AUTH_H
// How many authenticated API clients are allowed simultaneously? [.]
#define API_MAX_CLIENTS 16
// crypto library
#include <nettle/sha2.h>
#include <nettle/base64.h>

View File

@ -26,6 +26,8 @@
#include "shmem.h"
// hash_password()
#include "config/password.h"
// main_pid()
#include "signals.h"
static struct {
const char *name;
@ -128,12 +130,22 @@ static cJSON *addJSONvalue(const enum conf_type conf_type, union conf_value *val
return cJSON_CreateStringReference(get_temp_unit_str(val->temp_unit));
case CONF_STRUCT_IN_ADDR:
{
// Special case 0.0.0.0 -> return empty string
if(val->in_addr.s_addr == INADDR_ANY)
return cJSON_CreateStringReference("");
// else: normal address
char addr4[INET_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET, &val->in_addr, addr4, INET_ADDRSTRLEN);
return cJSON_CreateString(addr4); // Performs a copy
}
case CONF_STRUCT_IN6_ADDR:
{
// Special case :: -> return empty string
if(memcmp(&val->in6_addr, &in6addr_any, sizeof(in6addr_any)) == 0)
return cJSON_CreateStringReference("");
// else: normal address
char addr6[INET6_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET6, &val->in6_addr, addr6, INET6_ADDRSTRLEN);
return cJSON_CreateString(addr6); // Performs a copy
@ -400,11 +412,19 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
struct in_addr addr4 = { 0 };
if(!cJSON_IsString(elem))
return "not of type string";
if(!inet_pton(AF_INET, elem->valuestring, &addr4))
if(strlen(elem->valuestring) == 0)
{
// Special case: empty string -> 0.0.0.0
conf_item->v.in_addr.s_addr = INADDR_ANY;
}
else if(inet_pton(AF_INET, elem->valuestring, &addr4))
{
// Set item
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
}
else
return "not a valid IPv4 address";
// Set item
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
log_debug(DEBUG_CONFIG, "%s = %s", conf_item->k, elem->valuestring);
log_debug(DEBUG_CONFIG, "%s = \"%s\"", conf_item->k, elem->valuestring);
break;
}
case CONF_STRUCT_IN6_ADDR:
@ -412,11 +432,16 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
struct in6_addr addr6 = { 0 };
if(!cJSON_IsString(elem))
return "not of type string";
if(!inet_pton(AF_INET6, elem->valuestring, &addr6))
if(strlen(elem->valuestring) == 0)
{
// Special case: empty string -> ::
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
}
else if(!inet_pton(AF_INET6, elem->valuestring, &addr6))
return "not a valid IPv6 address";
// Set item
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
log_debug(DEBUG_CONFIG, "%s = %s", conf_item->k, elem->valuestring);
log_debug(DEBUG_CONFIG, "%s = \"%s\"", conf_item->k, elem->valuestring);
break;
}
case CONF_JSON_STRING_ARRAY:
@ -659,6 +684,7 @@ static int api_config_patch(struct ftl_conn *api)
// Read all known config items
bool config_changed = false;
bool dnsmasq_changed = false;
bool rewrite_hosts = false;
struct config newconf;
duplicate_config(&newconf, &config);
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)
@ -693,8 +719,23 @@ static int api_config_patch(struct ftl_conn *api)
const char *response = getJSONvalue(new_item, elem, &newconf);
if(response != NULL)
{
log_err("/api/config: %s invalid: %s", new_item->k, response);
continue;
char *hint = calloc(strlen(new_item->k) + strlen(response) + 3, sizeof(char));
if(hint == NULL)
{
free_config(&newconf);
return send_json_error(api, 500,
"internal_error",
"Failed to allocate memory for hint",
NULL);
}
strcpy(hint, new_item->k);
strcat(hint, ": ");
strcat(hint, response);
free_config(&newconf);
return send_json_error_free(api, 400,
"bad_request",
"Config item is invalid",
hint, true);
}
// Get pointer to memory location of this conf_item (global)
@ -730,6 +771,10 @@ static int api_config_patch(struct ftl_conn *api)
if(conf_item->f & FLAG_RESTART_FTL)
dnsmasq_changed = true;
// Check if this item requires rewriting the HOSTS file
if(conf_item == &config.dns.hosts)
rewrite_hosts = true;
// If the privacy level was decreased, we need to restart
if(new_item == &newconf.misc.privacylevel &&
new_item->v.privacy_level < conf_item->v.privacy_level)
@ -768,6 +813,10 @@ static int api_config_patch(struct ftl_conn *api)
// Store changed configuration to disk
writeFTLtoml(true);
// Rewrite HOSTS file if required
if(rewrite_hosts)
write_custom_list();
}
else
{
@ -816,6 +865,7 @@ static int api_config_put_delete(struct ftl_conn *api)
// Read all known config items
bool dnsmasq_changed = false;
bool rewrite_hosts = false;
bool found = false;
struct config newconf;
duplicate_config(&newconf, &config);
@ -907,6 +957,10 @@ static int api_config_put_delete(struct ftl_conn *api)
if(new_item->f & FLAG_RESTART_FTL)
dnsmasq_changed = true;
// Check if this item requires rewriting the HOSTS file
if(new_item == &newconf.dns.hosts)
rewrite_hosts = true;
break;
}
@ -954,6 +1008,10 @@ static int api_config_put_delete(struct ftl_conn *api)
// Store changed configuration to disk
writeFTLtoml(true);
// Rewrite HOSTS file if required
if(rewrite_hosts)
write_custom_list();
// Send empty reply with matching HTTP status code
// 201 - Created or 204 - No content
cJSON *json = JSON_NEW_OBJECT();

View File

@ -80,6 +80,8 @@ components:
$ref: 'auth.yaml#/components/examples/errors/no_password'
password_inval:
$ref: 'auth.yaml#/components/examples/errors/password_inval'
totp_missing:
$ref: 'auth.yaml#/components/examples/errors/totp_missing'
'401':
description: Unauthorized
content:
@ -88,6 +90,11 @@ components:
allOf:
- $ref: 'common.yaml#/components/errors/unauthorized'
- $ref: 'common.yaml#/components/schemas/took'
examples:
totp_invalid:
$ref: 'auth.yaml#/components/examples/errors/totp_invalid'
totp_reused:
$ref: 'auth.yaml#/components/examples/errors/totp_reused'
'429':
description: Too Many Requests
content:
@ -96,6 +103,11 @@ components:
allOf:
- $ref: 'common.yaml#/components/errors/too_many_requests'
- $ref: 'common.yaml#/components/schemas/took'
examples:
rate_limit:
$ref: 'auth.yaml#/components/examples/errors/rate_limit'
no_seats:
$ref: 'auth.yaml#/components/examples/errors/no_seats'
delete:
summary: Delete session
tags:
@ -486,6 +498,13 @@ components:
key: "bad_request"
message: "Field password has to be of type 'string'"
hint: null
totp_missing:
summary: Bad request (2FA token missing)
value:
error:
key: "bad_request"
message: "No 2FA token found in JSON payload"
hint: null
missing_session_id:
summary: Bad request (missing session ID)
value:
@ -507,6 +526,34 @@ components:
key: "bad_request"
message: "Session ID not in use"
hint: null
totp_invalid:
summary: 2FA token invalid
value:
error:
key: "unauthorized"
message: "Invalid 2FA token"
hint: null
totp_reused:
summary: 2FA token reused
value:
error:
key: "unauthorized"
message: "Reused 2FA token"
hint: "wait for new token"
rate_limit:
summary: Rate limit exceeded
value:
error:
key: "rate_limiting"
message: "Rate-limiting login attempts"
hint: null
no_seats:
summary: No free API seats available
value:
error:
key: "api_seats_exceeded"
message: "API seats exceeded"
hint: "increase webserver.api.max_sessions"
parameters:
id:
in: path

View File

@ -206,6 +206,8 @@ components:
type: boolean
expandHosts:
type: boolean
domain:
type: string
bogusPriv:
type: boolean
dnssec:
@ -268,8 +270,10 @@ components:
type: boolean
IPv4:
type: string
x-format: ipv4
IPv6:
type: string
x-format: ipv6
blocking:
type: object
properties:
@ -279,8 +283,10 @@ components:
type: boolean
IPv4:
type: string
x-format: ipv4
IPv6:
type: string
x-format: ipv6
rateLimit:
type: object
properties:
@ -295,12 +301,20 @@ components:
type: boolean
start:
type: string
x-format: ipv4
end:
type: string
x-format: ipv4
router:
type: string
x-format: ipv4
netmask:
type: string
x-format: ipv4
domain:
type: string
description: |
*Note:* This setting is deprecated and will be removed in a future release. Use dns.domain instead.
leaseTime:
type: string
ipv6:
@ -386,6 +400,8 @@ components:
type: boolean
searchAPIauth:
type: boolean
max_sessions:
type: integer
prettyJSON:
type: boolean
password:
@ -450,6 +466,8 @@ components:
type: integer
addr2line:
type: boolean
etc_dnsmasq_d:
type: boolean
privacylevel:
type: integer
dnsmasq_lines:
@ -583,6 +601,7 @@ components:
- "192.168.2.123 mymusicbox"
domainNeeded: true
expandHosts: true
domain: "lan"
bogusPriv: true
dnssec: true
interface: "eth0"
@ -627,6 +646,7 @@ components:
end: "192.168.0.250"
router: "192.168.0.1"
domain: "lan"
netmask: "0.0.0.0"
leaseTime: "24h"
ipv6: true
rapidCommit: true
@ -666,6 +686,7 @@ components:
api:
localAPIauth: false
searchAPIauth: false
max_sessions: 16
prettyJSON: false
password: "********"
pwhash: ''
@ -694,6 +715,7 @@ components:
delay_startup: 10
addr2line: true
privacylevel: 0
etc_dnsmasq_d: false
dnsmasq_lines: [ ]
check:
load: true

View File

@ -25,51 +25,11 @@
int api_history(struct ftl_conn *api)
{
unsigned int from = 0, until = OVERTIME_SLOTS;
const time_t now = time(NULL);
bool found = false;
lock_shm();
time_t mintime = overTime[0].timestamp;
// Start with the first non-empty overTime slot
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
if((overTime[slot].total > 0 || overTime[slot].blocked > 0) &&
overTime[slot].timestamp >= mintime)
{
from = slot;
found = true;
break;
}
}
// End with last non-empty overTime slot or the last slot that is not
// older than the maximum history to be sent
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
if(overTime[slot].timestamp >= now ||
overTime[slot].timestamp - now > (time_t)config.webserver.api.maxHistory.v.ui)
{
until = slot;
break;
}
}
// If there is no data to be sent, we send back an empty array
// and thereby return early
if(!found)
{
cJSON *json = JSON_NEW_ARRAY();
cJSON *item = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_ARRAY(json, item);
JSON_SEND_OBJECT_UNLOCK(json);
}
// Minimum structure is
// {"history":[]}
// Loop over all overTime slots and add them to the array
cJSON *history = JSON_NEW_ARRAY();
for(unsigned int slot = from; slot < until; slot++)
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
cJSON *item = JSON_NEW_OBJECT();
JSON_ADD_NUMBER_TO_OBJECT(item, "timestamp", overTime[slot].timestamp);
@ -79,33 +39,22 @@ int api_history(struct ftl_conn *api)
JSON_ADD_ITEM_TO_ARRAY(history, item);
}
// Unlock already here to avoid keeping the lock during JSON generation
// This is safe because we don't access any shared memory after this
// point. All numbers in the JSON are copied
unlock_shm();
// Minimum structure is
// {"history":[]}
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "history", history);
JSON_SEND_OBJECT_UNLOCK(json);
JSON_SEND_OBJECT(json);
}
int api_history_clients(struct ftl_conn *api)
{
int sendit = false;
unsigned int from = 0, until = OVERTIME_SLOTS;
const time_t now = time(NULL);
lock_shm();
// Find minimum ID to send
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
if((overTime[slot].total > 0 || overTime[slot].blocked > 0) &&
overTime[slot].timestamp >= overTime[0].timestamp)
{
sendit = true;
from = slot;
break;
}
}
// Exit before processing any data if requested via config setting
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS_CLIENTS || !sendit)
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS_CLIENTS)
{
// Minimum structure is
// {"history":[], "clients":[]}
@ -117,17 +66,7 @@ int api_history_clients(struct ftl_conn *api)
JSON_SEND_OBJECT_UNLOCK(json);
}
// End with last non-empty overTime slot or the last slot that is not
// older than the maximum history to be sent
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
if(overTime[slot].timestamp >= now ||
overTime[slot].timestamp - now > (time_t)config.webserver.api.maxHistory.v.ui)
{
until = slot;
break;
}
}
lock_shm();
// Get clients which the user doesn't want to see
// if skipclient[i] == true then this client should be hidden from
@ -154,7 +93,7 @@ int api_history_clients(struct ftl_conn *api)
}
}
// Also skip alias-clients
// Also skip clients included in others (in alias-clients)
for(int clientID = 0; clientID < counters->clients; clientID++)
{
// Get client pointer
@ -165,9 +104,9 @@ int api_history_clients(struct ftl_conn *api)
skipclient[clientID] = true;
}
cJSON *history = JSON_NEW_ARRAY();
// Main return loop
for(unsigned int slot = from; slot < until; slot++)
cJSON *history = JSON_NEW_ARRAY();
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
cJSON *item = JSON_NEW_OBJECT();
JSON_ADD_NUMBER_TO_OBJECT(item, "timestamp", overTime[slot].timestamp);
@ -196,8 +135,8 @@ int api_history_clients(struct ftl_conn *api)
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "history", history);
cJSON *clients = JSON_NEW_ARRAY();
// Loop over clients to generate output to be sent to the client
cJSON *clients = JSON_NEW_ARRAY();
for(int clientID = 0; clientID < counters->clients; clientID++)
{
if(skipclient[clientID])
@ -216,10 +155,16 @@ int api_history_clients(struct ftl_conn *api)
JSON_REF_STR_IN_OBJECT(item, "ip", client_ip);
JSON_ADD_ITEM_TO_ARRAY(clients, item);
}
JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
// Unlock already here to avoid keeping the lock during JSON generation
// This is safe because we don't access any shared memory after this
// point and all strings in the JSON are references to idempotent shared
// memory and can, thus, be accessed at any time without locking
unlock_shm();
// Free memory
free(skipclient);
JSON_SEND_OBJECT_UNLOCK(json);
JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
JSON_SEND_OBJECT(json);
}

View File

@ -393,7 +393,8 @@ static int api_list_write(struct ftl_conn *api,
strchr(it->valuestring, '\t') != NULL ||
strchr(it->valuestring, '\n') != NULL)
{
cJSON_free(row.items);
if(allocated_json)
cJSON_free(row.items);
return send_json_error(api, 400, // 400 Bad Request
"bad_request",
"Spaces, newlines and tabs are not allowed in domains and URLs",
@ -406,11 +407,12 @@ static int api_list_write(struct ftl_conn *api,
if(!okay)
{
// Send error reply
cJSON_free(row.items);
return send_json_error(api, 400, // 400 Bad Request
"regex_error",
"Regex validation failed",
regex_msg);
if(allocated_json)
cJSON_free(row.items);
return send_json_error_free(api, 400, // 400 Bad Request
"regex_error",
"Regex validation failed",
regex_msg, true);
}
// Try to add item(s) to table

View File

@ -244,9 +244,11 @@ int api_search(struct ftl_conn *api)
char *allow_list = cJSON_PrintUnformatted(allow_ids);
ret = search_table(api,punycode, GRAVITY_DOMAINLIST_ALLOW_REGEX, allow_list, limit, &Nregex, false, domains);
free(allow_list);
free(punycode);
if(ret != 200)
{
free(punycode);
return ret;
}
}
if(cJSON_GetArraySize(deny_ids) > 0)
@ -254,9 +256,11 @@ int api_search(struct ftl_conn *api)
char *deny_list = cJSON_PrintUnformatted(deny_ids);
ret = search_table(api, punycode, GRAVITY_DOMAINLIST_DENY_REGEX, deny_list, limit, &Nregex, false, domains);
free(deny_list);
free(punycode);
if(ret != 200)
{
free(punycode);
return ret;
}
}
cJSON *search = JSON_NEW_OBJECT();

View File

@ -309,6 +309,7 @@ void parse_args(int argc, char* argv[])
exit(read_teleporter_zip_from_disk(argv[2]) ? EXIT_SUCCESS : EXIT_FAILURE);
}
// Generate X.509 certificate
if(argc > 1 && strcmp(argv[1], "--gen-x509") == 0)
{
if(argc < 3 || argc > 5)
@ -327,6 +328,55 @@ void parse_args(int argc, char* argv[])
exit(generate_certificate(argv[2], rsa, domain) ? EXIT_SUCCESS : EXIT_FAILURE);
}
// Parse X.509 certificate
if(argc > 1 &&
(strcmp(argv[1], "--read-x509") == 0 ||
strcmp(argv[1], "--read-x509-key") == 0))
{
if(argc < 2 || argc > 4)
{
printf("Usage: %s %s [<input file>] [<domain>]\n", argv[0], argv[1]);
printf("Example: %s %s /etc/pihole/tls.pem\n", argv[0], argv[1]);
printf(" with domain: %s %s /etc/pihole/tls.pem pi.hole\n", argv[0], argv[1]);
exit(EXIT_FAILURE);
}
// Option parsing
// Should we report on the private key?
const bool private_key = strcmp(argv[1], "--read-x509-key") == 0;
// If no certificate file is given, we use the one from the config
const char *certfile = NULL;
if(argc == 2)
{
readFTLconf(&config, false);
certfile = config.webserver.tls.cert.v.s;
}
else
certfile = argv[2];
// If no domain is given, we only check the certificate
const char *domain = argc > 3 ? argv[3] : NULL;
// Enable stdout printing
cli_mode = true;
log_ctrl(false, true);
enum cert_check result = read_certificate(certfile, domain, private_key);
if(argc < 4)
exit(result == CERT_OKAY ? EXIT_SUCCESS : EXIT_FAILURE);
else if(result == CERT_DOMAIN_MATCH)
{
printf("Certificate matches domain %s\n", argv[3]);
exit(EXIT_SUCCESS);
}
else
{
printf("Certificate does not match domain %s\n", argv[3]);
exit(EXIT_FAILURE);
}
}
// If the first argument is "gravity" (e.g., /usr/bin/pihole-FTL gravity),
// we offer some specialized gravity tools
if(argc > 1 && (strcmp(argv[1], "gravity") == 0 || strcmp(argv[1], "antigravity") == 0))
@ -812,6 +862,16 @@ void parse_args(int argc, char* argv[])
printf(" an RSA (4096 bit) key will be generated instead.\n\n");
printf(" Usage: %spihole-FTL --gen-x509 %soutfile %s[rsa]%s\n\n", green, cyan, purple, normal);
printf("%sTLS X.509 certificate parser:%s\n", yellow, normal);
printf(" Parse the given X.509 certificate and optionally check if\n");
printf(" it matches a given domain. If no domain is given, only a\n");
printf(" human-readable output string is printed.\n\n");
printf(" If no certificate file is given, the one from the config\n");
printf(" is used (if applicable). If --read-x509-key is used, details\n");
printf(" about the private key are printed as well.\n\n");
printf(" Usage: %spihole-FTL --read-x509 %s[certfile] %s[domain]%s\n", green, cyan, purple, normal);
printf(" Usage: %spihole-FTL --read-x509-key %s[certfile] %s[domain]%s\n\n", green, cyan, purple, normal);
printf("%sGravity tools:%s\n", yellow, normal);
printf(" Check domains in a given file for validity using Pi-hole's\n");
printf(" gravity filters. The expected input format is one domain\n");

View File

@ -10,6 +10,8 @@
#ifndef CAPABILITIES_H
#define CAPABILITIES_H
#include <linux/capability.h>
bool check_capability(const unsigned int cap);
bool check_capabilities(void);

View File

@ -20,6 +20,8 @@
#include "tomlc99/toml.h"
// hash_password()
#include "config/password.h"
// check_capability()
#include "capabilities.h"
// Read a TOML value from a table depending on its type
static bool readStringValue(struct conf_item *conf_item, const char *value, struct config *newconf)
@ -295,7 +297,12 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
case CONF_STRUCT_IN_ADDR:
{
struct in_addr addr4 = { 0 };
if(inet_pton(AF_INET, value, &addr4))
if(strlen(value) == 0)
{
// Special case: empty string -> 0.0.0.0
conf_item->v.in_addr.s_addr = INADDR_ANY;
}
else if(inet_pton(AF_INET, value, &addr4))
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
else
{
@ -307,7 +314,12 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
case CONF_STRUCT_IN6_ADDR:
{
struct in6_addr addr6 = { 0 };
if(inet_pton(AF_INET6, value, &addr6))
if(strlen(value) == 0)
{
// Special case: empty string -> ::
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
}
else if(inet_pton(AF_INET6, value, &addr6))
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
else
{
@ -353,6 +365,25 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
int set_config_from_CLI(const char *key, const char *value)
{
// Check if we are either
// - root, or
// - pihole with CAP_CHOWN capability on the pihole-FTL binary
const uid_t euid = geteuid();
const struct passwd *current_user = getpwuid(euid);
const bool is_root = euid == 0;
const bool is_pihole = current_user != NULL && strcmp(current_user->pw_name, "pihole") == 0;
const bool have_chown_cap = check_capability(CAP_CHOWN);
if(!is_root && !(is_pihole && have_chown_cap))
{
if(is_pihole)
printf("Permission error: CAP_CHOWN is missing on the binary\n");
else
printf("Permission error: User %s is not allowed to edit Pi-hole's config\n", current_user->pw_name);
printf("Please run this command using sudo\n\n");
return EXIT_FAILURE;
}
// Identify config option
struct config newconf;
duplicate_config(&newconf, &config);
@ -420,10 +451,8 @@ int set_config_from_CLI(const char *key, const char *value)
}
else if(conf_item == &config.dns.hosts)
{
// We need to rewrite the custom.list file but do not need to
// restart dnsmasq. If dnsmasq is going to be restarted anyway,
// this is not necessary as the file will be rewritten during
// the restart
// We need to rewrite the custom.list file but do not
// need to restart dnsmasq
write_custom_list();
}

View File

@ -462,7 +462,7 @@ void initConfig(struct config *conf)
conf->dns.hosts.h = "Array of custom DNS records\n Example: hosts = [ \"127.0.0.1 mylocal\", \"192.168.0.1 therouter\" ]";
conf->dns.hosts.a = cJSON_CreateStringReference("Array of custom DNS records each one in HOSTS form: \"IP HOSTNAME\"");
conf->dns.hosts.t = CONF_JSON_STRING_ARRAY;
conf->dns.hosts.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->dns.hosts.f = FLAG_ADVANCED_SETTING;
conf->dns.hosts.d.json = cJSON_CreateArray();
conf->dns.domainNeeded.k = "dns.domainNeeded";
@ -477,6 +477,13 @@ void initConfig(struct config *conf)
conf->dns.expandHosts.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.expandHosts.d.b = false;
conf->dns.domain.k = "dns.domain";
conf->dns.domain.h = "The DNS domain used by your Pi-hole to expand hosts and for DHCP.\n\n Only if DHCP is enabled below: For DHCP, this has two effects; firstly it causes the DHCP server to return the domain to any hosts which request it, and secondly it sets the domain which it is legal for DHCP-configured hosts to claim. The intention is to constrain hostnames so that an untrusted host on the LAN cannot advertise its name via DHCP as e.g. \"google.com\" and capture traffic not meant for it. If no domain suffix is specified, then any DHCP hostname with a domain part (ie with a period) will be disallowed and logged. If a domain is specified, then hostnames with a domain part are allowed, provided the domain part matches the suffix. In addition, when a suffix is set then hostnames without a domain part have the suffix added as an optional domain part. For instance, we can set domain=mylab.com and have a machine whose DHCP hostname is \"laptop\". The IP address for that machine is available both as \"laptop\" and \"laptop.mylab.com\".\n\n You can disable setting a domain by setting this option to an empty string.";
conf->dns.domain.a = cJSON_CreateStringReference("<any valid domain>");
conf->dns.domain.t = CONF_STRING;
conf->dns.domain.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.domain.d.s = (char*)"lan";
conf->dns.bogusPriv.k = "dns.bogusPriv";
conf->dns.bogusPriv.h = "Should all reverse lookups for private IP ranges (i.e., 192.168.x.y, etc) which are not found in /etc/hosts or the DHCP leases file be answered with \"no such domain\" rather than being forwarded upstream?";
conf->dns.bogusPriv.t = CONF_BOOL;
@ -528,7 +535,7 @@ void initConfig(struct config *conf)
conf->dns.cnameRecords.k = "dns.cnameRecords";
conf->dns.cnameRecords.h = "List of CNAME records which indicate that <cname> is really <target>. If the <TTL> is given, it overwrites the value of local-ttl";
conf->dns.cnameRecords.a = cJSON_CreateStringReference("Array of static leases each on in one of the following forms: \"<cname>,<target>[,<TTL>]\"");
conf->dns.cnameRecords.a = cJSON_CreateStringReference("Array of CNAMEs each on in one of the following forms: \"<cname>,<target>[,<TTL>]\"");
conf->dns.cnameRecords.t = CONF_JSON_STRING_ARRAY;
conf->dns.cnameRecords.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.cnameRecords.d.json = cJSON_CreateArray();
@ -686,32 +693,39 @@ void initConfig(struct config *conf)
conf->dhcp.start.k = "dhcp.start";
conf->dhcp.start.h = "Start address of the DHCP address pool";
conf->dhcp.start.a = cJSON_CreateStringReference("<ip-addr>, e.g., \"192.168.0.10\"");
conf->dhcp.start.t = CONF_STRING;
conf->dhcp.start.a = cJSON_CreateStringReference("<valid IPv4 address> or empty string (\"\"), e.g., \"192.168.0.10\"");
conf->dhcp.start.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.start.f = FLAG_RESTART_FTL;
conf->dhcp.start.d.s = (char*)"";
memset(&conf->dhcp.start.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.end.k = "dhcp.end";
conf->dhcp.end.h = "End address of the DHCP address pool";
conf->dhcp.end.a = cJSON_CreateStringReference("<ip-addr>, e.g., \"192.168.0.250\"");
conf->dhcp.end.t = CONF_STRING;
conf->dhcp.end.a = cJSON_CreateStringReference("<valid IPv4 address> or empty string (\"\"), e.g., \"192.168.0.250\"");
conf->dhcp.end.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.end.f = FLAG_RESTART_FTL;
conf->dhcp.end.d.s = (char*)"";
memset(&conf->dhcp.end.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.router.k = "dhcp.router";
conf->dhcp.router.h = "Address of the gateway to be used (typically the address of your router in a home installation)";
conf->dhcp.router.a = cJSON_CreateStringReference("<ip-addr>, e.g., \"192.168.0.1\"");
conf->dhcp.router.t = CONF_STRING;
conf->dhcp.router.a = cJSON_CreateStringReference("<valid IPv4 address> or empty string (\"\"), e.g., \"192.168.0.1\"");
conf->dhcp.router.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.router.f = FLAG_RESTART_FTL;
conf->dhcp.router.d.s = (char*)"";
memset(&conf->dhcp.router.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.domain.k = "dhcp.domain";
conf->dhcp.domain.h = "The DNS domain used by your Pi-hole";
conf->dhcp.domain.h = "The DNS domain used by your Pi-hole (*** DEPRECATED ***)\n This setting is deprecated and will be removed in a future version. Please use dns.domain instead. Setting it to any non-default value will overwrite the value of dns.domain if it is still set to its default value.";
conf->dhcp.domain.a = cJSON_CreateStringReference("<any valid domain>");
conf->dhcp.domain.t = CONF_STRING;
conf->dhcp.domain.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dhcp.domain.d.s = (char*)"lan";
conf->dhcp.netmask.k = "dhcp.netmask";
conf->dhcp.netmask.h = "The netmask used by your Pi-hole. For directly connected networks (i.e., networks on which the machine running Pi-hole has an interface) the netmask is optional and may be set to an empty string (\"\"): it will then be determined from the interface configuration itself. For networks which receive DHCP service via a relay agent, we cannot determine the netmask itself, so it should explicitly be specified, otherwise Pi-hole guesses based on the class (A, B or C) of the network address.";
conf->dhcp.netmask.a = cJSON_CreateStringReference("<any valid netmask> (e.g., \"255.255.255.0\") or empty string (\"\") for auto-discovery");
conf->dhcp.netmask.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.netmask.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
memset(&conf->dhcp.netmask.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.leaseTime.k = "dhcp.leaseTime";
conf->dhcp.leaseTime.h = "If the lease time is given, then leases will be given for that length of time. If not given, the default lease time is one hour for IPv4 and one day for IPv6.";
conf->dhcp.leaseTime.a = cJSON_CreateStringReference("The lease time can be in seconds, or minutes (e.g., \"45m\") or hours (e.g., \"1h\") or days (like \"2d\") or even weeks (\"1w\"). You may also use \"infinite\" as string but be aware of the drawbacks");
@ -824,13 +838,14 @@ void initConfig(struct config *conf)
conf->webserver.acl.k = "webserver.acl";
conf->webserver.acl.h = "Webserver access control list (ACL) allowing for restrictions to be put on the list of IP addresses which have access to the web server. The ACL is a comma separated list of IP subnets, where each subnet is prepended by either a - or a + sign. A plus sign means allow, where a minus sign means deny. If a subnet mask is omitted, such as -1.2.3.4, this means to deny only that single IP address. If this value is not set (empty string), all accesses are allowed. Otherwise, the default setting is to deny all accesses. On each request the full list is traversed, and the last (!) match wins. IPv6 addresses may be specified in CIDR-form [a:b::c]/64.\n\n Example 1: acl = \"+127.0.0.1,+[::1]\"\n ---> deny all access, except from 127.0.0.1 and ::1,\n Example 2: acl = \"+192.168.0.0/16\"\n ---> deny all accesses, except from the 192.168.0.0/16 subnet,\n Example 3: acl = \"+[::]/0\" ---> allow only IPv6 access.";
conf->webserver.acl.a = cJSON_CreateStringReference("<valid ACL>");
conf->webserver.acl.f = FLAG_ADVANCED_SETTING;
conf->webserver.acl.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.acl.t = CONF_STRING;
conf->webserver.acl.d.s = (char*)"";
conf->webserver.port.k = "webserver.port";
conf->webserver.port.h = "Ports to be used by the webserver.\n Comma-separated list of ports to listen on. It is possible to specify an IP address to bind to. In this case, an IP address and a colon must be prepended to the port number. For example, to bind to the loopback interface on port 80 (IPv4) and to all interfaces port 8080 (IPv4), use \"127.0.0.1:80,8080\". \"[::]:80\" can be used to listen to IPv6 connections to port 80. IPv6 addresses of network interfaces can be specified as well, e.g. \"[::1]:80\" for the IPv6 loopback interface. [::]:80 will bind to port 80 IPv6 only.\n In order to use port 80 for all interfaces, both IPv4 and IPv6, use either the configuration \"80,[::]:80\" (create one socket for IPv4 and one for IPv6 only), or \"+80\" (create one socket for both, IPv4 and IPv6). The + notation to use IPv4 and IPv6 will only work if no network interface is specified. Depending on your operating system version and IPv6 network environment, some configurations might not work as expected, so you have to test to find the configuration most suitable for your needs. In case \"+80\" does not work for your environment, you need to use \"80,[::]:80\".\n If the port is TLS/SSL, a letter 's' must be appended, for example, \"80,443s\" will open port 80 and port 443, and connections on port 443 will be encrypted. For non-encrypted ports, it is allowed to append letter 'r' (as in redirect). Redirected ports will redirect all their traffic to the first configured SSL port. For example, if webserver.port is \"80r,443s\", then all HTTP traffic coming at port 80 will be redirected to HTTPS port 443.";
conf->webserver.port.a = cJSON_CreateStringReference("comma-separated list of <[ip_address:]port>");
conf->webserver.port.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.port.t = CONF_STRING;
conf->webserver.port.d.s = (char*)"80,[::]:80,443s,[::]:443s";
@ -862,14 +877,14 @@ void initConfig(struct config *conf)
conf->webserver.paths.webroot.h = "Server root on the host";
conf->webserver.paths.webroot.a = cJSON_CreateStringReference("<valid path>");
conf->webserver.paths.webroot.t = CONF_STRING;
conf->webserver.paths.webroot.f = FLAG_ADVANCED_SETTING;
conf->webserver.paths.webroot.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.paths.webroot.d.s = (char*)"/var/www/html";
conf->webserver.paths.webhome.k = "webserver.paths.webhome";
conf->webserver.paths.webhome.h = "Sub-directory of the root containing the web interface";
conf->webserver.paths.webhome.a = cJSON_CreateStringReference("<valid subpath>, both slashes are needed!");
conf->webserver.paths.webhome.t = CONF_STRING;
conf->webserver.paths.webhome.f = FLAG_ADVANCED_SETTING;
conf->webserver.paths.webhome.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.paths.webhome.d.s = (char*)"/admin/";
// sub-struct interface
@ -903,6 +918,12 @@ void initConfig(struct config *conf)
conf->webserver.api.localAPIauth.t = CONF_BOOL;
conf->webserver.api.localAPIauth.d.b = true;
conf->webserver.api.max_sessions.k = "webserver.api.max_sessions";
conf->webserver.api.max_sessions.h = "Number of concurrent sessions allowed for the API. If the number of sessions exceeds this value, no new sessions will be allowed until the number of sessions drops due to session expiration or logout. Note that the number of concurrent sessions is irrelevant if authentication is disabled as no sessions are used in this case.";
conf->webserver.api.max_sessions.t = CONF_UINT16;
conf->webserver.api.max_sessions.d.u16 = 16;
conf->webserver.api.max_sessions.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.api.prettyJSON.k = "webserver.api.prettyJSON";
conf->webserver.api.prettyJSON.h = "Should FTL prettify the API output (add extra spaces, newlines and indentation)?";
conf->webserver.api.prettyJSON.t = CONF_BOOL;
@ -985,7 +1006,7 @@ void initConfig(struct config *conf)
conf->files.pid.h = "The file which contains the PID of FTL's main process.";
conf->files.pid.a = cJSON_CreateStringReference("<any writable file>");
conf->files.pid.t = CONF_STRING;
conf->files.pid.f = FLAG_ADVANCED_SETTING;
conf->files.pid.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.pid.d.s = (char*)"/run/pihole-FTL.pid";
conf->files.database.k = "files.database";
@ -999,7 +1020,7 @@ void initConfig(struct config *conf)
conf->files.gravity.h = "The location of Pi-hole's gravity database";
conf->files.gravity.a = cJSON_CreateStringReference("<any Pi-hole gravity database>");
conf->files.gravity.t = CONF_STRING;
conf->files.gravity.f = FLAG_ADVANCED_SETTING;
conf->files.gravity.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.gravity.d.s = (char*)"/etc/pihole/gravity.db";
conf->files.macvendor.k = "files.macvendor";
@ -1020,14 +1041,14 @@ void initConfig(struct config *conf)
conf->files.pcap.h = "An optional file containing a pcap capture of the network traffic. This file is used for debugging purposes only. If you don't know what this is, you don't need it.\n Setting this to an empty string disables pcap recording. The file must be writable by the user running FTL (typically pihole). Failure to write to this file will prevent the DNS resolver from starting. The file is appended to if it already exists.";
conf->files.pcap.a = cJSON_CreateStringReference("<any writable pcap file>");
conf->files.pcap.t = CONF_STRING;
conf->files.pcap.f = FLAG_ADVANCED_SETTING;
conf->files.pcap.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.pcap.d.s = (char*)"";
conf->files.log.webserver.k = "files.log.webserver";
conf->files.log.webserver.h = "The log file used by the webserver";
conf->files.log.webserver.a = cJSON_CreateStringReference("<any writable file>");
conf->files.log.webserver.t = CONF_STRING;
conf->files.log.webserver.f = FLAG_ADVANCED_SETTING;
conf->files.log.webserver.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.log.webserver.d.s = (char*)"/var/log/pihole/webserver.log";
// sub-struct files.log
@ -1037,7 +1058,7 @@ void initConfig(struct config *conf)
conf->files.log.dnsmasq.h = "The log file used by the embedded dnsmasq DNS server";
conf->files.log.dnsmasq.a = cJSON_CreateStringReference("<any writable file>");
conf->files.log.dnsmasq.t = CONF_STRING;
conf->files.log.dnsmasq.f = FLAG_ADVANCED_SETTING;
conf->files.log.dnsmasq.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.log.dnsmasq.d.s = (char*)"/var/log/pihole/pihole.log";
@ -1065,7 +1086,7 @@ void initConfig(struct config *conf)
conf->misc.nice.k = "misc.nice";
conf->misc.nice.h = "Set niceness of pihole-FTL. Defaults to -10 and can be disabled altogether by setting a value of -999. The nice value is an attribute that can be used to influence the CPU scheduler to favor or disfavor a process in scheduling decisions. The range of the nice value varies across UNIX systems. On modern Linux, the range is -20 (high priority = not very nice to other processes) to +19 (low priority).";
conf->misc.nice.t = CONF_INT;
conf->misc.nice.f = FLAG_ADVANCED_SETTING;
conf->misc.nice.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->misc.nice.d.i = -10;
conf->misc.addr2line.k = "misc.addr2line";
@ -1074,11 +1095,17 @@ void initConfig(struct config *conf)
conf->misc.addr2line.f = FLAG_ADVANCED_SETTING;
conf->misc.addr2line.d.b = true;
conf->misc.etc_dnsmasq_d.k = "misc.etc_dnsmasq_d";
conf->misc.etc_dnsmasq_d.h = "Should FTL load additional dnsmasq configuration files from /etc/dnsmasq.d/?";
conf->misc.etc_dnsmasq_d.t = CONF_BOOL;
conf->misc.etc_dnsmasq_d.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->misc.etc_dnsmasq_d.d.b = false;
conf->misc.dnsmasq_lines.k = "misc.dnsmasq_lines";
conf->misc.dnsmasq_lines.h = "Additional lines to inject into the generated dnsmasq configuration.\n Warning: This is an advanced setting and should only be used with care. Incorrectly formatted or duplicated lines as well as lines conflicting with the automatic configuration of Pi-hole can break the embedded dnsmasq and will stop DNS resolution from working.\n Use this option with extra care.";
conf->misc.dnsmasq_lines.a = cJSON_CreateStringReference("array of valid dnsmasq config line options");
conf->misc.dnsmasq_lines.t = CONF_JSON_STRING_ARRAY;
conf->misc.dnsmasq_lines.f = FLAG_RESTART_FTL;
conf->misc.dnsmasq_lines.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->misc.dnsmasq_lines.d.json = cJSON_CreateArray();
// sub-struct misc.check

View File

@ -33,6 +33,11 @@
// This static string represents an unchanged password
#define PASSWORD_VALUE "********"
// Remove the following line to disable the use of UTF-8 in the config file
// As consequence, the config file will be written in ASCII and all non-ASCII
// characters will be replaced by their UTF-8 escape sequences (UCS-2)
#define TOML_UTF8
union conf_value {
bool b; // boolean value
int i; // integer value
@ -88,7 +93,7 @@ enum conf_type {
#define FLAG_PSEUDO_ITEM (1 << 2)
#define FLAG_INVALIDATE_SESSIONS (1 << 3)
#define FLAG_WRITE_ONLY (1 << 4)
#define FLAG_ENV_VAR (1 << 5)
#define FLAG_ENV_VAR (1 << 5)
struct conf_item {
const char *k; // item Key
@ -125,6 +130,7 @@ struct config {
struct conf_item hosts;
struct conf_item domainNeeded;
struct conf_item expandHosts;
struct conf_item domain;
struct conf_item bogusPriv;
struct conf_item dnssec;
struct conf_item interface;
@ -178,6 +184,7 @@ struct config {
struct conf_item end;
struct conf_item router;
struct conf_item domain;
struct conf_item netmask;
struct conf_item leaseTime;
struct conf_item ipv6;
struct conf_item rapidCommit;
@ -226,6 +233,7 @@ struct config {
struct {
struct conf_item localAPIauth;
struct conf_item searchAPIauth;
struct conf_item max_sessions;
struct conf_item prettyJSON;
struct conf_item pwhash;
struct conf_item password; // This is a pseudo-item
@ -261,6 +269,7 @@ struct config {
struct conf_item delay_startup;
struct conf_item nice;
struct conf_item addr2line;
struct conf_item etc_dnsmasq_d;
struct conf_item dnsmasq_lines;
struct {
struct conf_item load;

View File

@ -207,11 +207,12 @@ static void write_config_header(FILE *fp, const char *description)
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "ANY CHANGES MADE TO THIS FILE WILL BE LOST WHEN THE CONFIGURATION CHANGES");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "IF YOU WISH TO CHANGE ANY OF THESE VALUES, CHANGE THEM IN");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "etc/pihole/pihole.toml");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "/etc/pihole/pihole.toml");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "and restart pihole-FTL");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "ANY OTHER CHANGES SHOULD BE MADE IN A SEPARATE CONFIG FILE");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "WITHIN /etc/dnsmasq.d/yourname.conf");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "(make sure misc.etc_dnsmasq_d is set to true in /etc/pihole/pihole.toml)");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "");
CONFIG_CENTER(fp, HEADER_WIDTH, "Last updated: %s", timestring);
CONFIG_CENTER(fp, HEADER_WIDTH, "by FTL version %s", get_FTL_version());
@ -221,6 +222,73 @@ static void write_config_header(FILE *fp, const char *description)
bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_config, char errbuf[ERRBUF_SIZE])
{
// Early config checks
if(conf->dhcp.active.v.b)
{
// Check if the addresses are valid
// The addresses should neither be 0.0.0.0 nor 255.255.255.255
if((ntohl(conf->dhcp.start.v.in_addr.s_addr) == 0) ||
(ntohl(conf->dhcp.start.v.in_addr.s_addr) == 0xFFFFFFFF))
{
strncpy(errbuf, "DHCP start address is not valid", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
if((ntohl(conf->dhcp.end.v.in_addr.s_addr) == 0) ||
(ntohl(conf->dhcp.end.v.in_addr.s_addr) == 0xFFFFFFFF))
{
strncpy(errbuf, "DHCP end address is not valid", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
if((ntohl(conf->dhcp.router.v.in_addr.s_addr) == 0) ||
(ntohl(conf->dhcp.router.v.in_addr.s_addr) == 0xFFFFFFFF))
{
strncpy(errbuf, "DHCP router address is not valid", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
// The addresses should neither end in .0 or .255 in the last octet
if((ntohl(conf->dhcp.start.v.in_addr.s_addr) & 0xFF) == 0 ||
(ntohl(conf->dhcp.start.v.in_addr.s_addr) & 0xFF) == 0xFF)
{
strncpy(errbuf, "DHCP start address is not valid", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
if((ntohl(conf->dhcp.end.v.in_addr.s_addr) & 0xFF) == 0 ||
(ntohl(conf->dhcp.end.v.in_addr.s_addr) & 0xFF) == 0xFF)
{
strncpy(errbuf, "DHCP end address is not valid", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
if((ntohl(conf->dhcp.router.v.in_addr.s_addr) & 0xFF) == 0 ||
(ntohl(conf->dhcp.router.v.in_addr.s_addr) & 0xFF) == 0xFF)
{
strncpy(errbuf, "DHCP router address is not valid", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
// Check if the DHCP range is valid (start needs to be smaller than end)
if(ntohl(conf->dhcp.start.v.in_addr.s_addr) >= ntohl(conf->dhcp.end.v.in_addr.s_addr))
{
strncpy(errbuf, "DHCP range start address is larger than or equal to the end address", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
// Check if the router address is within the DHCP range
if(ntohl(conf->dhcp.router.v.in_addr.s_addr) >= ntohl(conf->dhcp.start.v.in_addr.s_addr) &&
ntohl(conf->dhcp.router.v.in_addr.s_addr) <= ntohl(conf->dhcp.end.v.in_addr.s_addr))
{
strncpy(errbuf, "DHCP router address should not be within DHCP range", ERRBUF_SIZE);
log_err("Unable to update dnsmasq configuration: %s", errbuf);
return false;
}
}
log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_TEMP_CONF" for writing");
FILE *pihole_conf = fopen(DNSMASQ_TEMP_CONF, "w");
// Return early if opening failed
@ -240,13 +308,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
write_config_header(pihole_conf, "Dnsmasq config for Pi-hole's FTLDNS");
fputs("addn-hosts=/etc/pihole/local.list\n", pihole_conf);
fputs("addn-hosts="DNSMASQ_CUSTOM_LIST"\n", pihole_conf);
fputs("hostsdir="DNSMASQ_HOSTSDIR"\n", pihole_conf);
fputs("\n", pihole_conf);
fputs("# Don't read /etc/resolv.conf. Get upstream servers only from the configuration\n", pihole_conf);
fputs("no-resolv\n", pihole_conf);
fputs("\n", pihole_conf);
fputs("# DNS port to be used\n", pihole_conf);
fprintf(pihole_conf, "port=%u\n", conf->dns.port.v.u16);
fputs("\n", pihole_conf);
if(cJSON_GetArraySize(conf->dns.upstreams.v.json) > 0)
{
fputs("# List of upstream DNS server\n", pihole_conf);
@ -278,12 +347,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
fputs("# Enable query logging\n", pihole_conf);
fputs("log-queries\n", pihole_conf);
fputs("log-async\n", pihole_conf);
fputs("\n", pihole_conf);
}
else
{
fputs("# Disable query logging\n", pihole_conf);
fputs("#log-queries\n", pihole_conf);
fputs("#log-async\n", pihole_conf);
fputs("\n", pihole_conf);
}
if(strlen(conf->files.log.dnsmasq.v.s) > 0)
@ -334,12 +405,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
{
fputs("# Add A, AAAA and PTR records to the DNS\n", pihole_conf);
fprintf(pihole_conf, "host-record=%s\n", conf->dns.hostRecord.v.s);
fputs("\n", pihole_conf);
}
if(conf->dns.cache.optimizer.v.ui > 0u)
{
fputs("# Use stale cache entries for a given number of seconds to optimize cache utilization\n", pihole_conf);
fprintf(pihole_conf, "use-stale-cache=%u\n", conf->dns.cache.optimizer.v.ui);
fputs("\n", pihole_conf);
}
const char *interface = conf->dns.interface.v.s;
@ -402,16 +475,18 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
fputs("# Never forward A or AAAA queries for plain names, without\n",pihole_conf);
fputs("# dots or domain parts, to upstream nameservers. If the name\n", pihole_conf);
fputs("# is not known from /etc/hosts or DHCP a NXDOMAIN is returned\n", pihole_conf);
fprintf(pihole_conf, "local=/%s/\n",
conf->dhcp.domain.v.s);
fputs("\n", pihole_conf);
if(strlen(conf->dns.domain.v.s))
fprintf(pihole_conf, "local=/%s/\n\n", conf->dns.domain.v.s);
else
fputs("\n", pihole_conf);
}
if(strlen(conf->dhcp.domain.v.s) > 0 && strcasecmp("none", conf->dhcp.domain.v.s) != 0)
// Add domain to DNS server. It will also be used for DHCP if the DHCP
// server is enabled below
if(strlen(conf->dns.domain.v.s) > 0)
{
fputs("# DNS domain for the DHCP server\n", pihole_conf);
fprintf(pihole_conf, "domain=%s\n", conf->dhcp.domain.v.s);
fputs("\n", pihole_conf);
fputs("# DNS domain for both the DNS and DHCP server\n", pihole_conf);
fprintf(pihole_conf, "domain=%s\n\n", conf->dns.domain.v.s);
}
if(conf->dhcp.active.v.b)
@ -419,12 +494,25 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
fputs("# DHCP server setting\n", pihole_conf);
fputs("dhcp-authoritative\n", pihole_conf);
fputs("dhcp-leasefile="DHCPLEASESFILE"\n", pihole_conf);
fprintf(pihole_conf, "dhcp-range=%s,%s,%s\n",
conf->dhcp.start.v.s,
conf->dhcp.end.v.s,
conf->dhcp.leaseTime.v.s);
fprintf(pihole_conf, "dhcp-option=option:router,%s\n",
conf->dhcp.router.v.s);
char start[INET_ADDRSTRLEN] = { 0 },
end[INET_ADDRSTRLEN] = { 0 },
router[INET_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET, &conf->dhcp.start.v.in_addr, start, INET_ADDRSTRLEN);
inet_ntop(AF_INET, &conf->dhcp.end.v.in_addr, end, INET_ADDRSTRLEN);
inet_ntop(AF_INET, &conf->dhcp.router.v.in_addr, router, INET_ADDRSTRLEN);
fprintf(pihole_conf, "dhcp-range=%s,%s", start, end);
// Net mask is optional, only add if it is not 0.0.0.0
const struct in_addr inaddr_empty = {0};
if(memcmp(&conf->dhcp.netmask.v.in_addr, &inaddr_empty, sizeof(inaddr_empty)) != 0)
{
char netmask[INET_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET, &conf->dhcp.netmask.v.in_addr, netmask, INET_ADDRSTRLEN);
fprintf(pihole_conf, ",%s", netmask);
}
// Lease time is optional, only add it if it is set
if(strlen(conf->dhcp.leaseTime.v.s) > 0)
fprintf(pihole_conf, ",%s", conf->dhcp.leaseTime.v.s);
fprintf(pihole_conf, "\ndhcp-option=option:router,%s\n", router);
if(conf->dhcp.rapidCommit.v.b)
fputs("dhcp-rapid-commit\n", pihole_conf);
@ -501,25 +589,27 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
fputs("# Pi-hole implements this via the dnsmasq option \"bogus-priv\" above\n", pihole_conf);
fputs("# (if enabled!) as this option also covers IPv6.\n", pihole_conf);
fputs("\n", pihole_conf);
fputs("# OpenWRT furthermore blocks bind, local, onion domains\n", pihole_conf);
fputs("# OpenWRT furthermore blocks bind, local, onion domains\n", pihole_conf);
fputs("# see https://git.openwrt.org/?p=openwrt/openwrt.git;a=blob_plain;f=package/network/services/dnsmasq/files/rfc6761.conf;hb=HEAD\n", pihole_conf);
fputs("# and https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml\n", pihole_conf);
fputs("# We do not include the \".local\" rule ourselves, see https://github.com/pi-hole/pi-hole/pull/4282#discussion_r689112972\n", pihole_conf);
fputs("server=/bind/\n", pihole_conf);
fputs("server=/onion/\n", pihole_conf);
fputs("\n", pihole_conf);
if(directory_exists("/etc/dnsmasq.d"))
if(directory_exists("/etc/dnsmasq.d") && conf->misc.etc_dnsmasq_d.v.b)
{
// Load possible additional user scripts from /etc/dnsmasq.d if
// the directory exists (it may not, e.g., in a container)
fputs("# Load possible additional user scripts\n", pihole_conf);
// Load additional user scripts from /etc/dnsmasq.d if the
// directory exists (it may not, e.g., in a container)
fputs("# Load additional user scripts\n", pihole_conf);
fputs("conf-dir=/etc/dnsmasq.d\n", pihole_conf);
fputs("\n", pihole_conf);
}
// Add option for caching all DNS records
fputs("# Cache all DNS records\n", pihole_conf);
fputs("cache-rr=ANY\n\n", pihole_conf);
fputs("cache-rr=ANY\n", pihole_conf);
fputs("\n", pihole_conf);
// Add option for PCAP file recording
if(strlen(conf->files.pcap.v.s) > 0)
@ -565,6 +655,13 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
return false;
}
// Close file
if(fclose(pihole_conf) != 0)
{
log_err("Cannot close dnsmasq config file: %s", strerror(errno));
return false;
}
log_debug(DEBUG_CONFIG, "Testing "DNSMASQ_TEMP_CONF);
if(test_config && !test_dnsmasq_config(errbuf))
{
@ -572,21 +669,26 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
return false;
}
// Rotate old config files
rotate_files(DNSMASQ_PH_CONFIG, NULL);
log_debug(DEBUG_CONFIG, "Installing "DNSMASQ_TEMP_CONF" to "DNSMASQ_PH_CONFIG);
if(rename(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG) != 0)
// Check if the new config file is different from the old one
// Skip the first 24 lines as they contain the header
if(files_different(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG, 24))
{
log_err("Cannot install dnsmasq config file: %s", strerror(errno));
return false;
if(rename(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG) != 0)
{
log_err("Cannot install dnsmasq config file: %s", strerror(errno));
return false;
}
log_debug(DEBUG_CONFIG, "Config file written to "DNSMASQ_PH_CONFIG);
}
// Close file
if(fclose(pihole_conf) != 0)
else
{
log_err("Cannot close dnsmasq config file: %s", strerror(errno));
return false;
log_debug(DEBUG_CONFIG, "dnsmasq.conf unchanged");
// Remove temporary config file
if(remove(DNSMASQ_TEMP_CONF) != 0)
{
log_err("Cannot remove temporary dnsmasq config file: %s", strerror(errno));
return false;
}
}
return true;
}
@ -719,8 +821,8 @@ bool read_legacy_cnames_config(void)
bool read_legacy_custom_hosts_config(void)
{
// Check if file exists, if not, there is nothing to do
const char *path = DNSMASQ_CUSTOM_LIST;
const char *target = DNSMASQ_CUSTOM_LIST".bck";
const char *path = DNSMASQ_CUSTOM_LIST_LEGACY;
const char *target = DNSMASQ_CUSTOM_LIST_LEGACY".bck";
if(!file_exists(path))
return true;
@ -782,22 +884,30 @@ bool read_legacy_custom_hosts_config(void)
bool write_custom_list(void)
{
// Rotate old hosts files
rotate_files(DNSMASQ_CUSTOM_LIST, NULL);
// Ensure that the directory exists
if(!directory_exists(DNSMASQ_HOSTSDIR))
{
log_debug(DEBUG_CONFIG, "Creating directory "DNSMASQ_HOSTSDIR);
if(mkdir(DNSMASQ_HOSTSDIR, 0755) != 0)
{
log_err("Cannot create directory "DNSMASQ_HOSTSDIR": %s", strerror(errno));
return false;
}
}
log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_CUSTOM_LIST" for writing");
FILE *custom_list = fopen(DNSMASQ_CUSTOM_LIST, "w");
log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_CUSTOM_LIST_LEGACY".tmp for writing");
FILE *custom_list = fopen(DNSMASQ_CUSTOM_LIST_LEGACY".tmp", "w");
// Return early if opening failed
if(!custom_list)
{
log_err("Cannot open "DNSMASQ_CUSTOM_LIST" for writing, unable to update custom.list: %s", strerror(errno));
log_err("Cannot open "DNSMASQ_CUSTOM_LIST_LEGACY".tmp for writing, unable to update custom.list: %s", strerror(errno));
return false;
}
// Lock file, may block if the file is currently opened
if(flock(fileno(custom_list), LOCK_EX) != 0)
{
log_err("Cannot open "DNSMASQ_CUSTOM_LIST" in exclusive mode: %s", strerror(errno));
log_err("Cannot open "DNSMASQ_CUSTOM_LIST_LEGACY".tmp in exclusive mode: %s", strerror(errno));
fclose(custom_list);
return false;
}
@ -838,5 +948,28 @@ bool write_custom_list(void)
log_err("Cannot close custom.list: %s", strerror(errno));
return false;
}
// Check if the new config file is different from the old one
// Skip the first 24 lines as they contain the header
if(files_different(DNSMASQ_CUSTOM_LIST_LEGACY".tmp", DNSMASQ_CUSTOM_LIST, 24))
{
if(rename(DNSMASQ_CUSTOM_LIST_LEGACY".tmp", DNSMASQ_CUSTOM_LIST) != 0)
{
log_err("Cannot install custom.list: %s", strerror(errno));
return false;
}
log_debug(DEBUG_CONFIG, "HOSTS file written to "DNSMASQ_CUSTOM_LIST);
}
else
{
log_debug(DEBUG_CONFIG, "custom.list unchanged");
// Remove temporary config file
if(remove(DNSMASQ_CUSTOM_LIST_LEGACY".tmp") != 0)
{
log_err("Cannot remove temporary custom.list: %s", strerror(errno));
return false;
}
}
return true;
}

View File

@ -26,7 +26,9 @@ bool write_custom_list(void);
#define DNSMASQ_TEMP_CONF "/etc/pihole/dnsmasq.conf.temp"
#define DNSMASQ_STATIC_LEASES "/etc/pihole/04-pihole-static-dhcp.conf"
#define DNSMASQ_CNAMES "/etc/pihole/05-pihole-custom-cname.conf"
#define DNSMASQ_CUSTOM_LIST "/etc/pihole/custom.list"
#define DNSMASQ_HOSTSDIR "/etc/pihole/hosts"
#define DNSMASQ_CUSTOM_LIST DNSMASQ_HOSTSDIR"/custom.list"
#define DNSMASQ_CUSTOM_LIST_LEGACY "/etc/pihole/custom.list"
#define DHCPLEASESFILE "/etc/pihole/dhcp.leases"
#endif //DNSMASQ_CONFIG_H

View File

@ -47,6 +47,10 @@ FILE * __attribute((malloc)) __attribute((nonnull(1))) openFTLtoml(const char *m
{
// Use global config file
strncpy(filename, GLOBALTOMLPATH, sizeof(filename));
// Append ".tmp" if we are writing
if(mode[0] == 'w')
strncat(filename, ".tmp", sizeof(filename));
}
else
{
@ -148,17 +152,51 @@ static void printTOMLstring(FILE *fp, const char *s, const bool toml)
continue;
}
// Escape special characters
// Escape special characters with simple escape sequences
switch (ch) {
case 0x08: fprintf(fp, "\\b"); continue;
case 0x09: fprintf(fp, "\\t"); continue;
case 0x0a: fprintf(fp, "\\n"); continue;
case 0x0c: fprintf(fp, "\\f"); continue;
case 0x0d: fprintf(fp, "\\r"); continue;
case '"': fprintf(fp, "\\\""); continue;
case '\\': fprintf(fp, "\\\\"); continue;
default: fprintf(fp, "\\0x%02x", ch & 0xff); continue;
case '\b': fputs("\\b", fp); continue;
case '\t': fputs("\\t", fp); continue;
case '\n': fputs("\\n", fp); continue;
case '\f': fputs("\\f", fp); continue;
case '\r': fputs("\\r", fp); continue;
case '"': fputs("\\\"", fp); continue;
case '\\': fputs("\\\\", fp); continue;
}
#ifndef TOML_UTF8
// The Universal Coded Character Set (UCS, Unicode) is a
// standard set of characters defined by the international
// standard ISO/IEC 10646, Information technology — Universal
// Coded Character Set (UCS) (plus amendments to that standard),
// which is the basis of many character encodings, improving as
// characters from previously unrepresented typing systems are
// added.
// The following code converts a UTF-8 character to UCS and
// prints it as \UXXXXXXXX
int64_t ucs;
int bytes = toml_utf8_to_ucs(s, len, &ucs);
if(bytes > 0)
{
// Print 4-byte UCS as \UXXXXXXXX
fprintf(fp, "\\U%08X", (uint32_t)ucs);
// Advance string pointer
s += bytes - 1;
// Decrease remaining string length
len -= bytes - 1;
continue;
}
#else
// Escape all other control characters as short 2-byte
// UCS sequences
if(iscntrl(ch))
{
fprintf(fp, "\\u%04X", ch);
continue;
}
// Print remaining characters as is
putc(ch, fp);
#endif
}
if(toml) fprintf(fp, "\"");
}
@ -389,6 +427,13 @@ void writeTOMLvalue(FILE * fp, const int indent, const enum conf_type t, union c
break;
case CONF_STRUCT_IN_ADDR:
{
// Special case: 0.0.0.0 -> return empty string
if(v->in_addr.s_addr == INADDR_ANY)
{
printTOMLstring(fp, "", toml);
break;
}
// else: normal address
char addr4[INET_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET, &v->in_addr, addr4, INET_ADDRSTRLEN);
printTOMLstring(fp, addr4, toml);
@ -396,6 +441,13 @@ void writeTOMLvalue(FILE * fp, const int indent, const enum conf_type t, union c
}
case CONF_STRUCT_IN6_ADDR:
{
// Special case: :: -> return empty string
if(memcmp(&v->in6_addr, &in6addr_any, sizeof(in6addr_any)) == 0)
{
printTOMLstring(fp, "", toml);
break;
}
// else: normal address
char addr6[INET6_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET6, &v->in6_addr, addr6, INET6_ADDRSTRLEN);
printTOMLstring(fp, addr6, toml);
@ -679,7 +731,12 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t
const toml_datum_t val = toml_string_in(toml, key);
if(val.ok)
{
if(inet_pton(AF_INET, val.u.s, &addr4))
if(strlen(val.u.s) == 0)
{
// Special case: empty string -> 0.0.0.0
conf_item->v.in_addr.s_addr = INADDR_ANY;
}
else if(inet_pton(AF_INET, val.u.s, &addr4))
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
else
log_warn("Config %s is invalid (not of type IPv4 address)", conf_item->k);
@ -695,7 +752,12 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t
const toml_datum_t val = toml_string_in(toml, key);
if(val.ok)
{
if(inet_pton(AF_INET6, val.u.s, &addr6))
if(strlen(val.u.s) == 0)
{
// Special case: empty string -> ::
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
}
else if(inet_pton(AF_INET6, val.u.s, &addr6))
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
else
log_warn("Config %s is invalid (not of type IPv6 address)", conf_item->k);
@ -935,7 +997,12 @@ bool readEnvValue(struct conf_item *conf_item, struct config *newconf)
case CONF_STRUCT_IN_ADDR:
{
struct in_addr addr4 = { 0 };
if(inet_pton(AF_INET, envvar, &addr4))
if(strlen(envvar) == 0)
{
// Special case: empty string -> 0.0.0.0
conf_item->v.in_addr.s_addr = INADDR_ANY;
}
else if(inet_pton(AF_INET, envvar, &addr4))
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
else
log_warn("ENV %s is invalid (not of type IPv4 address)", envkey);
@ -944,7 +1011,12 @@ bool readEnvValue(struct conf_item *conf_item, struct config *newconf)
case CONF_STRUCT_IN6_ADDR:
{
struct in6_addr addr6 = { 0 };
if(inet_pton(AF_INET6, envvar, &addr6))
if(strlen(envvar) == 0)
{
// Special case: empty string -> ::
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
}
else if(inet_pton(AF_INET6, envvar, &addr6))
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
else
log_warn("ENV %s is invalid (not of type IPv6 address)", envkey);

View File

@ -141,7 +141,7 @@ static toml_table_t *parseTOML(const unsigned int version)
FILE *fp;
if((fp = openFTLtoml("r", version)) == NULL)
{
log_warn("No config file available (%s), using defaults",
log_info("No config file available (%s), using defaults",
strerror(errno));
return NULL;
}

View File

@ -19,39 +19,55 @@
#include "datastructure.h"
// watch_config()
#include "config/inotify.h"
// files_different()
#include "files.h"
static void migrate_config(void)
{
// Migrating dhcp.domain -> dns.domain
if(strcmp(config.dns.domain.v.s, config.dns.domain.d.s) == 0)
{
// If the domain is the same as the default, check if the dhcp domain
// is different from the default. If so, migrate it
if(strcmp(config.dhcp.domain.v.s, config.dhcp.domain.d.s) != 0)
{
// Migrate dhcp.domain -> dns.domain
log_info("Migrating dhcp.domain = \"%s\" -> dns.domain", config.dhcp.domain.v.s);
if(config.dns.domain.t == CONF_STRING_ALLOCATED)
free(config.dns.domain.v.s);
config.dns.domain.v.s = strdup(config.dhcp.domain.v.s);
config.dns.domain.t = CONF_STRING_ALLOCATED;
}
}
}
bool writeFTLtoml(const bool verbose)
{
// Stop watching for changes in the config file
watch_config(false);
// Try to open global config file
// Try to open a temporary config file for writing
FILE *fp;
if((fp = openFTLtoml("w", 0)) == NULL)
{
log_warn("Cannot write to FTL config file (%s), content not updated", strerror(errno));
// Restart watching for changes in the config file
watch_config(true);
return false;
}
// Log that we are (re-)writing the config file if either in verbose or
// debug mode
if(verbose || config.debug.config.v.b)
log_info("Writing config file");
// Write header
fputs("# This file is managed by pihole-FTL\n#\n", fp);
fputs("# Do not edit the file while FTL is\n", fp);
fputs("# running or your changes may be overwritten\n#\n", fp);
fprintf(fp, "# Pi-hole configuration file (%s)\n", get_FTL_version());
#ifdef TOML_UTF8
fputs("# Encoding: UTF-8\n", fp);
#else
fputs("# Encoding: ASCII + UCS\n", fp);
#endif
fputs("# This file is managed by pihole-FTL\n", fp);
char timestring[TIMESTR_SIZE] = "";
get_timestr(timestring, time(NULL), false, false);
fputs("# Last updated on ", fp);
fputs(timestring, fp);
fputs("\n# by FTL ", fp);
fputs(get_FTL_version(), fp);
fputs("\n\n", fp);
// Perform possible config migration
migrate_config();
// Iterate over configuration and store it into the file
char *last_path = (char*)"";
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)
@ -119,8 +135,46 @@ bool writeFTLtoml(const bool verbose)
// Close file and release exclusive lock
closeFTLtoml(fp);
// Restart watching for changes in the config file
watch_config(true);
// Move temporary file to the final location if it is different
// We skip the first 8 lines as they contain the header and will always
// be different
if(files_different(GLOBALTOMLPATH".tmp", GLOBALTOMLPATH, 8))
{
// Stop watching for changes in the config file
watch_config(false);
// Rotate config file
rotate_files(GLOBALTOMLPATH, NULL);
// Move file
if(rename(GLOBALTOMLPATH".tmp", GLOBALTOMLPATH) != 0)
{
log_warn("Cannot move temporary config file to final location (%s), content not updated", strerror(errno));
// Restart watching for changes in the config file
watch_config(true);
return false;
}
// Restart watching for changes in the config file
watch_config(true);
// Log that we have written the config file if either in verbose or
// debug mode
if(verbose || config.debug.config.v.b)
log_info("Config file written to %s", GLOBALTOMLPATH);
}
else
{
// Remove temporary file
if(unlink(GLOBALTOMLPATH".tmp") != 0)
{
log_warn("Cannot remove temporary config file (%s), content not updated", strerror(errno));
return false;
}
// Log that the config file has not changed if in debug mode
log_debug(DEBUG_CONFIG, "pihole.toml unchanged");
}
return true;
}

View File

@ -35,6 +35,8 @@
#include "webserver/webserver.h"
// free_api()
#include "api/api.h"
// setlocale()
#include <locale.h>
pthread_t threads[THREADS_MAX] = { 0 };
bool resolver_ready = false;
@ -444,3 +446,16 @@ bool ipv6_enabled(void)
// IPv6-capable interface
return true;
}
void init_locale(void)
{
// Set locale to system default, needed for libidn to work properly
// Without this, libidn will not be able to convert UTF-8 to ASCII
// (error message "Character encoding conversion error")
setlocale(LC_ALL, "");
// Set locale for numeric values to C to ensure that we always use
// the dot as decimal separator (even if the system locale uses a
// comma, e.g., in German)
setlocale(LC_NUMERIC, "C");
}

View File

@ -24,6 +24,7 @@ void set_nice(void);
void calc_cpu_usage(void);
float get_cpu_percentage(void) __attribute__((pure));
bool ipv6_enabled(void);
void init_locale(void);
#include <sys/syscall.h>
#include <unistd.h>

View File

@ -1663,8 +1663,8 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
querystr = "INSERT INTO client (ip,comment) VALUES (:item,:comment) "\
"ON CONFLICT(ip) DO UPDATE SET comment = :comment;";
else // domainlist
querystr = "INSERT INTO domainlist (domain,type,enabled,comment) VALUES (:item,:type,:enabled,:comment) "\
"ON CONFLICT(domain) DO UPDATE SET type = :type, enabled = :enabled, comment = :comment;";
querystr = "INSERT INTO domainlist (domain,type,enabled,comment) VALUES (:item,:oldtype,:enabled,:comment) "\
"ON CONFLICT(domain,type) DO UPDATE SET type = :type, enabled = :enabled, comment = :comment;";
}
int rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &stmt, NULL);
@ -1672,7 +1672,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s) - SQL error prepare (%i): %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
return false;
}
@ -1694,7 +1694,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind name (error %d) - %s",
row->type_int, row->name, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1706,7 +1706,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind type (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1727,7 +1727,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
// Error, one is not meaningful without the other
*message = "Field type missing from request";
log_err("gravityDB_addToTable(%d, %s): type missing",
row->type_int, row->domain);
row->type_int, row->item);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1737,7 +1737,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
// Error, one is not meaningful without the other
*message = "Field oldkind missing from request";
log_err("gravityDB_addToTable(%d, %s): Oldkind missing",
row->type_int, row->domain);
row->type_int, row->item);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1745,7 +1745,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
else
{
if(strcasecmp("allow", row->type) == 0 &&
strcasecmp("exact", row->kind) == 0)
strcasecmp("exact", row->kind) == 0)
oldtype = 0;
else if(strcasecmp("deny", row->type) == 0 &&
strcasecmp("exact", row->kind) == 0)
@ -1760,7 +1760,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = "Cannot interpret type/kind";
log_err("gravityDB_addToTable(%d, %s): Failed to identify type=\"%s\", kind=\"%s\"",
row->type_int, row->domain, row->type, row->kind);
row->type_int, row->item, row->type, row->kind);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1772,7 +1772,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind oldtype (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1785,7 +1785,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind enabled (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1797,7 +1797,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind comment (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;

View File

@ -54,6 +54,8 @@ static const char *get_message_type_str(const enum message_type type)
return "LIST";
case DISK_MESSAGE_EXTENDED:
return "DISK_EXTENDED";
case CERTIFICATE_DOMAIN_MISMATCH_MESSAGE:
return "CERTIFICATE_DOMAIN_MISMATCH";
case MAX_MESSAGE:
default:
return "UNKNOWN";
@ -84,6 +86,8 @@ static enum message_type get_message_type_from_string(const char *typestr)
return INACCESSIBLE_ADLIST_MESSAGE;
else if (strcmp(typestr, "DISK_EXTENDED") == 0)
return DISK_MESSAGE_EXTENDED;
else if (strcmp(typestr, "CERTIFICATE_DOMAIN_MISMATCH") == 0)
return CERTIFICATE_DOMAIN_MISMATCH_MESSAGE;
else
return MAX_MESSAGE;
}
@ -167,6 +171,14 @@ static unsigned char message_blob_types[MAX_MESSAGE][5] =
SQLITE_TEXT, // File system type
SQLITE_TEXT, // Directory mounted on
SQLITE_NULL // not used
},
{
// CERTIFICATE_DOMAIN_MISMATCH_MESSAGE: The message column contains the certificate file
SQLITE_TEXT, // domain
SQLITE_NULL, // not used
SQLITE_NULL, // not used
SQLITE_NULL, // not used
SQLITE_NULL // not used
}
};
// Create message table in the database
@ -333,6 +345,8 @@ static int add_message(const enum message_type type,
case SQLITE_NULL: /* Fall through */
default:
log_warn("add_message(type=%s, message=%s) - Excess property, binding NULL",
get_message_type_str(type), message);
rc = sqlite3_bind_null(stmt, 3 + j);
break;
}
@ -653,6 +667,28 @@ static void format_inaccessible_adlist_message(char *plain, const int sizeof_pla
free(escaped_address);
}
static void format_certificate_domain_mismatch(char *plain, const int sizeof_plain, char *html, const int sizeof_html,
const char *certfile, const char*domain)
{
if(snprintf(plain, sizeof_plain, "SSL/TLS certificate %s does not match domain %s!", certfile, domain) > sizeof_plain)
log_warn("format_certificate_domain_mismatch(): Buffer too small to hold plain message, warning truncated");
// Return early if HTML text is not required
if(sizeof_html < 1 || html == NULL)
return;
char *escaped_certfile = escape_html(certfile);
char *escaped_domain = escape_html(domain);
if(snprintf(html, sizeof_html, "SSL/TLS certificate %s does not match domain <strong>%s</strong>!", escaped_certfile, escaped_domain) > sizeof_html)
log_warn("format_certificate_domain_mismatch(): Buffer too small to hold HTML message, warning truncated");
if(escaped_certfile != NULL)
free(escaped_certfile);
if(escaped_domain != NULL)
free(escaped_domain);
}
int count_messages(const bool filter_dnsmasq_warnings)
{
int count = 0;
@ -876,6 +912,17 @@ bool format_messages(cJSON *array)
break;
}
case CERTIFICATE_DOMAIN_MISMATCH_MESSAGE:
{
const char *certfile = (const char*)sqlite3_column_text(stmt, 3);
const char *domain = (const char*)sqlite3_column_text(stmt, 4);
format_certificate_domain_mismatch(plain, sizeof(plain), html, sizeof(html),
certfile, domain);
break;
}
}
// Add the plain message
@ -1095,3 +1142,19 @@ void logg_inaccessible_adlist(const int dbindex, const char *address)
if(rowid == -1)
log_err("logg_inaccessible_adlist(): Failed to add message to database");
}
void log_certificate_domain_mismatch(const char *certfile, const char *domain)
{
// Create message
char buf[2048];
format_certificate_domain_mismatch(buf, sizeof(buf), NULL, 0, certfile, domain);
// Log to FTL.log
log_warn("%s", buf);
// Log to database
const int rowid = add_message(CERTIFICATE_DOMAIN_MISMATCH_MESSAGE, certfile, 1, domain);
if(rowid == -1)
log_err("log_certificate_domain_mismatch(): Failed to add message to database");
}

View File

@ -28,5 +28,6 @@ void logg_rate_limit_message(const char *clientIP, const unsigned int rate_limit
void logg_warn_dnsmasq_message(char *message);
void log_resource_shortage(const double load, const int nprocs, const int shmem, const int disk, const char *path, const char *msg);
void logg_inaccessible_adlist(const int dbindex, const char *address);
void log_certificate_domain_mismatch(const char *certfile, const char *domain);
#endif //MESSAGETABLE_H

View File

@ -64,7 +64,7 @@ bool add_session_app_column(sqlite3 *db)
}
// Store all session in database
bool backup_db_sessions(struct session *sessions)
bool backup_db_sessions(struct session *sessions, const uint16_t max_sessions)
{
if(!config.webserver.session.restore.v.b)
{
@ -89,7 +89,7 @@ bool backup_db_sessions(struct session *sessions)
}
unsigned int api_sessions = 0;
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
for(unsigned int i = 0; i < max_sessions; i++)
{
// Get session
struct session *sess = &sessions[i];
@ -198,8 +198,8 @@ bool backup_db_sessions(struct session *sessions)
return false;
}
log_info("Stored %u API session%s in the database",
api_sessions, api_sessions == 1 ? "" : "s");
log_info("Stored %u/%u API session%s in the database",
api_sessions, max_sessions, max_sessions == 1 ? "" : "s");
// Close database connection
dbclose(&db);
@ -208,7 +208,7 @@ bool backup_db_sessions(struct session *sessions)
}
// Restore all sessions found in the database
bool restore_db_sessions(struct session *sessions)
bool restore_db_sessions(struct session *sessions, const uint16_t max_sessions)
{
if(!config.webserver.session.restore.v.b)
{
@ -237,7 +237,7 @@ bool restore_db_sessions(struct session *sessions)
// Iterate over all still valid sessions
unsigned int i = 0;
while(sqlite3_step(stmt) == SQLITE_ROW && i++ < API_MAX_CLIENTS)
while(sqlite3_step(stmt) == SQLITE_ROW && i < max_sessions)
{
// Allocate memory for new session
struct session *sess = &sessions[i];
@ -292,10 +292,12 @@ bool restore_db_sessions(struct session *sessions)
// Mark session as used
sess->used = true;
i++;
}
log_info("Restored %u API session%s from the database",
i, i == 1 ? "" : "s");
log_info("Restored %u/%u API session%s from the database",
i, max_sessions, max_sessions == 1 ? "" : "s");
// Finalize statement
if(sqlite3_finalize(stmt) != SQLITE_OK)

View File

@ -16,7 +16,7 @@
bool create_session_table(sqlite3 *db);
bool add_session_app_column(sqlite3 *db);
bool backup_db_sessions(struct session *sessions);
bool restore_db_sessions(struct session *sessions);
bool backup_db_sessions(struct session *sessions, const uint16_t max_sessions);
bool restore_db_sessions(struct session *sessions, const uint16_t max_sessions);
#endif // SESSION_TABLE_PRIVATE_H

View File

@ -81,10 +81,7 @@ int main_dnsmasq (int argc, char **argv)
#endif
#if defined(HAVE_IDN) || defined(HAVE_LIBIDN2) || defined(LOCALEDIR)
setlocale(LC_ALL, "");
/*** Pi-hole modification ***/
setlocale(LC_NUMERIC, "C");
/****************************/
/*** Pi-hole modification: Locale is already initialized in main.c ***/
#endif
#ifdef LOCALEDIR
bindtextdomain("dnsmasq", LOCALEDIR);

View File

@ -270,6 +270,7 @@ enum message_type {
DISK_MESSAGE,
INACCESSIBLE_ADLIST_MESSAGE,
DISK_MESSAGE_EXTENDED,
CERTIFICATE_DOMAIN_MISMATCH_MESSAGE,
MAX_MESSAGE,
} __attribute__ ((packed));
@ -311,4 +312,13 @@ enum adlist_type {
ADLIST_ALLOW
} __attribute__ ((packed));
enum cert_check {
CERT_FILE_NOT_FOUND,
CERT_CANNOT_PARSE_CERT,
CERT_CANNOT_PARSE_KEY,
CERT_DOMAIN_MISMATCH,
CERT_DOMAIN_MATCH,
CERT_OKAY
} __attribute__ ((packed));
#endif // ENUMS_H

View File

@ -623,3 +623,66 @@ char * __attribute__((malloc)) get_hwmon_target(const char *path)
return target;
}
// Returns true if the files have different contents
// from specifies from which line number the files should be compared
bool files_different(const char *pathA, const char* pathB, unsigned int from)
{
// Check if both files exist
if(!file_exists(pathA) || !file_exists(pathB))
return true;
// Check if both files are identical
if(strcmp(pathA, pathB) == 0)
return false;
// Open both files
FILE *fpA = fopen(pathA, "r");
if(fpA == NULL)
{
log_warn("files_different(): Failed to open \"%s\" for reading: %s", pathA, strerror(errno));
return true;
}
FILE *fpB = fopen(pathB, "r");
if(fpB == NULL)
{
log_warn("files_different(): Failed to open \"%s\" for reading: %s", pathB, strerror(errno));
fclose(fpA);
return true;
}
// Compare both files line by line
char *lineA = NULL;
size_t lenA = 0;
ssize_t readA;
char *lineB = NULL;
size_t lenB = 0;
ssize_t readB;
bool different = false;
while((readA = getline(&lineA, &lenA, fpA)) != -1 &&
(readB = getline(&lineB, &lenB, fpB)) != -1)
{
// Skip lines until we reach the requested line number
if(from > 0)
{
from--;
continue;
}
// Compare lines
if(strcmp(lineA, lineB) != 0)
{
different = true;
break;
}
}
// Free memory
free(lineA);
free(lineB);
// Close files
fclose(fpA);
fclose(fpB);
return different;
}

View File

@ -31,6 +31,7 @@ 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);
void rotate_files(const char *path, char **first_file);
bool files_different(const char *pathA, const char* pathB, unsigned int from);
int parse_line(char *line, char **key, char **value);

View File

@ -45,6 +45,9 @@ jmp_buf exit_jmp;
int main (int argc, char *argv[])
{
// Initialize locale (needed for libidn)
init_locale();
// Get user pihole-FTL is running as
// We store this in a global variable
// such that the log routine can access

View File

@ -339,7 +339,7 @@ void importsetupVarsConf(void)
get_conf_upstream_servers_from_setupVars(&config.dns.upstreams);
// Try to get Pi-hole domain
get_conf_string_from_setupVars("PIHOLE_DOMAIN", &config.dhcp.domain);
get_conf_string_from_setupVars("PIHOLE_DOMAIN", &config.dns.domain);
// Try to get bool properties (the first two are intentionally set from the same key)
get_conf_bool_from_setupVars("DNS_FQDN_REQUIRED", &config.dns.domainNeeded);

View File

@ -13,17 +13,26 @@
#include "../log.h"
#undef free
void FTLfree(void *ptr, const char *file, const char *func, const int line)
void FTLfree(void **ptr, const char *file, const char *func, const int line)
{
// The free() function frees the memory space pointed to by ptr, which
// must have been returned by a previous call to malloc(), calloc(), or
// realloc(). Otherwise, or if free(ptr) has already been called before,
// undefined behavior occurs. If ptr is NULL, no operation is performed.
if(ptr == NULL)
{
log_warn("Trying to free NULL memory location in %s() (%s:%i)", func, file, line);
return;
}
if(*ptr == NULL)
{
log_warn("Trying to free NULL pointer in %s() (%s:%i)", func, file, line);
return;
}
free(ptr);
// Actually free the memory
free(*ptr);
// Set the pointer to NULL
*ptr = NULL;
}

View File

@ -14,7 +14,7 @@
char *FTLstrdup(const char *src, const char *file, const char *func, const int line) __attribute__((malloc));
void *FTLcalloc(size_t n, size_t size, const char *file, const char *func, const int line) __attribute__((malloc)) __attribute__((alloc_size(1,2)));
void *FTLrealloc(void *ptr_in, size_t size, const char *file, const char *func, const int line) __attribute__((alloc_size(2)));
void FTLfree(void *ptr, const char*file, const char *func, const int line);
void FTLfree(void **ptr, const char*file, const char *func, const int line);
int FTLfallocate(const int fd, const off_t offset, const off_t len, const char *file, const char *func, const int line);

View File

@ -40,7 +40,7 @@ static const char *false_positives[] = {
#define MAX_INVALID_DOMAINS 5
// Validate domain name
static inline bool __attribute__((pure)) valid_domain(const char *domain, const size_t len)
static inline bool __attribute__((pure)) valid_domain(const char *domain, const size_t len, const bool abp)
{
// Domain must not be NULL or empty, and they should not be longer than
// 255 characters
@ -84,8 +84,10 @@ static inline bool __attribute__((pure)) valid_domain(const char *domain, const
// TLD checks
// There must be at least two labels (i.e. one dot)
// e.g., "example.com" but not "localhost"
if(last_dot == -1)
// e.g., "example.com" but not "localhost" for exact domain
// We do not enforce this for ABP domains
// (see https://github.com/pi-hole/pi-hole/pull/5240)
if(last_dot == -1 && !abp)
return false;
// TLD must not start or end with a hyphen
@ -121,7 +123,7 @@ static inline bool __attribute__((pure)) valid_abp_domain(const char *line, cons
return false;
// Domain must be valid
return valid_domain(line+4, len-5);
return valid_domain(line+4, len-5, true);
}
else
{
@ -138,7 +140,7 @@ static inline bool __attribute__((pure)) valid_abp_domain(const char *line, cons
return false;
// Domain must be valid
return valid_domain(line+2, len-3);
return valid_domain(line+2, len-3, true);
}
}
@ -279,7 +281,7 @@ int gravity_parseList(const char *infile, const char *outfile, const char *adlis
// Validate line
if(line[0] != (antigravity ? '@' : '|') && // <- Not an ABP-style match
valid_domain(line, read))
valid_domain(line, read, false))
{
// Exact match found
if(checkOnly)

View File

@ -85,7 +85,7 @@ int send_json_error_free(struct ftl_conn *api, const int code,
const char *key, const char* message,
char *hint, bool free_hint)
{
if(hint)
if(hint != NULL)
log_warn("API: %s (%s)", message, hint);
else
log_warn("API: %s", message);
@ -94,7 +94,7 @@ int send_json_error_free(struct ftl_conn *api, const int code,
JSON_REF_STR_IN_OBJECT(error, "key", key);
JSON_REF_STR_IN_OBJECT(error, "message", message);
JSON_COPY_STR_TO_OBJECT(error, "hint", hint);
if(free_hint)
if(free_hint && hint != NULL)
free(hint);
cJSON *json = JSON_NEW_OBJECT();
@ -524,6 +524,10 @@ void read_and_parse_payload(struct ftl_conn *api)
// See https://www.w3.org/International/questions/qa-escapes#use
char *__attribute__((malloc)) escape_html(const char *string)
{
// If the string is NULL, return NULL
if(string == NULL)
return NULL;
// Allocate memory for escaped string
char *escaped = calloc(strlen(string) * 6 + 1, sizeof(char));
if(!escaped)

View File

@ -8,24 +8,26 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "../FTL.h"
#include "webserver.h"
#include "FTL.h"
#include "webserver/webserver.h"
// api_handler()
#include "../api/api.h"
#include "api/api.h"
// send_http()
#include "http-common.h"
// struct config
#include "../config/config.h"
#include "config/config.h"
// log_web()
#include "../log.h"
#include "log.h"
// get_nprocs()
#include <sys/sysinfo.h>
// file_readable()
#include "../files.h"
#include "files.h"
// generate_certificate()
#include "x509.h"
#include "webserver/x509.h"
// allocate_lua(), free_lua(), init_lua(), request_handler()
#include "lua_web.h"
#include "webserver/lua_web.h"
// log_certificate_domain_mismatch()
#include "database/message-table.h"
// Server context handle
static struct mg_context *ctx = NULL;
@ -341,6 +343,10 @@ void http_init(void)
if(file_readable(config.webserver.tls.cert.v.s))
{
if(read_certificate(config.webserver.tls.cert.v.s, config.webserver.domain.v.s, false) != CERT_DOMAIN_MATCH)
{
log_certificate_domain_mismatch(config.webserver.tls.cert.v.s, config.webserver.domain.v.s);
}
options[++next_option] = "ssl_certificate";
options[++next_option] = config.webserver.tls.cert.v.s;

View File

@ -145,6 +145,17 @@ bool generate_certificate(const char* certfile, bool rsa, const char *domain)
serial[i] = '0' + (serial[i] % 10);
serial[sizeof(serial) - 1] = '\0';
// Create validity period
// Use YYYYMMDDHHMMSS as required by RFC 5280
const time_t now = time(NULL);
struct tm tms = { 0 };
struct tm *tm = localtime_r(&now, &tms);
char not_before[16] = { 0 };
char not_after[16] = { 0 };
strftime(not_before, sizeof(not_before), "%Y%m%d%H%M%S", tm);
tm->tm_year += 30; // 30 years from now
strftime(not_after, sizeof(not_after), "%Y%m%d%H%M%S", tm);
// Generate certificate
printf("Generating new certificate with serial number %s...\n", serial);
mbedtls_x509write_crt_set_version(&crt, MBEDTLS_X509_CRT_VERSION_3);
@ -154,7 +165,7 @@ bool generate_certificate(const char* certfile, bool rsa, const char *domain)
mbedtls_x509write_crt_set_subject_key(&crt, &key);
mbedtls_x509write_crt_set_issuer_key(&crt, &key);
mbedtls_x509write_crt_set_issuer_name(&crt, "CN=pi.hole");
mbedtls_x509write_crt_set_validity(&crt, "20010101000000", "20301231235959");
mbedtls_x509write_crt_set_validity(&crt, not_before, not_after);
mbedtls_x509write_crt_set_basic_constraints(&crt, 0, -1);
mbedtls_x509write_crt_set_subject_key_identifier(&crt);
mbedtls_x509write_crt_set_authority_key_identifier(&crt);
@ -282,3 +293,239 @@ bool generate_certificate(const char* certfile, bool rsa, const char *domain)
return true;
}
// This function reads a X.509 certificate from a file and prints a
// human-readable representation of the certificate to stdout. If a domain is
// specified, we only check if this domain is present in the certificate.
// Otherwise, we print verbose human-readable information about the certificate
// and about the private key (if requested).
enum cert_check read_certificate(const char* certfile, const char *domain, const bool private_key)
{
if(certfile == NULL && domain == NULL)
{
log_err("No certificate file specified\n");
return CERT_FILE_NOT_FOUND;
}
mbedtls_x509_crt crt;
mbedtls_pk_context key;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_x509_crt_init(&crt);
mbedtls_pk_init(&key);
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
printf("Reading certificate from %s ...\n\n", certfile);
// Check if the file exists and is readable
if(access(certfile, R_OK) != 0)
{
log_err("Could not read certificate file: %s\n", strerror(errno));
return CERT_FILE_NOT_FOUND;
}
int rc = mbedtls_pk_parse_keyfile(&key, certfile, NULL, mbedtls_ctr_drbg_random, &ctr_drbg);
if (rc != 0)
{
log_err("Cannot parse key: Error code %d\n", rc);
return CERT_CANNOT_PARSE_KEY;
}
rc = mbedtls_x509_crt_parse_file(&crt, certfile);
if (rc != 0)
{
log_err("Cannot parse certificate: Error code %d\n", rc);
return CERT_CANNOT_PARSE_CERT;
}
// Parse mbedtls_x509_parse_subject_alt_names()
mbedtls_x509_sequence *sans = &crt.subject_alt_names;
bool found = false;
if(domain != NULL)
{
// Loop over all SANs
while(sans != NULL)
{
// Parse the SAN
mbedtls_x509_subject_alternative_name san = { 0 };
const int ret = mbedtls_x509_parse_subject_alt_name(&sans->buf, &san);
// Check if SAN is used (otherwise ret < 0, e.g.,
// MBEDTLS_ERR_X509_FEATURE_UNAVAILABLE) and if it is a
// DNS name, skip otherwise
if(ret < 0 || san.type != MBEDTLS_X509_SAN_DNS_NAME)
goto next_san;
// Check if the SAN matches the domain
if(strncasecmp(domain, (char*)san.san.unstructured_name.p, san.san.unstructured_name.len) == 0)
{
found = true;
break;
}
next_san:
// Go to next SAN
sans = sans->next;
}
// Also check against the common name (CN) field
char subject[MBEDTLS_X509_MAX_DN_NAME_SIZE];
if(mbedtls_x509_dn_gets(subject, sizeof(subject), &crt.subject) > 0)
{
// Check subject == "CN=<domain>"
if(strlen(subject) > 3 && strncasecmp(subject, "CN=", 3) == 0 && strcasecmp(domain, subject + 3) == 0)
found = true;
// Check subject == "<domain>"
else if(strcasecmp(domain, subject) == 0)
found = true;
}
// Free resources
mbedtls_x509_crt_free(&crt);
mbedtls_pk_free(&key);
mbedtls_entropy_free(&entropy);
mbedtls_ctr_drbg_free(&ctr_drbg);
return found ? CERT_DOMAIN_MATCH : CERT_DOMAIN_MISMATCH;
}
// else: Print verbose information about the certificate
char certinfo[BUFFER_SIZE] = { 0 };
mbedtls_x509_crt_info(certinfo, BUFFER_SIZE, " ", &crt);
puts("Certificate (X.509):\n");
puts(certinfo);
if(!private_key)
goto end;
puts("Private key:");
const char *keytype = mbedtls_pk_get_name(&key);
printf(" Type: %s\n", keytype);
mbedtls_pk_type_t pk_type = mbedtls_pk_get_type(&key);
if(pk_type == MBEDTLS_PK_RSA)
{
mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key);
printf(" RSA modulus: %zu bit\n", 8*mbedtls_rsa_get_len(rsa));
mbedtls_mpi E, N, P, Q, D;
mbedtls_mpi_init(&E); // E = public exponent (public)
mbedtls_mpi_init(&N); // N = P * Q (public)
mbedtls_mpi_init(&P); // P = prime factor 1 (private)
mbedtls_mpi_init(&Q); // Q = prime factor 2 (private)
mbedtls_mpi_init(&D); // D = private exponent (private)
mbedtls_mpi DP, DQ, QP;
mbedtls_mpi_init(&DP);
mbedtls_mpi_init(&DQ);
mbedtls_mpi_init(&QP);
if(mbedtls_rsa_export(rsa, &N, &P, &Q, &D, &E) != 0 ||
mbedtls_rsa_export_crt(rsa, &DP, &DQ, &QP) != 0)
{
puts(" could not export RSA parameters\n");
return EXIT_FAILURE;
}
puts(" Core parameters:");
if(mbedtls_mpi_write_file(" Exponent:\n E = 0x", &E, 16, NULL) != 0)
{
puts(" could not write MPI\n");
return EXIT_FAILURE;
}
if(mbedtls_mpi_write_file(" Modulus:\n N = 0x", &N, 16, NULL) != 0)
{
puts(" could not write MPI\n");
return EXIT_FAILURE;
}
if(mbedtls_mpi_cmp_mpi(&P, &Q) >= 0)
{
if(mbedtls_mpi_write_file(" Prime factors:\n P = 0x", &P, 16, NULL) != 0 ||
mbedtls_mpi_write_file(" Q = 0x", &Q, 16, NULL) != 0)
{
puts(" could not write MPIs\n");
return EXIT_FAILURE;
}
}
else
{
if(mbedtls_mpi_write_file(" Prime factors:\n Q = 0x", &Q, 16, NULL) != 0 ||
mbedtls_mpi_write_file("\n P = 0x", &P, 16, NULL) != 0)
{
puts(" could not write MPIs\n");
return EXIT_FAILURE;
}
}
if(mbedtls_mpi_write_file(" Private exponent:\n D = 0x", &D, 16, NULL) != 0)
{
puts(" could not write MPI\n");
return EXIT_FAILURE;
}
mbedtls_mpi_free(&N);
mbedtls_mpi_free(&P);
mbedtls_mpi_free(&Q);
mbedtls_mpi_free(&D);
mbedtls_mpi_free(&E);
puts(" CRT parameters:");
if(mbedtls_mpi_write_file(" D mod (P-1):\n DP = 0x", &DP, 16, NULL) != 0 ||
mbedtls_mpi_write_file(" D mod (Q-1):\n DQ = 0x", &DQ, 16, NULL) != 0 ||
mbedtls_mpi_write_file(" Q^-1 mod P:\n QP = 0x", &QP, 16, NULL) != 0)
{
puts(" could not write MPIs\n");
return EXIT_FAILURE;
}
mbedtls_mpi_free(&DP);
mbedtls_mpi_free(&DQ);
mbedtls_mpi_free(&QP);
}
else if(pk_type == MBEDTLS_PK_ECKEY)
{
mbedtls_ecp_keypair *ec = mbedtls_pk_ec(key);
mbedtls_ecp_curve_type ec_type = mbedtls_ecp_get_type(&ec->private_grp);
switch (ec_type)
{
case MBEDTLS_ECP_TYPE_NONE:
puts(" Curve type: Unknown");
break;
case MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:
puts(" Curve type: Short Weierstrass (y^2 = x^3 + a x + b)");
break;
case MBEDTLS_ECP_TYPE_MONTGOMERY:
puts(" Curve type: Montgomery (y^2 = x^3 + a x^2 + x)");
break;
}
const size_t bitlen = mbedtls_mpi_bitlen(&ec->private_d);
printf(" Bitlen: %zu bit\n", bitlen);
mbedtls_mpi_write_file(" Private key:\n D = 0x", &ec->private_d, 16, NULL);
mbedtls_mpi_write_file(" Public key:\n X = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), 16, NULL);
mbedtls_mpi_write_file(" Y = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), 16, NULL);
mbedtls_mpi_write_file(" Z = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Z), 16, NULL);
}
else
{
puts("Sorry, but FTL does not know how to print key information for this type\n");
goto end;
}
// Print private key in PEM format
mbedtls_pk_write_key_pem(&key, (unsigned char*)certinfo, BUFFER_SIZE);
puts("Private key (PEM):");
puts(certinfo);
end:
// Print public key in PEM format
mbedtls_pk_write_pubkey_pem(&key, (unsigned char*)certinfo, BUFFER_SIZE);
puts("Public key (PEM):");
puts(certinfo);
// Free resources
mbedtls_x509_crt_free(&crt);
mbedtls_pk_free(&key);
mbedtls_entropy_free(&entropy);
mbedtls_ctr_drbg_free(&ctr_drbg);
return CERT_OKAY;
}

View File

@ -13,6 +13,9 @@
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
#include "enums.h"
bool generate_certificate(const char* certfile, bool rsa, const char *domain);
enum cert_check read_certificate(const char* certfile, const char *domain, const bool private_key);
#endif // X509_H

View File

@ -10,7 +10,7 @@
# Please see LICENSE file for your rights under this license.
import io
import pprint
import ipaddress
import random
import zipfile
from libs.openAPI import openApi
@ -152,8 +152,8 @@ class ResponseVerifyer():
# Check for properties in FTL that are not in the API specs
for property in FTLflat.keys():
if property not in YAMLflat.keys() and len([p.startswith(property + ".") for p in YAMLflat.keys()]) == 0:
self.errors.append("Property '" + property + "' missing in the API specs (have " + ",".join(YAMLflat.keys()) + ")")
if property not in YAMLflat.keys():
self.errors.append("Property '" + property + "' missing in the API specs")
elif expected_mimetype == "application/zip":
file_like_object = io.BytesIO(FTLresponse)
@ -172,7 +172,7 @@ class ResponseVerifyer():
if expected_file not in zipfile_obj.namelist():
self.errors.append("File " + expected_file + " is missing in received archive.")
pihole_toml = zipfile_obj.read("etc/pihole/pihole.toml")
if not pihole_toml.startswith(b"# This file is managed by pihole-FTL"):
if not pihole_toml.startswith(b"# Pi-hole configuration file (v"):
self.errors.append("Received ZIP file's pihole.toml starts with wrong header")
except Exception as err:
self.errors.append("Error during ZIP analysis: " + str(err))
@ -221,8 +221,35 @@ class ResponseVerifyer():
return self.errors
# Check if a string is a valid IPv4 address
def valid_ipv4(self, addr: str) -> bool:
# Empty string is valid (0.0.0.0)
if len(addr) == 0:
return True
try:
if type(ipaddress.ip_address(addr)) is ipaddress.IPv4Address:
return True
except ValueError:
pass
return False
# Check if a string is a valid IPv6 address
def valid_ipv6(self, addr: str) -> bool:
# Empty string is valid (::)
if len(addr) == 0:
return True
try:
if type(ipaddress.ip_address(addr)) is ipaddress.IPv6Address:
return True
except ValueError:
pass
return False
# Verify a single property's type
def verify_type(self, prop_type: any, yaml_type: str, yaml_nullable: bool):
def verify_type(self, prop: any, yaml_type: str, yaml_nullable: bool, yaml_format: str = None):
# Get the type of the property
prop_type = type(prop)
# None is an acceptable reply when this is specified in the API specs
if prop_type is type(None) and yaml_nullable:
return True
@ -230,6 +257,14 @@ class ResponseVerifyer():
if yaml_type not in self.YAML_TYPES:
self.errors.append("Property type \"" + yaml_type + "\" is not valid in OpenAPI specs")
return False
if yaml_format is not None:
# Check if the format is correct
if yaml_format == "ipv4" and not self.valid_ipv4(prop):
self.errors.append("Property \"" + str(prop) + "\" is not a valid IPv4 address")
return False
elif yaml_format == "ipv6" and not self.valid_ipv6(prop):
self.errors.append("Property \"" + str(prop) + "\" is not a valid IPv6 address")
return False
return prop_type in self.YAML_TYPES[yaml_type]
@ -296,6 +331,9 @@ class ResponseVerifyer():
for j in FTLprop[i]:
if not self.verify_property(YAMLprop['items']['properties'], YAMLexamples, FTLprop[i], props + [i, str(j)]):
all_okay = False
# Add this property to the YAML response
self.YAMLresponse[flat_path] = []
else:
# Check this property
@ -306,17 +344,19 @@ class ResponseVerifyer():
# if not defined as string, integer, etc.)
yaml_nullable = 'nullable' in YAMLprop and YAMLprop['nullable'] == True
# Get format of this property (if defined)
yaml_format = YAMLprop['format'] if 'format' in YAMLprop else YAMLprop['x-format'] if 'x-format' in YAMLprop else None
# Add this property to the YAML response
self.YAMLresponse[flat_path] = []
# Check type of YAML example (if defined)
if 'example' in YAMLprop:
example_type = type(YAMLprop['example'])
# Check if the type of the example matches the
# type we defined in the API specs
self.YAMLresponse[flat_path].append(YAMLprop['example'])
if not self.verify_type(example_type, yaml_type, yaml_nullable):
self.errors.append(f"API example ({str(example_type)}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
if not self.verify_type(YAMLprop['example'], yaml_type, yaml_nullable, yaml_format):
self.errors.append(f"API example ({str(type(YAMLprop['example']))}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
return False
# Check type of externally defined YAML examples (next to schema)
@ -340,16 +380,14 @@ class ResponseVerifyer():
if skip_this:
continue
# Check if the type of the example matches the type we defined in the API specs
example_type = type(example)
self.YAMLresponse[flat_path].append(example)
if not self.verify_type(example_type, yaml_type, yaml_nullable):
self.errors.append(f"API example ({str(example_type)}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
if not self.verify_type(example, yaml_type, yaml_nullable, yaml_format):
self.errors.append(f"API example ({str(type(example))}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
return False
# Compare type of FTL's reply against what we defined in the API specs
ftl_type = type(FTLprop)
if not self.verify_type(ftl_type, yaml_type, yaml_nullable):
self.errors.append(f"FTL's reply ({str(ftl_type)}) does not match defined type ({yaml_type}) in {flat_path}")
if not self.verify_type(FTLprop, yaml_type, yaml_nullable, yaml_format):
self.errors.append(f"FTL's reply ({str(type(FTLprop))}) does not match defined type ({yaml_type}) in {flat_path}")
return False
return all_okay

View File

@ -100,7 +100,10 @@
#
# Possible values are:
# Array of custom DNS records each one in HOSTS form: "IP HOSTNAME"
hosts = []
hosts = [
"1.1.1.1 abc-custom.com def-custom.de",
"2.2.2.2 äste.com steä.com"
] ### CHANGED, default = []
# If set, A and AAAA queries for plain names, without dots or domain parts, are never
# forwarded to upstream nameservers
@ -110,6 +113,27 @@
# same way as for DHCP-derived names
expandHosts = false
# The DNS domain used by your Pi-hole to expand hosts and for DHCP.
#
# Only if DHCP is enabled below: For DHCP, this has two effects; firstly it causes the
# DHCP server to return the domain to any hosts which request it, and secondly it sets
# the domain which it is legal for DHCP-configured hosts to claim. The intention is to
# constrain hostnames so that an untrusted host on the LAN cannot advertise its name
# via DHCP as e.g. "google.com" and capture traffic not meant for it. If no domain
# suffix is specified, then any DHCP hostname with a domain part (ie with a period)
# will be disallowed and logged. If a domain is specified, then hostnames with a
# domain part are allowed, provided the domain part matches the suffix. In addition,
# when a suffix is set then hostnames without a domain part have the suffix added as
# an optional domain part. For instance, we can set domain=mylab.com and have a
# machine whose DHCP hostname is "laptop". The IP address for that machine is
# available both as "laptop" and "laptop.mylab.com".
#
# You can disable setting a domain by setting this option to an empty string.
#
# Possible values are:
# <any valid domain>
domain = "lan"
# Should all reverse lookups for private IP ranges (i.e., 192.168.x.y, etc) which are
# not found in /etc/hosts or the DHCP leases file be answered with "no such domain"
# rather than being forwarded upstream?
@ -175,7 +199,9 @@
# Possible values are:
# Array of static leases each on in one of the following forms:
# "<cname>,<target>[,<TTL>]"
cnameRecords = []
cnameRecords = [
"brücke.com,äste.com,2",
]
# Port used by the DNS server
port = 53
@ -370,12 +396,26 @@
# <ip-addr>, e.g., "192.168.0.1"
router = ""
# The DNS domain used by your Pi-hole
# The DNS domain used by your Pi-hole (*** DEPRECATED ***)
# This setting is deprecated and will be removed in a future version. Please use
# dns.domain instead. Setting it to any non-default value will overwrite the value of
# dns.domain if it is still set to its default value.
#
# Possible values are:
# <any valid domain>
domain = "lan"
# The netmask used by your Pi-hole. For directly connected networks (i.e., networks on
# which the machine running Pi-hole has an interface) the netmask is optional and may
# be set to "0.0.0.0": it will then be determined from the interface configuration
# itself. For networks which receive DHCP service via a relay agent, we cannot
# determine the netmask itself, so it should explicitly be specified, otherwise
# Pi-hole guesses based on the class (A, B or C) of the network address.
#
# Possible values are:
# <any valid netmask>, e.g., "255.255.255.0" or "0.0.0.0" for auto-discovery
netmask = "0.0.0.0"
# If the lease time is given, then leases will be given for that length of time. If not
# given, the default lease time is one hour for IPv4 and one day for IPv6.
#
@ -520,7 +560,7 @@
#
# Possible values are:
# comma-separated list of <[ip_address:]port>
port = "80,[::]:80,443s"
port = "80,[::]:80,443s,[::]:443s"
[webserver.session]
# Session timeout in seconds. If a session is inactive for more than this time, it will
@ -604,6 +644,12 @@
# sense of the option means only 127.0.0.1 and [::1]
searchAPIauth = false
# Number of concurrent sessions allowed for the API. If the number of sessions exceeds
# this value, no new sessions will be allowed until the number of sessions drops due
# to session expiration or logout. Note that the number of concurrent sessions is
# irrelevant if authentication is disabled as no sessions are used in this case.
max_sessions = 16
# Should FTL prettify the API output (add extra spaces, newlines and indentation)?
prettyJSON = false
@ -775,6 +821,9 @@
# malfunctioning addr2line can prevent from generating any backtrace at all.
addr2line = true
# Should FTL load additional dnsmasq configuration files from /etc/dnsmasq.d/?
etc_dnsmasq_d = true ### CHANGED, default = false
# Additional lines to inject into the generated dnsmasq configuration. Warning: This is
# an advanced setting and should only be used with care. Incorrectly formatted or
# duplicated lines as well as lines conflicting with the automatic configuration of

View File

@ -20,7 +20,7 @@ while pidof -s pihole-FTL > /dev/null; do
done
# Clean up possible old files from earlier test runs
rm -f /etc/pihole/gravity.db /etc/pihole/pihole-FTL.db /var/log/pihole/pihole.log /var/log/pihole/FTL.log /dev/shm/FTL-*
rm -rf /etc/pihole /var/log/pihole /dev/shm/FTL-*
# Create necessary directories and files
mkdir -p /home/pihole /etc/pihole /run/pihole /var/log/pihole

View File

@ -493,21 +493,17 @@
[[ ${lines[0]} == "The Pi-hole FTL engine - "* ]]
}
#@test "No WARNING messages in FTL.log (besides known capability issues)" {
# run bash -c 'grep "WARNING" /var/log/pihole/FTL.log'
# printf "%s\n" "${lines[@]}"
# run bash -c 'grep "WARNING" /var/log/pihole/FTL.log | grep -c -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN"'
# printf "%s\n" "${lines[@]}"
# [[ ${lines[0]} == "0" ]]
#}
@test "No WARNING messages in FTL.log (besides known capability issues)" {
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|(Cannot set process priority)"'
printf "%s\n" "${lines[@]}"
[[ "${lines[@]}" == "" ]]
}
#@test "No FATAL messages in FTL.log (besides error due to starting FTL more than once)" {
# run bash -c 'grep "FATAL" /var/log/pihole/FTL.log'
# printf "%s\n" "${lines[@]}"
# run bash -c 'grep "FATAL:" /var/log/pihole/FTL.log | grep -c -v "FATAL: create_shm(): Failed to create shared memory object \"FTL-lock\": File exists"'
# printf "%s\n" "${lines[@]}"
# [[ ${lines[0]} == "0" ]]
#}
@test "No CRIT messages in FTL.log (besides error due to starting FTL more than once)" {
run bash -c 'grep "CRIT:" /var/log/pihole/FTL.log | grep -v "CRIT: Initialization of shared memory failed"'
printf "%s\n" "${lines[@]}"
[[ "${lines[@]}" == "" ]]
}
@test "No \"database not available\" messages in FTL.log" {
run bash -c 'grep -c "database not available" /var/log/pihole/FTL.log'
@ -1253,6 +1249,35 @@
[[ "${lines[0]}" == "192.168.1.7" ]]
}
@test "Custom DNS records: Multiple domains per line are accepted" {
run bash -c "dig A abc-custom.com +short @127.0.0.1"
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "1.1.1.1" ]]
run bash -c "dig A def-custom.de +short @127.0.0.1"
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "1.1.1.1" ]]
}
@test "Custom DNS records: International domains are converted to IDNA form" {
# äste.com ---> xn--ste-pla.com
run bash -c "dig A xn--ste-pla.com +short @127.0.0.1"
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "2.2.2.2" ]]
# steä.com -> xn--ste-sla.com
run bash -c "dig A xn--ste-sla.com +short @127.0.0.1"
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "2.2.2.2" ]]
}
@test "Local CNAME records: International domains are converted to IDNA form" {
# brücke.com ---> xn--brcke-lva.com
run bash -c "dig A xn--brcke-lva.com +short @127.0.0.1"
printf "%s\n" "${lines[@]}"
# xn--ste-pla.com ---> äste.com
[[ "${lines[0]}" == "xn--ste-pla.com." ]]
[[ "${lines[1]}" == "2.2.2.2" ]]
}
@test "Environmental variable is favored over config file" {
# The config file has -10 but we set FTLCONF_misc_nice="-11"
run bash -c 'grep -B1 "nice = -11" /etc/pihole/pihole.toml'
@ -1292,6 +1317,18 @@
[[ ${lines[0]} == '"xn--bc-uia.com"' ]]
}
@test "API history: Returns full 24 hours even if only a few queries are made" {
run bash -c 'curl -s 127.0.0.1/api/history | jq ".history | length"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "145" ]]
}
@test "API history/clients: Returns full 24 hours even if only a few queries are made" {
run bash -c 'curl -s 127.0.0.1/api/history/clients | jq ".history | length"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "145" ]]
}
@test "API authorization (without password): No login required" {
run bash -c 'curl -s 127.0.0.1/api/auth'
printf "%s\n" "${lines[@]}"
@ -1356,6 +1393,89 @@
run bash -c 'curl -I --cacert /etc/pihole/test.crt --resolve pi.hole:443:127.0.0.1 https://pi.hole/'
}
@test "X.509 certificate parser returns expected result" {
# We are getting the certificate from the config
run bash -c './pihole-FTL --read-x509'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate (X.509):" ]]
[[ "${lines[2]}" == " cert. version : 3" ]]
[[ "${lines[3]}" == " serial number : 30:36:35:35:38:30:34:30:38:32:39:39:39:31:36" ]]
[[ "${lines[4]}" == " issuer name : CN=pi.hole" ]]
[[ "${lines[5]}" == " subject name : CN=pi.hole" ]]
[[ "${lines[6]}" == " issued on : 2001-01-01 00:00:00" ]]
[[ "${lines[7]}" == " expires on : 2030-12-31 23:59:59" ]]
[[ "${lines[8]}" == " signed using : ECDSA with SHA256" ]]
[[ "${lines[9]}" == " EC key size : 521 bits" ]]
[[ "${lines[10]}" == " basic constraints : CA=false" ]]
[[ "${lines[11]}" == "Public key (PEM):" ]]
[[ "${lines[12]}" == "-----BEGIN PUBLIC KEY-----" ]]
[[ "${lines[13]}" == "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBQ51HeOLjSap1Xr+pnFQJqvBZc92T" ]]
[[ "${lines[14]}" == "XyL4KwIZdpsHl95Pc0Xcn8Xzyox0cWhMyycQgcGbIw3nuefCZaXfc3CuU30BPDdb" ]]
[[ "${lines[15]}" == "91h+rDhV4+VkEkANPBbgKQ6kCiHNtMAdugyaeHxzFpqegGGvgQ2l4Vp98l4M7zBC" ]]
[[ "${lines[16]}" == "G6K/RbZDlDvNUCgwElE=" ]]
[[ "${lines[17]}" == "-----END PUBLIC KEY-----" ]]
[[ "${lines[18]}" == "" ]]
}
@test "X.509 certificate parser returns expected result (with private key)" {
# We are explicitly specifying the certificate file here
run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate (X.509):" ]]
[[ "${lines[2]}" == " cert. version : 3" ]]
[[ "${lines[3]}" == " serial number : 30:36:35:35:38:30:34:30:38:32:39:39:39:31:36" ]]
[[ "${lines[4]}" == " issuer name : CN=pi.hole" ]]
[[ "${lines[5]}" == " subject name : CN=pi.hole" ]]
[[ "${lines[6]}" == " issued on : 2001-01-01 00:00:00" ]]
[[ "${lines[7]}" == " expires on : 2030-12-31 23:59:59" ]]
[[ "${lines[8]}" == " signed using : ECDSA with SHA256" ]]
[[ "${lines[9]}" == " EC key size : 521 bits" ]]
[[ "${lines[10]}" == " basic constraints : CA=false" ]]
[[ "${lines[11]}" == "Private key:" ]]
[[ "${lines[12]}" == " Type: EC" ]]
[[ "${lines[13]}" == " Curve type: Short Weierstrass (y^2 = x^3 + a x + b)" ]]
[[ "${lines[14]}" == " Bitlen: 518 bit" ]]
[[ "${lines[15]}" == " Private key:" ]]
[[ "${lines[16]}" == " D = 0x2CBE6CF8A913B445F211165B0473B7037B5B06187C8685AEF4A58354C7061C388173E0B00374A55CEAC7BB5886159C9D54B3C020564355A0FA71A55559304156D8"* ]]
[[ "${lines[17]}" == " Public key:" ]]
[[ "${lines[18]}" == " X = 0x01439D4778E2E349AA755EBFA99C5409AAF05973DD935F22F82B0219769B0797DE4F7345DC9FC5F3CA8C7471684CCB271081C19B230DE7B9E7C265A5DF7370AE537D"* ]]
[[ "${lines[19]}" == " Y = 0x013C375BF7587EAC3855E3E56412400D3C16E0290EA40A21CDB4C01DBA0C9A787C73169A9E8061AF810DA5E15A7DF25E0CEF30421BA2BF45B643943BCD5028301251"* ]]
[[ "${lines[20]}" == " Z = 0x01"* ]]
[[ "${lines[21]}" == "Private key (PEM):" ]]
[[ "${lines[22]}" == "-----BEGIN EC PRIVATE KEY-----" ]]
[[ "${lines[23]}" == "MIHcAgEBBEIALL5s+KkTtEXyERZbBHO3A3tbBhh8hoWu9KWDVMcGHDiBc+CwA3Sl" ]]
[[ "${lines[24]}" == "XOrHu1iGFZydVLPAIFZDVaD6caVVWTBBVtigBwYFK4EEACOhgYkDgYYABAFDnUd4" ]]
[[ "${lines[25]}" == "4uNJqnVev6mcVAmq8Flz3ZNfIvgrAhl2mweX3k9zRdyfxfPKjHRxaEzLJxCBwZsj" ]]
[[ "${lines[26]}" == "Dee558Jlpd9zcK5TfQE8N1v3WH6sOFXj5WQSQA08FuApDqQKIc20wB26DJp4fHMW" ]]
[[ "${lines[27]}" == "mp6AYa+BDaXhWn3yXgzvMEIbor9FtkOUO81QKDASUQ==" ]]
[[ "${lines[28]}" == "-----END EC PRIVATE KEY-----" ]]
[[ "${lines[29]}" == "Public key (PEM):" ]]
[[ "${lines[30]}" == "-----BEGIN PUBLIC KEY-----" ]]
[[ "${lines[31]}" == "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBQ51HeOLjSap1Xr+pnFQJqvBZc92T" ]]
[[ "${lines[32]}" == "XyL4KwIZdpsHl95Pc0Xcn8Xzyox0cWhMyycQgcGbIw3nuefCZaXfc3CuU30BPDdb" ]]
[[ "${lines[33]}" == "91h+rDhV4+VkEkANPBbgKQ6kCiHNtMAdugyaeHxzFpqegGGvgQ2l4Vp98l4M7zBC" ]]
[[ "${lines[34]}" == "G6K/RbZDlDvNUCgwElE=" ]]
[[ "${lines[35]}" == "-----END PUBLIC KEY-----" ]]
[[ "${lines[36]}" == "" ]]
}
@test "X.509 certificate parser can check if domain is included" {
run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem pi.hole'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate matches domain pi.hole" ]]
[[ "${lines[2]}" == "" ]]
[[ $status == 0 ]]
run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem pi-hole.net'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate does not match domain pi-hole.net" ]]
[[ "${lines[2]}" == "" ]]
[[ $status == 1 ]]
}
@test "Test embedded GZIP compressor" {
run bash -c './pihole-FTL gzip test/pihole-FTL.db.sql'
printf "Compression output:\n"
@ -1410,10 +1530,10 @@
[[ "${lines[0]}" == "PI.HOLE" ]]
run bash -c './pihole-FTL --config dns.hosts'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "[]" ]]
[[ "${lines[0]}" == "[ 1.1.1.1 abc-custom.com def-custom.de, 2.2.2.2 äste.com steä.com ]" ]]
run bash -c './pihole-FTL --config webserver.port'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "80,[::]:80,443s" ]]
[[ "${lines[0]}" == "80,[::]:80,443s,[::]:443s" ]]
}
@test "Create, verify and re-import Teleporter file via CLI" {
@ -1433,3 +1553,24 @@
[[ $status == 0 ]]
run bash -c "rm ${filename}"
}
@test "Expected number of config file rotations" {
run bash -c 'grep -c "INFO: Config file written to /etc/pihole/pihole.toml" /var/log/pihole/FTL.log'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "3" ]]
run bash -c 'grep -c "DEBUG_CONFIG: pihole.toml unchanged" /var/log/pihole/FTL.log'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "3" ]]
run bash -c 'grep -c "DEBUG_CONFIG: Config file written to /etc/pihole/dnsmasq.conf" /var/log/pihole/FTL.log'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "1" ]]
run bash -c 'grep -c "DEBUG_CONFIG: dnsmasq.conf unchanged" /var/log/pihole/FTL.log'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "2" ]]
run bash -c 'grep -c "DEBUG_CONFIG: HOSTS file written to /etc/pihole/hosts/custom.list" /var/log/pihole/FTL.log'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "1" ]]
run bash -c 'grep -c "DEBUG_CONFIG: custom.list unchanged" /var/log/pihole/FTL.log'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "3" ]]
}