diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4fb40e3d..008e6d7d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -293,6 +293,7 @@ add_executable(pihole-FTL $ $ $ + $ ) if(STATIC) set_target_properties(pihole-FTL PROPERTIES LINK_SEARCH_START_STATIC ON) @@ -335,6 +336,7 @@ add_subdirectory(tre-regex) add_subdirectory(syscalls) add_subdirectory(config) add_subdirectory(tools) +add_subdirectory(ntp) find_library(LIBREADLINE NAMES libreadline${LIBRARY_SUFFIX} readline) find_library(LIBHISTORY NAMES libhistory${LIBRARY_SUFFIX} history) @@ -373,5 +375,5 @@ find_program(SETCAP setcap) install(TARGETS pihole-FTL RUNTIME DESTINATION bin PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) -install(CODE "execute_process(COMMAND ${SETCAP} CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_NET_ADMIN,CAP_SYS_NICE,CAP_CHOWN+eip \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/pihole-FTL)") +install(CODE "execute_process(COMMAND ${SETCAP} CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_NET_ADMIN,CAP_SYS_NICE,CAP_CHOWN,CAP_SYS_TIME+eip \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/pihole-FTL)") diff --git a/src/api/config.c b/src/api/config.c index 16f66817..d223bc83 100644 --- a/src/api/config.c +++ b/src/api/config.c @@ -37,6 +37,7 @@ static struct { { { "dns", "DNS", "DNS server settings" }, { "dhcp", "DHCP", "DHCP server settings" }, + { "ntp", "NTP", "Network Time Sync settings" }, { "resolver", "Resolver", "Resolver settings" }, { "database", "Database", "Database settings" }, { "webserver", "HTTP/API", "Webserver and API settings" }, diff --git a/src/api/docs/content/specs/config.yaml b/src/api/docs/content/specs/config.yaml index 07633ef2..7bcd3029 100644 --- a/src/api/docs/content/specs/config.yaml +++ b/src/api/docs/content/specs/config.yaml @@ -326,6 +326,43 @@ components: type: array items: type: string + ntp: + type: object + properties: + ipv4: + type: object + properties: + active: + type: boolean + address: + type: string + x-format: ipv4 + ipv6: + type: object + properties: + active: + type: boolean + address: + type: string + x-format: ipv6 + sync: + type: object + properties: + server: + type: string + interval: + type: integer + count: + type: integer + rtc: + type: object + properties: + set: + type: boolean + device: + type: string + utc: + type: boolean resolver: type: object properties: @@ -549,6 +586,8 @@ components: type: boolean reserved: type: boolean + ntp: + type: boolean all: type: boolean topics: @@ -661,6 +700,21 @@ components: hosts: - "11:22:33:44:55:66,192.168.1.123" - "11:22:33:44:55:67,192.168.1.124,hostname" + ntp: + ipv4: + active: true + address: "" + ipv6: + active: true + address: "" + sync: + server: "pool.ntp.org" + interval: 3600 + count: 8 + rtc: + set: true + device: "" + utc: true resolver: resolveIPv4: true resolveIPv6: true @@ -761,6 +815,7 @@ components: webserver: false extra: false reserved: false + ntp: false all: false config_one: summary: One option diff --git a/src/args.c b/src/args.c index d643ebc1..a261d3be 100644 --- a/src/args.c +++ b/src/args.c @@ -66,6 +66,10 @@ #include "files.h" // resolveHostname() #include "resolve.h" +// ntp_client() +#include "ntp/ntp.h" +// check_capability() +#include "capabilities.h" // defined in dnsmasq.c extern void print_dnsmasq_version(const char *yellow, const char *green, const char *bold, const char *normal); @@ -305,6 +309,38 @@ void parse_args(int argc, char* argv[]) exit(write_teleporter_zip_to_disk() ? EXIT_SUCCESS : EXIT_FAILURE); } + // Create test NTP client + if((argc > 1 && argc < 5) && strcmp(argv[1], "ntp") == 0) + { + // Parse arguments + const bool update = (argc > 2 && strcmp(argv[2], "--update") == 0) || + (argc > 3 && strcmp(argv[3], "--update") == 0); + const char *server = "127.0.0.1"; + if(argc > 2 && strcmp(argv[2], "--update") != 0) + server = argv[2]; + + // Ensure we have the necessary capabilities + if(update && !check_capability(CAP_SYS_TIME)) + { + puts("Insufficient capabilities to run NTP client"); + const char *bold = cli_bold(); + const char *normal = cli_normal(); + printf("Try: %ssudo%s ", bold, normal); + for(int i = 0; i < argc; i++) + printf("%s ", argv[i]); + puts(""); + exit(EXIT_FAILURE); + } + + printf("Using NTP server: %s\n", server); + + // Enable stdout printing + cli_mode = true; + log_ctrl(false, true); + readFTLconf(&config, false); + exit(ntp_client(server, update, true) ? EXIT_SUCCESS : EXIT_FAILURE); + } + // Import teleporter archive through CLI if(argc == 3 && strcmp(argv[1], "--teleporter") == 0) { @@ -1017,6 +1053,14 @@ void parse_args(int argc, char* argv[]) printf(" Encoding: %spihole-FTL idn2 %sdomain%s\n", green, cyan, normal); printf(" Decoding: %spihole-FTL idn2 -d %spunycode%s\n\n", green, cyan, normal); + printf("%sNTP client:%s\n", yellow, normal); + printf(" Query an NTP server for the current time and print the\n"); + printf(" result in human-readable format. An optional %sserver%s may be\n", cyan, normal); + printf(" as argument. If the server is omitted, 127.0.0.1 is used.\n\n"); + printf(" The system time is updated on the system when the optional\n"); + printf(" %s--update%s flag is given.\n\n", purple, normal); + printf(" Usage: %spihole-FTL ntp %s[server]%s %s[--update]%s\n\n", green, cyan, normal, purple, normal); + printf("%sOther:%s\n", yellow, normal); printf("\t%sptr %sIP%s %s[tcp]%s Resolve IP address to hostname\n", green, cyan, normal, purple, normal); printf("\t Append %stcp%s to use TCP instead of UDP\n", purple, normal); diff --git a/src/capabilities.c b/src/capabilities.c index 79d0532e..57631edb 100644 --- a/src/capabilities.c +++ b/src/capabilities.c @@ -141,6 +141,13 @@ bool check_capabilities(void) log_warn("Required Linux capability CAP_CHOWN not available"); capabilities_okay = false; } + if (!(data->permitted & (1 << CAP_SYS_TIME)) || + !(data->effective & (1 << CAP_SYS_TIME))) + { + // Necessary for setting the system time in the NTP client + log_warn("Required Linux capability CAP_SYS_TIME not available"); + capabilities_okay = false; + } // Free allocated memory free(hdr); diff --git a/src/config/config.c b/src/config/config.c index 0632318b..6109e964 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -783,6 +783,76 @@ void initConfig(struct config *conf) conf->dhcp.hosts.c = validate_stub; // Type-based checking + dnsmasq syntax checking + // struct ntp + conf->ntp.ipv4.active.k = "ntp.ipv4.active"; + conf->ntp.ipv4.active.h = "Should FTL act as network time protocol (NTP) server (IPv4)?"; + conf->ntp.ipv4.active.t = CONF_BOOL; + conf->ntp.ipv4.active.f = FLAG_RESTART_FTL; + conf->ntp.ipv4.active.d.b = true; + conf->ntp.ipv4.active.c = validate_stub; // Only type-based checking + + conf->ntp.ipv4.address.k = "ntp.ipv4.address"; + conf->ntp.ipv4.address.h = "IPv4 address to listen on for NTP requests"; + conf->ntp.ipv4.address.a = cJSON_CreateStringReference(" or empty string (\"\") for wildcard (0.0.0.0)"); + conf->ntp.ipv4.address.t = CONF_STRUCT_IN_ADDR; + conf->ntp.ipv4.address.f = FLAG_RESTART_FTL; + memset(&conf->ntp.ipv4.address.d.in_addr, 0, sizeof(struct in_addr)); + conf->ntp.ipv4.address.c = validate_stub; // Only type-based checking + + conf->ntp.ipv6.active.k = "ntp.ipv6.active"; + conf->ntp.ipv6.active.h = "Should FTL act as network time protocol (NTP) server (IPv6)?"; + conf->ntp.ipv6.active.t = CONF_BOOL; + conf->ntp.ipv6.active.f = FLAG_RESTART_FTL; + conf->ntp.ipv6.active.d.b = true; + conf->ntp.ipv6.active.c = validate_stub; // Only type-based checking + + conf->ntp.ipv6.address.k = "ntp.ipv6.address"; + conf->ntp.ipv6.address.h = "IPv6 address to listen on for NTP requests"; + conf->ntp.ipv6.address.a = cJSON_CreateStringReference(" or empty string (\"\") for wildcard (::)"); + conf->ntp.ipv6.address.t = CONF_STRUCT_IN6_ADDR; + conf->ntp.ipv6.address.f = FLAG_RESTART_FTL; + memset(&conf->ntp.ipv6.address.d.in6_addr, 0, sizeof(struct in6_addr)); + conf->ntp.ipv6.address.c = validate_stub; // Only type-based checking + + conf->ntp.sync.server.k = "ntp.sync.server"; + conf->ntp.sync.server.h = "NTP upstream server to sync with, e.g., \"pool.ntp.org\". Note that the NTP server should be located as close as possible to you in order to minimize the time offset possibly introduced by different routing paths."; + conf->ntp.sync.server.a = cJSON_CreateStringReference("valid NTP upstream server"); + conf->ntp.sync.server.t = CONF_STRING; + conf->ntp.sync.server.d.s = (char*)"pool.ntp.org"; + conf->ntp.sync.server.c = validate_stub; // Only type-based checking + + conf->ntp.sync.interval.k = "ntp.sync.interval"; + conf->ntp.sync.interval.h = "Interval in seconds between successive synchronization attempts with the NTP server"; + conf->ntp.sync.interval.t = CONF_UINT; + conf->ntp.sync.interval.d.ui = 3600; + conf->ntp.sync.interval.c = validate_stub; // Only type-based checking + + conf->ntp.sync.count.k = "ntp.sync.count"; + conf->ntp.sync.count.h = "Number of NTP syncs to perform and average before updating the system time"; + conf->ntp.sync.count.t = CONF_UINT; + conf->ntp.sync.count.d.ui = 8; + conf->ntp.sync.count.c = validate_stub; // Only type-based checking + + conf->ntp.rtc.set.k = "ntp.rtc.set"; + conf->ntp.rtc.set.h = "Should FTL update a real-time clock (RTC) if available?"; + conf->ntp.rtc.set.t = CONF_BOOL; + conf->ntp.rtc.set.d.b = true; + conf->ntp.rtc.set.c = validate_stub; // Only type-based checking + + conf->ntp.rtc.device.k = "ntp.rtc.device"; + conf->ntp.rtc.device.h = "Path to the RTC device to update. Leave empty for auto-discovery"; + conf->ntp.rtc.device.a = cJSON_CreateStringReference("Path to the RTC device, e.g., \"/dev/rtc0\""); + conf->ntp.rtc.device.t = CONF_STRING; + conf->ntp.rtc.device.d.s = (char*)""; + conf->ntp.rtc.device.c = validate_stub; // Only type-based checking + + conf->ntp.rtc.utc.k = "ntp.rtc.utc"; + conf->ntp.rtc.utc.h = "Should the RTC be set to UTC?"; + conf->ntp.rtc.utc.t = CONF_BOOL; + conf->ntp.rtc.utc.d.b = true; + conf->ntp.rtc.utc.c = validate_stub; // Only type-based checking + + // struct resolver conf->resolver.resolveIPv6.k = "resolver.resolveIPv6"; conf->resolver.resolveIPv6.h = "Should FTL try to resolve IPv6 addresses to hostnames?"; @@ -1400,6 +1470,12 @@ void initConfig(struct config *conf) conf->debug.reserved.d.b = false; conf->debug.reserved.c = validate_stub; // Only type-based checking + conf->debug.ntp.k = "debug.ntp"; + conf->debug.ntp.h = "Print information about NTP synchronization"; + conf->debug.ntp.t = CONF_BOOL; + conf->debug.ntp.d.b = false; + conf->debug.ntp.c = validate_stub; // Only type-based checking + conf->debug.all.k = "debug.all"; conf->debug.all.h = "Set all debug flags at once. This is a convenience option to enable all debug flags at once. Note that this option is not persistent, setting it to true will enable all *remaining* debug flags but unsetting it will disable *all* debug flags."; conf->debug.all.t = CONF_ALL_DEBUG_BOOL; diff --git a/src/config/config.h b/src/config/config.h index 6299ffd0..a7edb463 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -191,6 +191,27 @@ struct config { struct conf_item hosts; } dhcp; + struct { + struct { + struct conf_item active; + struct conf_item address; + } ipv4; + struct { + struct conf_item active; + struct conf_item address; + } ipv6; + struct { + struct conf_item server; + struct conf_item interval; + struct conf_item count; + } sync; + struct { + struct conf_item set; + struct conf_item device; + struct conf_item utc; + } rtc; + } ntp; + struct { struct conf_item resolveIPv4; struct conf_item resolveIPv6; @@ -313,6 +334,7 @@ struct config { struct conf_item webserver; struct conf_item extra; struct conf_item reserved; + struct conf_item ntp; // all must be the last item in this struct struct conf_item all; } debug; diff --git a/src/config/dnsmasq_config.c b/src/config/dnsmasq_config.c index 8c2e2686..6b8cbc82 100644 --- a/src/config/dnsmasq_config.c +++ b/src/config/dnsmasq_config.c @@ -582,11 +582,20 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_ fputs("log-dhcp\n\n", pihole_conf); } + // Check if IPv4 NTP server is active and broadcast it as DHCP option + if(conf->ntp.ipv4.active.v.b) + { + fputs("# Add NTP server to DHCP\n", pihole_conf); + // The special address 0.0.0.0 is taken to mean "the + // address of the machine running the DHCP server" + fputs("dhcp-option=option:ntp-server,0.0.0.0\n\n", pihole_conf); + } + // Add option to ignore unknown clients if enabled if(conf->dhcp.ignoreUnknownClients.v.b) { fputs("# Ignore clients not configured below\n", pihole_conf); - fputs("dhcp-ignore=tag:!known\n", pihole_conf); + fputs("dhcp-ignore=tag:!known\n\n", pihole_conf); } // Add per-host parameters diff --git a/src/daemon.c b/src/daemon.c index 0345e046..5e7cf2db 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -265,16 +265,19 @@ pid_t FTL_gettid(void) static void terminate_threads(void) { - struct timespec ts; // Terminate threads before closing database connections and finishing shared memory killed = true; // Try to join threads to ensure cancellation has succeeded log_info("Waiting for threads to join"); for(int i = 0; i < THREADS_MAX; i++) { + log_debug(DEBUG_EXTRA, "Joining %s thread (%d)", thread_names[i], i); // Skip threads that have never been started or which are already stopped - if(!thread_running[i]) + if(threads[i] == 0) + { + log_debug(DEBUG_EXTRA, "Skipping thread as it was never started"); continue; + } // Cancel thread if it is idle if(thread_cancellable[i]) @@ -285,6 +288,8 @@ static void terminate_threads(void) } // Cancel thread if we cannot set a timeout for joining + struct timespec ts; + memset(&ts, 0, sizeof(ts)); if (clock_gettime(CLOCK_REALTIME, &ts) == -1) { log_info("Thread %s (%d) is busy, cancelling it (cannot set timeout).", @@ -297,8 +302,7 @@ static void terminate_threads(void) ts.tv_sec += 2; // Try to join thread and cancel it if it is still busy - const int s = pthread_timedjoin_np(threads[i], NULL, &ts); - if(s != 0) + if(pthread_timedjoin_np(threads[i], NULL, &ts) != 0) { log_info("Thread %s (%d) is still busy, cancelling it.", thread_names[i], i); diff --git a/src/database/common.c b/src/database/common.c index f209a50f..bcf7d61d 100644 --- a/src/database/common.c +++ b/src/database/common.c @@ -103,6 +103,15 @@ sqlite3* _dbopen(const bool readonly, const bool create, const char *func, const return NULL; } + // If the database is opened in read-write mode, actually check if it is + // writable. If it is not, close the database and return an error + if(!readonly && sqlite3_db_readonly(db, NULL)) + { + log_err("Cannot open database in read-write mode"); + dbclose(&db); + return NULL; + } + // Explicitly set busy handler to value defined in FTL.h rc = sqlite3_busy_timeout(db, DATABASE_BUSY_TIMEOUT); if( rc != SQLITE_OK ) diff --git a/src/database/database-thread.c b/src/database/database-thread.c index efdd1bd6..f8768902 100644 --- a/src/database/database-thread.c +++ b/src/database/database-thread.c @@ -78,13 +78,11 @@ static bool analyze_database(sqlite3 *db) } #define DBOPEN_OR_AGAIN() { if(!db) db = dbopen(false, false); if(!db) { thread_sleepms(DB, 5000); continue; } } -#define BREAK_IF_KILLED() { if(killed) break; } #define DBCLOSE_OR_BREAK() { dbclose(&db); BREAK_IF_KILLED(); } void *DB_thread(void *val) { // Set thread name - thread_names[DB] = "database"; thread_running[DB] = true; prctl(PR_SET_NAME, thread_names[DB], 0, 0, 0); diff --git a/src/database/message-table.c b/src/database/message-table.c index 9271bd96..031d7fb8 100644 --- a/src/database/message-table.c +++ b/src/database/message-table.c @@ -97,6 +97,8 @@ static const char *get_message_type_str(const enum message_type type) return "CERTIFICATE_DOMAIN_MISMATCH"; case CONNECTION_ERROR_MESSAGE: return "CONNECTION_ERROR"; + case NTP_MESSAGE: + return "NTP"; case MAX_MESSAGE: default: return "UNKNOWN"; @@ -131,6 +133,8 @@ static enum message_type get_message_type_from_string(const char *typestr) return CERTIFICATE_DOMAIN_MISMATCH_MESSAGE; else if (strcmp(typestr, "CONNECTION_ERROR") == 0) return CONNECTION_ERROR_MESSAGE; + else if (strcmp(typestr, "NTP") == 0) + return NTP_MESSAGE; else return MAX_MESSAGE; } @@ -230,6 +234,14 @@ static unsigned char message_blob_types[MAX_MESSAGE][5] = SQLITE_NULL, // not used SQLITE_NULL, // not used SQLITE_NULL // not used + }, + { + // NTP: The message column contains the warning/error + SQLITE_TEXT, // level (warning/error) + SQLITE_TEXT, // component (server/client) + SQLITE_NULL, // not used + SQLITE_NULL, // not used + SQLITE_NULL // not used } }; // Create message table in the database @@ -320,10 +332,8 @@ static int _add_message(const enum message_type type, sqlite3 *db; // Open database connection if((db = dbopen(false, false)) == NULL) - { - log_err("add_message() - Failed to open DB"); + // Reason for failure is logged in dbopen() return -1; - } // Ensure there are no duplicates when adding messages sqlite3_stmt* stmt = NULL; @@ -537,7 +547,7 @@ static void format_regex_message(char *plain, const int sizeof_plain, char *html } if(snprintf(html, sizeof_html, "Encountered an error when processing regex %s filter with ID %d:
%s
Error message:
%s
", - dbindex, type, dbindex, escaped_regex, escaped_warning)) + dbindex, type, dbindex, escaped_regex, escaped_warning) > sizeof_html) log_warn("format_regex_message(): Buffer too small to hold HTML message, warning truncated"); free(escaped_regex); @@ -900,6 +910,20 @@ static void format_connection_error(char *plain, const int sizeof_plain, char *h free(escaped_server); } +static void format_ntp_message(char *plain, const int sizeof_plain, char *html, const int sizeof_html, + const char *message, const char *level, const char *who) +{ + if(snprintf(plain, sizeof_plain, "%s NTP %s: %s", level, who, message) > sizeof_plain) + log_warn("format_ntp_message(): Buffer too small to hold plain message, warning truncated"); + + // Return early if HTML text is not required + if(sizeof_html < 1 || html == NULL) + return; + + if(snprintf(html, sizeof_html, "%s in NTP %s:
%s
", level, who, message) > sizeof_html) + log_warn("format_ntp_message(): Buffer too small to hold HTML message, warning truncated"); +} + int count_messages(const bool filter_dnsmasq_warnings) { int count = 0; @@ -1147,6 +1171,18 @@ bool format_messages(cJSON *array) break; } + case NTP_MESSAGE: + { + const char *message = (const char*)sqlite3_column_text(stmt, 3); + const char *level = (const char*)sqlite3_column_text(stmt, 4); + const char *who = (const char*)sqlite3_column_text(stmt, 5); + + format_ntp_message(plain, sizeof(plain), html, sizeof(html), + message, level, who); + + break; + } + case MAX_MESSAGE: // Fall through default: log_warn("format_messages() - Unknown message type: %s", mtypestr); @@ -1206,9 +1242,7 @@ void logg_regex_warning(const char *type, const char *warning, const int dbindex return; // Add to database - const int rowid = add_message(REGEX_MESSAGE, regex, type, warning, dbindex); - if(rowid == -1) - log_err("logg_regex_warning(): Failed to add message to database"); + add_message(REGEX_MESSAGE, regex, type, warning, dbindex); } void logg_subnet_warning(const char *ip, const int matching_count, const char *matching_ids, @@ -1226,10 +1260,8 @@ void logg_subnet_warning(const char *ip, const int matching_count, const char *m log_warn("%s", buf); // Log to database - const int rowid = add_message(SUBNET_MESSAGE, ip, matching_count, names, matching_ids, chosen_match_text, chosen_match_id); + add_message(SUBNET_MESSAGE, ip, matching_count, names, matching_ids, chosen_match_text, chosen_match_id); - if(rowid == -1) - log_err("logg_subnet_warning(): Failed to add message to database"); free(names); } @@ -1248,10 +1280,8 @@ void logg_hostname_warning(const char *ip, const char *name, const unsigned int log_warn("%s", buf); // Log to database - const int rowid = add_message(HOSTNAME_MESSAGE, ip, name, (const int)pos); + add_message(HOSTNAME_MESSAGE, ip, name, (const int)pos); - if(rowid == -1) - log_err("logg_hostname_warning(): Failed to add message to database"); } void logg_fatal_dnsmasq_message(const char *message) @@ -1264,10 +1294,8 @@ void logg_fatal_dnsmasq_message(const char *message) log_crit("%s", buf); // Log to database - const int rowid = add_message_no_args(DNSMASQ_CONFIG_MESSAGE, message); + add_message_no_args(DNSMASQ_CONFIG_MESSAGE, message); - if(rowid == -1) - log_err("logg_fatal_dnsmasq_message(): Failed to add message to database"); } void logg_rate_limit_message(const char *clientIP, const unsigned int rate_limit_count) @@ -1282,10 +1310,8 @@ void logg_rate_limit_message(const char *clientIP, const unsigned int rate_limit log_info("%s", buf); // Log to database - const int rowid = add_message(RATE_LIMIT_MESSAGE, clientIP, config.dns.rateLimit.count.v.ui, config.dns.rateLimit.interval.v.ui, turnaround); + add_message(RATE_LIMIT_MESSAGE, clientIP, config.dns.rateLimit.count.v.ui, config.dns.rateLimit.interval.v.ui, turnaround); - if(rowid == -1) - log_err("logg_rate_limit_message(): Failed to add message to database"); } void logg_warn_dnsmasq_message(char *message) @@ -1298,10 +1324,8 @@ void logg_warn_dnsmasq_message(char *message) log_warn("%s", buf); // Log to database - const int rowid = add_message_no_args(DNSMASQ_WARN_MESSAGE, message); + add_message_no_args(DNSMASQ_WARN_MESSAGE, message); - if(rowid == -1) - log_err("logg_warn_dnsmasq_message(): Failed to add message to database"); } void log_resource_shortage(const double load, const int nprocs, const int shmem, const int disk, const char *path, const char *msg) @@ -1317,10 +1341,9 @@ void log_resource_shortage(const double load, const int nprocs, const int shmem, log_warn("%s", buf); // Log to database - const int rowid = add_message(LOAD_MESSAGE, "excessive load", load, nprocs); + add_message(LOAD_MESSAGE, "excessive load", load, nprocs); + - if(rowid == -1) - log_err("log_resource_shortage(): Failed to add message to database"); } else if(shmem > -1) { @@ -1330,10 +1353,9 @@ void log_resource_shortage(const double load, const int nprocs, const int shmem, log_warn("%s", buf); // Log to database - const int rowid = add_message(SHMEM_MESSAGE, path, shmem, msg); + add_message(SHMEM_MESSAGE, path, shmem, msg); + - if(rowid == -1) - log_err("log_resource_shortage(): Failed to add message to database"); } else if(disk > -1) { @@ -1367,12 +1389,11 @@ void log_resource_shortage(const double load, const int nprocs, const int shmem, log_warn("%s", buf); // Log to database - const int rowid = fsdetails != NULL ? + fsdetails != NULL ? add_message(DISK_MESSAGE_EXTENDED, path, disk, msg, fsdetails->mnt_type, fsdetails->mnt_dir) : add_message(DISK_MESSAGE, path, disk, msg); - if(rowid == -1) - log_err("log_resource_shortage(): Failed to add message to database"); + } } @@ -1386,10 +1407,8 @@ void logg_inaccessible_adlist(const int dbindex, const char *address) log_warn("%s", buf); // Log to database - const int rowid = add_message(INACCESSIBLE_ADLIST_MESSAGE, address, dbindex); + add_message(INACCESSIBLE_ADLIST_MESSAGE, address, dbindex); - 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) @@ -1402,10 +1421,8 @@ void log_certificate_domain_mismatch(const char *certfile, const char *domain) log_warn("%s", buf); // Log to database - const int rowid = add_message(CERTIFICATE_DOMAIN_MISMATCH_MESSAGE, certfile, domain); + add_message(CERTIFICATE_DOMAIN_MISMATCH_MESSAGE, certfile, domain); - if(rowid == -1) - log_err("log_certificate_domain_mismatch(): Failed to add message to database"); } void log_connection_error(const char *server, const char *reason, const char *error) @@ -1418,8 +1435,26 @@ void log_connection_error(const char *server, const char *reason, const char *er log_warn("%s", buf); // Log to database - const int rowid = add_message(CONNECTION_ERROR_MESSAGE, server, reason, error); + add_message(CONNECTION_ERROR_MESSAGE, server, reason, error); + +} + +void log_ntp_message(const bool error, const bool server, const char *message) +{ + const char *who = server ? "server" : "client"; + const char *level = error ? "Error" : "Warning"; + + // Create message + char buf[2048]; + format_ntp_message(buf, sizeof(buf), NULL, 0, message, level, who); + + // Log to FTL.log + if(error) + log_err("%s", buf); + else + log_warn("%s", buf); + + // Log to database + add_message(NTP_MESSAGE, message, level, who); - if(rowid == -1) - log_err("logg_connection_error(): Failed to add message to database"); } diff --git a/src/database/message-table.h b/src/database/message-table.h index d92bbe8d..5354af6f 100644 --- a/src/database/message-table.h +++ b/src/database/message-table.h @@ -30,5 +30,6 @@ void log_resource_shortage(const double load, const int nprocs, const int shmem, void logg_inaccessible_adlist(const int dbindex, const char *address); void log_certificate_domain_mismatch(const char *certfile, const char *domain); void log_connection_error(const char *server, const char *reason, const char *error); +void log_ntp_message(const bool error, const bool server, const char *message); #endif //MESSAGETABLE_H diff --git a/src/database/query-table.c b/src/database/query-table.c index 342730a4..435f560a 100644 --- a/src/database/query-table.c +++ b/src/database/query-table.c @@ -21,8 +21,11 @@ #include "overTime.h" #include "database/common.h" #include "timers.h" +// runGC() +#include "gc.h" static sqlite3 *_memdb = NULL; +static bool store_in_database = false; static double new_last_timestamp = 0; static unsigned int new_total = 0, new_blocked = 0; static unsigned long last_mem_db_idx = 0, last_disk_db_idx = 0; @@ -1367,6 +1370,11 @@ bool queries_to_database(void) log_debug(DEBUG_DATABASE, "Not storing query in database as there are none"); return true; } + if(!store_in_database) + { + log_debug(DEBUG_DATABASE, "Not storing query in database as this is disabled"); + return true; + } // Loop over recent queries and store new or changed ones in the // in-memory database @@ -1626,3 +1634,22 @@ bool queries_to_database(void) return true; } + +void load_queries_from_disk(void) +{ + // Compensate for possible jumps in time + runGC(time(NULL), NULL, false); + + // Skip if we are not supposed to load queries from disk + if(!config.database.DBimport.v.b) + return; + + // Try to import queries from long-term database if available + import_queries_from_disk(); + DB_read_queries(); + + // Log some information about the imported queries (if any) + log_counter_info(); + + store_in_database = true; +} diff --git a/src/database/query-table.h b/src/database/query-table.h index 9fecb9ea..71458e28 100644 --- a/src/database/query-table.h +++ b/src/database/query-table.h @@ -119,6 +119,7 @@ bool add_additional_info_column(sqlite3 *db); void DB_read_queries(void); void update_disk_db_idx(void); bool queries_to_database(void); +void load_queries_from_disk(void); bool optimize_queries_table(sqlite3 *db); bool create_addinfo_table(sqlite3 *db); diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index 366195bc..72bb41ce 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -58,6 +58,8 @@ #include "config/config.h" // FTL_fork_and_bind_sockets() #include "main.h" +// ntp_server_start() +#include "ntp/ntp.h" // Private prototypes static void print_flags(const unsigned int flags); @@ -2902,19 +2904,9 @@ void FTL_fork_and_bind_sockets(struct passwd *ent_pw, bool dnsmasq_start) // Flush messages stored in the long-term database flush_message_table(); - // Try to import queries from long-term database if available - if(config.database.DBimport.v.b) - { - import_queries_from_disk(); - DB_read_queries(); - } - // Initialize in-memory database starting index update_disk_db_idx(); - // Log some information about the imported queries (if any) - log_counter_info(); - // Handle real-time signals in this process (and its children) // Helper processes are already split from the main instance // so they will not listen to real-time signals @@ -2924,8 +2916,16 @@ void FTL_fork_and_bind_sockets(struct passwd *ent_pw, bool dnsmasq_start) // detached mode pthread_attr_t attr; // Initialize thread attributes object with default attribute values + // Do NOT detach threads as we want to join them during shutdown with a + // fixed timeout to give them time to clean up and finish their work pthread_attr_init(&attr); + // Initialize NTP server + ntp_server_start(&attr); + + // Start NTP sync thread + ntp_start_sync_thread(&attr); + // Start database thread if database is used if(pthread_create( &threads[DB], &attr, DB_thread, NULL ) != 0) { diff --git a/src/enums.h b/src/enums.h index 09769a9c..406e8f31 100644 --- a/src/enums.h +++ b/src/enums.h @@ -162,6 +162,7 @@ enum debug_flag { DEBUG_WEBSERVER, DEBUG_EXTRA, DEBUG_RESERVED, + DEBUG_NTP, DEBUG_MAX } __attribute__ ((packed)); @@ -249,8 +250,8 @@ enum thread_types { DB, GC, DNSclient, - CONF_READER, TIMER, + NTP, THREADS_MAX } __attribute__ ((packed)); @@ -275,6 +276,7 @@ enum message_type { DISK_MESSAGE_EXTENDED, CERTIFICATE_DOMAIN_MISMATCH_MESSAGE, CONNECTION_ERROR_MESSAGE, + NTP_MESSAGE, MAX_MESSAGE, } __attribute__ ((packed)); diff --git a/src/gc.c b/src/gc.c index a142ca23..3d4f0962 100644 --- a/src/gc.c +++ b/src/gc.c @@ -481,7 +481,6 @@ static bool check_files_on_same_device(const char *path1, const char *path2) void *GC_thread(void *val) { // Set thread name - thread_names[GC] = "housekeeper"; thread_running[GC] = true; prctl(PR_SET_NAME, thread_names[GC], 0, 0, 0); diff --git a/src/log.c b/src/log.c index a10fe836..0fbbe447 100644 --- a/src/log.c +++ b/src/log.c @@ -219,6 +219,8 @@ const char *debugstr(const enum debug_flag flag) return "DEBUG_WEBSERVER"; case DEBUG_RESERVED: return "DEBUG_RESERVED"; + case DEBUG_NTP: + return "DEBUG_NTP"; case DEBUG_MAX: return "DEBUG_MAX"; case DEBUG_NONE: // fall through diff --git a/src/ntp/CMakeLists.txt b/src/ntp/CMakeLists.txt new file mode 100644 index 00000000..5cdc5d12 --- /dev/null +++ b/src/ntp/CMakeLists.txt @@ -0,0 +1,20 @@ +# Pi-hole: A black hole for Internet advertisements +# (c) 2024 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# FTL Engine +# /src/ntp/CMakeList.txt +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +set(ntp_sources + server.c + client.c + rtc.c + ntp.h + ) + +add_library(ntp OBJECT ${ntp_sources}) +target_compile_options(ntp PRIVATE "${EXTRAWARN}") +target_include_directories(ntp PRIVATE ${PROJECT_SOURCE_DIR}/src) diff --git a/src/ntp/client.c b/src/ntp/client.c new file mode 100644 index 00000000..532689a2 --- /dev/null +++ b/src/ntp/client.c @@ -0,0 +1,641 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2024 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* NTP client routines +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#include "ntp.h" +// close() +#include +// clock_gettime() +#include +// socket(), connect(), send(), recv(), AF_INET, SOCK_DGRAM, IPPROTO_UDP +#include +// getaddrinfo(), freeaddrinfo(), struct addrinfo +#include +// memcpy() +#include +// pow() +#include +// ctime() +#include +// errno +#include +// PRIi64 +#include +// config struct +#include "config/config.h" +// adjtime() +#include +// threads[] +#include "daemon.h" +// thread_names[] +#include "signals.h" +// adjtimex() +#include +// log_ntp_message() +#include "database/message-table.h" +// load_queries_from_disk() +#include "database/query-table.h" +struct ntp_sync +{ + bool valid; + uint64_t org; + uint64_t xmt; + double theta; + double delta; + double precision; +}; + +// Create minimal NTP request, see server implementation for details about the +// packet structure +static bool request(int fd, const char *server, struct ntp_sync *ntp) +{ + // NTP Packet buffer + unsigned char buf[48] = {0}; + + // LI = 0, VN = 4 (current version), Mode = 3 (Client) + buf[0] = 0x23; + + // Minimum poll interval (2^6 = 64 seconds) + buf[2] = 0x06; + + // Set Reference Timestamp (ref) to 0 + // This is the time at which the local clock was last set or corrected. + memset(&buf[8], 0, sizeof(uint64_t)); + + // Set Origin Timestamp (org) in NTP format + ntp->org = gettime64(); + const uint64_t norg = hton64(ntp->org); + memcpy(&buf[40], &norg, sizeof(norg)); + + // Send request + if(send(fd, buf, 48, 0) != 48) + { + log_err("Failed to send data to NTP server %s: %s", + server, errno == EAGAIN ? "Timeout" : strerror(errno)); + return false; + } + + return true; +} + +// Display NTP time in human-readable format +// This function is similar to get_timestr() in src/log.c but differs in that it +// includes microseconds whereas get_timestr() only includes milliseconds +static void format_NTP_time(char time_str[TIMESTR_SIZE], const uint64_t ntp_time) +{ + struct timeval client_time; + client_time.tv_sec = NTPtoSEC(ntp_time); + client_time.tv_usec = NTPtoUSEC(ntp_time); + struct tm *client_tm = localtime(&client_time.tv_sec); + snprintf(time_str, TIMESTR_SIZE, "%04i-%02i-%02i %02i:%02i:%02i.%06"PRIi64" %s", + client_tm->tm_year + 1900, client_tm->tm_mon + 1, client_tm->tm_mday, + client_tm->tm_hour, client_tm->tm_min, client_tm->tm_sec, client_time.tv_usec, + client_tm->tm_zone); + time_str[TIMESTR_SIZE - 1] = '\0'; +} + +// Print NTP timestamp in human-readable form for debugging +void print_debug_time(const char *label, const uint32_t *u32p, const uint64_t ntp_time) +{ + // Get the time from the appropriate buffer + uint64_t timevar; + if(u32p != NULL) + { + memcpy(&timevar, u32p, sizeof(uint64_t)); + // Convert to host byte order + timevar = ntoh64(timevar); + } + else + { + // Use the provided time (already in host byte order) + timevar = ntp_time; + } + + + // Format the time + char time_str[TIMESTR_SIZE]; + format_NTP_time(time_str, timevar); + + // Print the time + log_debug(DEBUG_NTP, "%s: %08"PRIx64".%08"PRIx64" = %s", label, + (timevar >> 32) & 0xFFFFFFFF, timevar & 0xFFFFFFFF, time_str); +} + +static uint64_t get_new_time(struct timeval *unix_time, const double offset) +{ + // Get current time + gettimeofday(unix_time, NULL); + + // Convert from double to native format (signed) and add to the + // current time. Note the addition is done in native format to + // avoid overflow or loss of precision. + const uint64_t ntp_time = U2LFP(*unix_time) + D2LFP(offset); + + // Convert NTP to native format + unix_time->tv_sec = NTPtoSEC(ntp_time); + unix_time->tv_usec = NTPtoUSEC(ntp_time); + + return ntp_time; +} + +static bool settime_step(struct timeval *unix_time, const double offset) +{ + log_debug(DEBUG_NTP, "Stepping system time by %e s", offset); + + // Set time immediately + if(settimeofday(unix_time, NULL) != 0) + { + char errbuf[1024]; + strncpy(errbuf, "Failed to set time during NTP sync: ", sizeof(errbuf)); + strncat(errbuf, errno == EPERM ? "Insufficient permissions" : strerror(errno), sizeof(errbuf) - strlen(errbuf) - 1); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, false, errbuf); + return false; + } + + return true; +} + +static bool settime_skew(const double offset) +{ + // This function gradually adjusts the system clock. + // + // Linux uses David L. Mills' clock adjustment algorithm (see RFC 5905). + // If the adjustment in delta is positive, then the system clock is + // speeded up by some small percentage (i.e., by adding a small amount + // of time to the clock value in each second) until the adjustment has + // been completed. If the adjustment in delta is negative, then the + // clock is slowed down in a similar fashion. + // + // If a clock adjustment from an earlier adjtime() call is already in + // progress at the time of a later adjtime() call, and delta is not NULL + // for the later call, then the earlier adjustment is stopped, but any + // already completed part of that adjustment is not undone. + // + // The adjustment that adjtimex() makes to the clock is carried out in + // such a manner that the clock is always monotonically increasing. + // Using adjtimex() to adjust the time prevents the problems that can be + // caused for certain applications (e.g., make(1)) by abrupt positive or + // negative jumps in the system time. + // + // adjtimex() is intended to be used to make small adjustments to the + // system time. The actual time adjustment rate is implementation-specific + // but is typically on the order of 500 ppm, i.e., 0.5 ms/s. + // + // man rtc(4) adds: + // When the kernel's system time is synchronized with an external + // reference using adjtimex() it will update a designated RTC + // periodically every 11 minutes. + + struct timex tx = { 0 }; + tx.offset = 1000000 * offset; + tx.modes = ADJ_OFFSET_SINGLESHOT; + + log_debug(DEBUG_NTP, "Gradually adjusting system time by %ld us", tx.offset); + + if(adjtimex(&tx) < 0) + { + char errbuf[1024]; + strncpy(errbuf, "Failed to adjust time during NTP sync: ", sizeof(errbuf)); + strncat(errbuf, errno == EPERM ? "Insufficient permissions" : strerror(errno), sizeof(errbuf) - strlen(errbuf) - 1); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, false, errbuf); + return false; + } + + return true; +} + +static bool reply(int fd, const char *server, struct ntp_sync *ntp, const bool verbose) +{ + // NTP Packet buffer + unsigned char buf[48]; + + // Receive reply + if(recv(fd, buf, 48, 0) < 48) + { + log_err("Failed to receive data from NTP server %s: %s", + server, errno == EAGAIN ? "Timeout" : strerror(errno)); + return false; + } + + // Extract precision of server clock + signed char rho = (signed char)buf[3]; + if(rho < -32 || rho > 0) + { + // Accepted limits are 2^-32 (~ 0.2 nanoseconds) + // to 2^0 (= 1 second) + char errbuf[1024]; + snprintf(errbuf, sizeof(errbuf), "Received NTP reply has invalid precision: 2^(%i), assuming microsecond accuracy", rho); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(false, false, errbuf); + rho = -19; + } + // Compute precision of server clock in seconds 2^rho + ntp->precision = pow(2, rho); + + // Extract root delay and root dispersion of server clock + uint32_t srv_root_delay, srv_root_dispersion; + memcpy(&srv_root_delay, &buf[4], sizeof(srv_root_delay)); + memcpy(&srv_root_dispersion, &buf[8], sizeof(srv_root_dispersion)); + + // Extract Transmit Timestamp + uint64_t netbuffer; + // ref = Reference Timestamp (Time at which the clock was last set or corrected) + memcpy(&netbuffer, &buf[16], sizeof(netbuffer)); + const uint64_t ref = ntoh64(netbuffer); + // org = Origin Timestamp (Transmit Timestamp @ Client) + memcpy(&netbuffer, &buf[24], sizeof(netbuffer)); + const uint64_t org = ntoh64(netbuffer); + // rec = Receive Timestamp (Receive Timestamp @ Server) + memcpy(&netbuffer, &buf[32], sizeof(netbuffer)); + const uint64_t rec = ntoh64(netbuffer); + // xmt = Transmit Timestamp (Transmit Timestamp @ Server) + memcpy(&netbuffer, &buf[40], sizeof(netbuffer)); + ntp->xmt = ntoh64(netbuffer); + + // dst = Destination Timestamp (Receive Timestamp @ Client) + uint64_t dst = gettime64(); + + // Check org_ and org are identical (otherwise, the reply corresponds to + // a different request and should be ignored), note that the byte order + // of the received packet is already converted while org_ is still in + // network byte order + if(ntp->org != org) + { + log_warn("Received NTP reply does not match request (request %"PRIx64", reply %"PRIx64"), ignoring", + ntp->org, org); + return false; + } + + // Check stratum, mode, version, etc. + if((buf[0] & 0x07) != 4) + { + log_warn("Received NTP reply has invalid version, ignoring"); + return false; + } + + // Calculate delay and offset + const double T1 = ntp->org / FRAC; + const double T2 = rec / FRAC; + const double T3 = ntp->xmt / FRAC; + const double T4 = dst / FRAC; + + // RFC 5905, Section 8: On-wire protocol + // It is recommended to use double precision floating point arithmetic + // for the calculations to allow unambiguous interpretation of the + // results within the maximum adjustment range of 68 years. + + // Compute offset of client clock relative to server clock + ntp->theta = ( ( T2 - T1 ) + ( T3 - T4 ) ) / 2; + // Compute round-trip delay, which represents the delay of the packet + // passing through the network, which can be due switches and network + // technologies are highly variable + ntp->delta = ( T4 - T1 ) - ( T3 - T2 ); + + // This reply is valid + ntp->valid = true; + + // In some scenarios where the initial frequency offset of the client is + // relatively large and the actual propagation time small, it is + // possible for the delay computation to become negative. For instance, + // if the frequency difference is 100 ppm and the interval T4-T1 is 64 + // s, the apparent delay is -6.4 ms. Since negative values are + // misleading in subsequent computations, the value of delta should be + // clamped not less than s.rho, where s.rho is the system precision + // described in Section 11.1, expressed in seconds. + if(ntp->delta < ntp->precision) + ntp->delta = 0; + + // Return early if not verbose + if(!config.debug.ntp.v.b) + return true; + + // Print current time at server + print_debug_time("Server reference time", NULL, ref); + + // Print current time at client + print_debug_time("Current time at client", NULL, dst); + + // Print current time at server + print_debug_time("Current time at server", NULL, ntp->xmt); + + // Print offset and delay + log_debug(DEBUG_NTP, "Time offset: %e s", ntp->theta); + log_debug(DEBUG_NTP, "Round-trip delay: %e s", ntp->delta); + const uint32_t root_delay = ntohl(srv_root_delay); + log_debug(DEBUG_NTP, "Root delay: %e s", FP2D(root_delay)); + const uint32_t root_dispersion = ntohl(srv_root_dispersion); + log_debug(DEBUG_NTP, "Root dispersion: %e s", FP2D(root_dispersion)); + + return true; +} + +static int getsock(const struct addrinfo *saddr) +{ + // Create UDP socket + const int protocol = saddr->ai_addrlen == sizeof(struct sockaddr_in6) ? AF_INET6 : AF_INET; + const int s = socket(protocol, SOCK_DGRAM, IPPROTO_UDP); + if(s == -1) + { + char errbuf[1024]; + strncpy(errbuf, "Cannot create UDP socket: ", sizeof(errbuf)); + strncat(errbuf, strerror(errno), sizeof(errbuf) - strlen(errbuf) - 1); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, false, errbuf); + return -1; + } + + // Set socket timeout to 5 seconds + struct timeval tv; + tv.tv_sec = 5; + tv.tv_usec = 0; + if(setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) + { + char errbuf[1024]; + strncpy(errbuf, "Cannot set socket timeout: ", sizeof(errbuf)); + strncat(errbuf, strerror(errno), sizeof(errbuf) - strlen(errbuf) - 1); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, false, errbuf); + close(s); + return -1; + } + + // Set address to send to/receive from + if(connect(s, saddr->ai_addr, saddr->ai_addrlen) != 0) + { + char errbuf[1024]; + strncpy(errbuf, "Cannot connect to NTP server: ", sizeof(errbuf)); + strncat(errbuf, strerror(errno), sizeof(errbuf) - strlen(errbuf) - 1); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, false, errbuf); + close(s); + return -1; + } + + // Return socket + return s; +} + +bool ntp_client(const char *server, const bool settime, const bool print) +{ + // Resolve server address + int eai; + struct addrinfo *saddr; + if((eai = getaddrinfo(server, "ntp", NULL, &saddr)) != 0) + { + char errbuf[1024]; + strncpy(errbuf, "Cannot resolve NTP server address: ", sizeof(errbuf)); + strncat(errbuf, errno == EAI_SYSTEM ? strerror(errno) : gai_strerror(eai), + sizeof(errbuf) - strlen(errbuf) - 1); + if(eai == EAI_NONAME || eai == EAI_NODATA) + { + strncat(errbuf, " \"", sizeof(errbuf) - strlen(errbuf) - 1); + strncat(errbuf, server, sizeof(errbuf) - strlen(errbuf) - 1); + strncat(errbuf, "\"", sizeof(errbuf) - strlen(errbuf) - 1); + } + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, false, errbuf); + return false; + } + + const unsigned int count = config.ntp.sync.count.v.ui; + struct ntp_sync *ntp = calloc(count, sizeof(struct ntp_sync)); + if(ntp == NULL) + { + log_err("Cannot allocate memory for NTP client"); + return false; + } + + // Send and receive NTP packets + for(unsigned int i = 0; i < count; i++) + { + // Create socket + const int s = getsock(saddr); + if(s == -1) + continue; + + // Send request + if(!request(s, server, &ntp[i])) + { + close(s); + free(ntp); + freeaddrinfo(saddr); + return false; + } + // Get reply + if(!reply(s, server, &ntp[i], false)) + { + close(s); + continue; + } + + // Close socket + close(s); + + // Sleep for some time to avoid flooding the server + if(print) + printf("."); + fflush(stdout); + usleep(NTP_DELAY); + } + if(print) + printf("\n"); + + // Free allocated memory + freeaddrinfo(saddr); + + // Compute average and standard deviation + unsigned int valid = 0; + double theta_avg = 0.0, theta_stdev = 0.0; + double delta_avg = 0.0, delta_stdev = 0.0; + for(unsigned int i = 0; i < count; i++) + { + // Skip invalid values + if(fabs(ntp[i].theta) < ntp[i].precision || + fabs(ntp[i].delta) < ntp[i].precision || + !ntp[i].valid) + continue; + + theta_avg += ntp[i].theta; + delta_avg += ntp[i].delta; + valid++; + } + + if(valid == 0) + { + log_ntp_message(false, false, "No valid NTP replies received, check server and network connectivity"); + free(ntp); + return false; + } + log_info("Received %u/%u valid NTP replies from %s", valid, count, server); + + theta_avg /= valid; + delta_avg /= valid; + for(unsigned int i = 0; i < count; i++) + { + // Skip invalid values + if(fabs(ntp[i].theta) < ntp[i].precision || + fabs(ntp[i].delta) < ntp[i].precision || + !ntp[i].valid) + continue; + + theta_stdev += pow(ntp[i].theta - theta_avg, 2); + delta_stdev += pow(ntp[i].delta - delta_avg, 2); + } + theta_stdev = sqrt(theta_stdev / valid); + delta_stdev = sqrt(delta_stdev / valid); + + log_debug(DEBUG_NTP, "Average time offset: (%e +/- %e) s", theta_avg, theta_stdev); + log_debug(DEBUG_NTP, "Average round-trip delay: (%e +/- %e) s", delta_avg, delta_stdev); + + // Reject synchronization if the standard deviation of the time offset + // or round-trip delay is larger than 1 second + if(theta_stdev > 1.0 || delta_stdev > 1.0) + { + log_ntp_message(false, false, "Standard deviation of time offset is too large, rejecting synchronization"); + free(ntp); + return false; + } + + // Compute trimmed mean (average excluding outliers) + double theta_trim = 0.0, delta_trim = 0.0; + unsigned int trim = 0; + for(unsigned int i = 0; i < count; i++) + { + // Skip invalid values + if(fabs(ntp[i].theta) < ntp[i].precision || + fabs(ntp[i].delta) < ntp[i].precision || + !ntp[i].valid) + continue; + + // Skip outliers + // We consider values > 2 standard deviations from the mean as + // outliers + if(fabs(ntp[i].theta - theta_avg) > 2 * theta_stdev || + fabs(ntp[i].delta - delta_avg) > 2 * delta_stdev) + continue; + + theta_trim += ntp[i].theta; + delta_trim += ntp[i].delta; + trim++; + } + + // Free allocated memory + free(ntp); + + if(trim == 0) + { + log_warn("No valid NTP replies after outlier removal, check server and network connectivity"); + return false; + } + theta_trim /= trim; + delta_trim /= trim; + + log_info("Time offset: %e ms (excluded %u outliers)", 1e3*theta_trim, count - trim); + log_info("Round-trip delay: %e ms (excluded %u outliers)", 1e3*delta_trim, count - trim); + + // Set time if requested + if(settime) + { + // Calculate corrected time + struct timeval unix_time; + const uint64_t ntp_time = get_new_time(&unix_time, theta_trim); + + // If the clock deviates more than 0.5 seconds from the NTP server, + // the time is updated immediately. Otherwise, the time is updated + // gradually to avoid sudden jumps in the system clock. + // The threshold of 0.5 seconds is hard-wired into the kernel + // since Linux 2.6.26, see man ntp_adjtime(2) for details. + bool success; + if(fabs(theta_trim) > 0.5) + success = settime_step(&unix_time, theta_trim); + else + success = settime_skew(theta_trim); + + // Return early if time could not be set + if(!success) + return false; + + // Update last NTP sync time + ntp_last_sync = ntp_time; + + // Compute our server's root dispersion and delay + // Both quantities are the maximum error and maximum delay of + // the server's time relative to the reference time. The root + // dispersion is the maximum error of the server's time relative + // to the reference time, while the root delay is the maximum + // delay of the server's time relative to the reference time + ntp_root_delay = D2FP(theta_trim); + ntp_root_dispersion = D2FP(theta_stdev); + + // Finally, adjust RTC if configured + if(config.ntp.rtc.set.v.b) + ntp_sync_rtc(); + } + + // Offset and delay larger than 0.1 seconds are considered as invalid + // during local testing (e.g., when the server is on the same machine) + return theta_avg < 0.1 && delta_avg < 0.1; +} + +static void *ntp_client_thread(void *arg) +{ + // Set thread name + thread_running[NTP] = true; + prctl(PR_SET_NAME, thread_names[NTP], 0, 0, 0); + + // Run NTP client + bool first_run = true; + while(!killed) + { + // Run NTP client + ntp_client(config.ntp.sync.server.v.s, true, false); + + // Load queries from database after first NTP synchronization + if(first_run) + { + load_queries_from_disk(); + first_run = false; + } + + // Intermediate cancellation-point + BREAK_IF_KILLED(); + + // Sleep before retrying + thread_sleepms(NTP, 1000 * config.ntp.sync.interval.v.ui); + } + + log_info("Terminating NTP thread"); + thread_running[NTP] = false; + + return NULL; +} + +bool ntp_start_sync_thread(pthread_attr_t *attr) +{ + // Return early if NTP client is disabled + if(config.ntp.sync.server.v.s == NULL || + strlen(config.ntp.sync.server.v.s) == 0 || + config.ntp.sync.interval.v.ui == 0) + { + load_queries_from_disk(); + return false; + } + + // Create thread + if(pthread_create(&threads[NTP], attr, ntp_client_thread, NULL) != 0) + { + log_err("Cannot create NTP client thread"); + load_queries_from_disk(); + return false; + } + + return true; +} diff --git a/src/ntp/ntp.h b/src/ntp/ntp.h new file mode 100644 index 00000000..72445423 --- /dev/null +++ b/src/ntp/ntp.h @@ -0,0 +1,78 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2024 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* NTP prototypes +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#ifndef NTP_H +#define NTP_H + +#include "FTL.h" +// TIMESTR_SIZE +#include "log.h" + +// uint64_t +#include +// bool +#include + +// Get current time in NTP (64bit) format +uint64_t gettime64(void); + +// Print NTP timestamp in human-readable form +void print_debug_time(const char *label, const uint32_t *u32p, const uint64_t ntp_time); + +// Start NTP server +bool ntp_server_start(pthread_attr_t *attr); + +// Start NTP client +bool ntp_client(const char *server, const bool settime, const bool print); + +// Start NTP sync thread +bool ntp_start_sync_thread(pthread_attr_t *attr); + +// Sync RTC time +bool ntp_sync_rtc(void); + +// Number of NTP queries to average. The more queries, the more accurate the +// time, but the longer it takes to synchronize. The minimum is 1. +#define NTP_AVERGAGE_COUNT 8 + +// Delay between consecutive NTP queries in microseconds +#define NTP_DELAY 500000 + +// number of seconds between 1900 and 1970 (MSB=1) +#define DIFF_SEC_1900_1970 (2208988800UL) +// number of seconds between 1970 and Feb 7, 2036 (6:28:16 UTC) (MSB=0) +#define DIFF_SEC_1970_2036 (2085978496UL) + +// Timestamp conversion macroni (RFC 5905, Appendix A) +#define FRIC 65536. // 2^16 as a double +#define D2FP(r) ((uint32_t)((r) * FRIC)) // NTP short +#define FP2D(r) ((double)(r) / FRIC) +#define FRAC 4294967296. // 2^32 as double +#define D2LFP(a) ((uint64_t)((a) * FRAC)) // NTP timestamp +#define LFP2D(a) ((double)(a) / FRAC) +#define U2LFP(a) (((uint64_t)((a).tv_sec + DIFF_SEC_1900_1970) << 32) + (uint64_t) ((a).tv_usec / 1e6 * FRAC)) + +// Convert NTP timestamp to seconds and microseconds +//#define NTPtoSEC(x) (((x & 0x80000000) != 0) ? ((x >> 32) - DIFF_SEC_1900_1970) : ((x >> 32) + DIFF_SEC_1970_2036)) +#define NTPtoSEC(x) ((x >> 32) - DIFF_SEC_1900_1970) +#define NTPtoUSEC(x) (suseconds_t)((LFP2D(x & 0xFFFFFFFF) * 1e6)) + +// Convert uint64_t to network byte order and vice versa +#define hton64(x) ((((uint64_t)htonl(x)) << 32) + htonl((x) >> 32)) +#define ntoh64(x) ((((uint64_t)ntohl(x)) << 32) + ntohl((x) >> 32)) + +extern uint64_t ntp_last_sync; +extern uint32_t ntp_root_delay; +extern uint32_t ntp_root_dispersion; + +#endif // NTP_H + + + diff --git a/src/ntp/rtc.c b/src/ntp/rtc.c new file mode 100644 index 00000000..abeb457e --- /dev/null +++ b/src/ntp/rtc.c @@ -0,0 +1,296 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2024 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* Real Time Clock (RTC) functions +* The routines in this file have been inspired by man pages +* and the source of the hwclock which is part of the util-linux +* project (https://github.com/util-linux/util-linux/) +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#include "ntp/ntp.h" + +// ioctl() +#include +// RTC +#include +// O_WRONLY +#include +// struct config +#include "config/config.h" + +// List of RTC devices from +// https://github.com/util-linux/util-linux/blob/41e7686c9ad1ea7892b9d8941c266869bf6a28dd/sys-utils/hwclock-rtc.c#L85-L93 +static const char * const rtc_devices[] = { +#ifdef __ia64__ + "/dev/efirtc", + "/dev/misc/efirtc", +#endif + "/dev/rtc0", + "/dev/rtc", + "/dev/misc/rtc" +}; + +static void print_tm_time(const char *label, const struct tm *tm) +{ + char timestr[TIMESTR_SIZE] = { 0 }; + strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S", tm); + log_info("%s %s", label, timestr); +} + +// Try to find the RTC device and open it +static int open_rtc(void) +{ + int rtc_fd = -1; + + // Get current user's UID and GID + const uid_t uid = getuid(); + const gid_t gid = getgid(); + + // If the user has specified an RTC device, try to open it + if(config.ntp.rtc.device.v.s != NULL && + strlen(config.ntp.rtc.device.v.s) > 0) + { + // Open the RTC device + rtc_fd = open(config.ntp.rtc.device.v.s, O_RDONLY); + if (rtc_fd != -1) + { + log_debug(DEBUG_NTP, "Successfully opened RTC at \"%s\"", + config.ntp.rtc.device.v.s); + return rtc_fd; + } + + // If the open failed because of permissions, try to change them + // momentarily. On some embedded systems, the RTC device is owned by + // root exclusively and users do not have permission to even open it. + // Without being able to access the RTC, the capability to set the + // time (CAP_SYS_TIME) is useless. + if(errno == EACCES) + { + // Get current owner of the device + struct stat st = { 0 }; + if(stat(config.ntp.rtc.device.v.s, &st) == -1) + { + log_debug(DEBUG_NTP, "stat(\"%s\") failed: %s", + config.ntp.rtc.device.v.s, strerror(errno)); + return -1; + } + + if(chown(config.ntp.rtc.device.v.s, uid, gid) == -1) + { + log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s", + config.ntp.rtc.device.v.s, uid, gid, strerror(errno)); + return -1; + } + + rtc_fd = open(config.ntp.rtc.device.v.s, O_RDONLY); + if (rtc_fd != -1) + { + log_debug(DEBUG_NTP, "Successfully opened RTC at \"%s\"", + config.ntp.rtc.device.v.s); + } + + // Chown the device back to the original owner + if(chown(config.ntp.rtc.device.v.s, st.st_uid, st.st_gid) == -1) + { + log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s", + config.ntp.rtc.device.v.s, st.st_uid, st.st_gid, strerror(errno)); + return -1; + } + + // Return the RTC file descriptor (can be -1) + return rtc_fd; + } + + log_debug(DEBUG_NTP, "Failed to open RTC at \"%s\": %s", + config.ntp.rtc.device.v.s, strerror(errno)); + + return -1; + } + + // If the user has not specified an RTC device, try to open the default + // ones + for(size_t i = 0; i < ArraySize(rtc_devices); i++) + { + rtc_fd = open(rtc_devices[i], O_RDONLY); + if (rtc_fd != -1) + { + log_debug(DEBUG_NTP, "Successfully opened RTC at \"%s\"", + rtc_devices[i]); + break; + } + + // If the open failed because of permissions, try to change them + // momentarily + if(errno == EACCES) + { + // Get current owner of the device + struct stat st = { 0 }; + if(stat(rtc_devices[i], &st) == -1) + { + log_debug(DEBUG_NTP, "stat(\"%s\") failed: %s", + rtc_devices[i], strerror(errno)); + return -1; + } + + if(chown(rtc_devices[i], uid, gid) == -1) + { + log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s", + rtc_devices[i], uid, gid, strerror(errno)); + return -1; + } + + rtc_fd = open(rtc_devices[i], O_RDONLY); + if (rtc_fd != -1) + { + log_debug(DEBUG_NTP, "Successfully opened RTC at \"%s\"", + rtc_devices[i]); + } + + // Chown the device back to the original owner + if(chown(rtc_devices[i], st.st_uid, st.st_gid) == -1) + { + log_debug(DEBUG_NTP, "chown(\"%s\", %u, %u) failed: %s", + rtc_devices[i], st.st_uid, st.st_gid, strerror(errno)); + return -1; + } + + // Return the RTC file descriptor (can be -1) + return rtc_fd; + } + + log_debug(DEBUG_NTP, "Failed to open RTC at \"%s\": %s", + rtc_devices[i], strerror(errno)); + } + + return rtc_fd; +} + +static bool read_rtc(struct tm *tm) +{ + // Open the RTC device + const int rtc_fd = open_rtc(); + if(rtc_fd == -1) + return false; + + // Read the RTC time + struct rtc_time rtc_tm = { 0 }; + const int rc = ioctl(rtc_fd, RTC_RD_TIME, &rtc_tm); + if(rc == -1) + { + log_debug(DEBUG_NTP, "ioctl(RTC_RD_NAME) failed: %s", + strerror(errno)); + close(rtc_fd); + return false; + } + + // Convert the kernel's struct tm to the standard struct tm + tm->tm_sec = rtc_tm.tm_sec; + tm->tm_min = rtc_tm.tm_min; + tm->tm_hour = rtc_tm.tm_hour; + tm->tm_mday = rtc_tm.tm_mday; + tm->tm_mon = rtc_tm.tm_mon; + tm->tm_year = rtc_tm.tm_year; + tm->tm_wday = rtc_tm.tm_wday; + tm->tm_yday = rtc_tm.tm_yday; + tm->tm_isdst = -1; // the RTC does not provide this information + print_tm_time("Current RTC time is", tm); + + // Close the RTC device + close(rtc_fd); + + return true; +} + +// Set the Hardware Clock to the broken down time . +// Use ioctls to "rtc" device to set the time. +static bool set_rtc(const struct tm *new_time) +{ + // Open the RTC device + const int rtc_fd = open_rtc(); + if(rtc_fd == -1) + return false; + + // Set the RTC time from the broken down time + struct rtc_time rtc_tm = { 0 }; + rtc_tm.tm_sec = new_time->tm_sec; + rtc_tm.tm_min = new_time->tm_min; + rtc_tm.tm_hour = new_time->tm_hour; + rtc_tm.tm_mday = new_time->tm_mday; + rtc_tm.tm_mon = new_time->tm_mon; + rtc_tm.tm_year = new_time->tm_year; + rtc_tm.tm_wday = new_time->tm_wday; + rtc_tm.tm_yday = new_time->tm_yday; + rtc_tm.tm_isdst = new_time->tm_isdst; + + // Set the RTC time + const int rc = ioctl(rtc_fd, RTC_SET_TIME, &rtc_tm); + if(rc == -1) + { + log_debug(DEBUG_NTP, "ioctl(RTC_SET_TIME) failed: %s", + strerror(errno)); + close(rtc_fd); + return false; + } + print_tm_time("RTC time set to", new_time); + + // Close the RTC device + close(rtc_fd); + return true; +} + +bool ntp_sync_rtc(void) +{ + // Wait until the beginning of the next second as the RTC only has a + // resolution of one second + struct timespec ts = { 0 }; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec++; + ts.tv_nsec = 0; + clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &ts, NULL); + + // Time to which we will set Hardware Clock, in broken down format + struct tm new_time = { 0 }; + const time_t newtime = time(NULL); + if(config.ntp.rtc.utc.v.b) + // UTC + gmtime_r(&newtime, &new_time); + else + // Local time + localtime_r(&newtime, &new_time); + + // Read the current time from the RTC + struct tm rtc_time = { 0 }; + if(!read_rtc(&rtc_time)) + { + log_debug(DEBUG_NTP, "Failed to read RTC time"); + return false; + } + + // If the RTC time is the same as the current time, we don't need to set + // it. We don't use memcmp() here because the tm struct may contain + // additional fields that are not filled in by the RTC (e.g. tm_isdst). + if(rtc_time.tm_sec == new_time.tm_sec && + rtc_time.tm_min == new_time.tm_min && + rtc_time.tm_hour == new_time.tm_hour && + rtc_time.tm_mday == new_time.tm_mday && + rtc_time.tm_mon == new_time.tm_mon && + rtc_time.tm_year == new_time.tm_year) + { + // The RTC time is already correct, return early + log_debug(DEBUG_NTP, "RTC time is already correct"); + return true; + } + + // Set the RTC time + if(!set_rtc(&new_time)) + { + log_debug(DEBUG_NTP, "Failed to set RTC time"); + return false; + } + + return true; +} diff --git a/src/ntp/server.c b/src/ntp/server.c new file mode 100644 index 00000000..652c9525 --- /dev/null +++ b/src/ntp/server.c @@ -0,0 +1,405 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2024 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* NTP server routines +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#include "ntp/ntp.h" +// exit(0) +#include +// memcpy() +#include +// close() +#include +// fork(), wait() +#include +// clock_gettime() +#include +//#include +#include +// wait() +#include +// htonl(), etc. +#include +// errno +#include +// ctime() +#include +// pthread_create +#include +// PR_SET_NAME +#include +// config struct +#include "config/config.h" +// PRIi64 +#include +// log_ntp_message() +#include "database/message-table.h" + +uint64_t ntp_last_sync = 0u; +uint32_t ntp_root_delay = 0u; +uint32_t ntp_root_dispersion = 0u; + +// RFC 5905 Appendix A.4: Kernel System Clock Interface +uint64_t gettime64(void) +{ + struct timeval unix_time; + gettimeofday(&unix_time, NULL); + return (U2LFP(unix_time)); +} + +// Create and send an NTP reply to the client +static bool ntp_reply(const int socket_fd, const struct sockaddr *saddr_p, const socklen_t saddrlen, + const unsigned char recv_buf[], const uint64_t *recv_time) +{ + // Buffer for the response + unsigned char send_buf[48]; + memset(send_buf, 0, sizeof(send_buf)); + + // DWORD-aligned pointer to the send buffer + uint32_t *u32p = (uint32_t*)((void*)&send_buf[0]); + // DWORD-aligned read-only pointer to the receive buffer + const uint32_t *u32r = (uint32_t*)((void*)&recv_buf[0]); + +// NTP Packet Header Format (RFC 5905), page 18 +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |LI | VN |Mode | Stratum | Poll | Precision | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Check if the first byte is valid: mode is expected to be 3 ("client") + if ((recv_buf[0] & 0x07) != 0x3) { + log_warn("Received invalid NTP request: not from an NTP client, ignoring"); + return false; + } + + // set LI = 0 (no warning about leap seconds), set version-number to + // 4 and set mode = 4 ("server") + send_buf[0] = (0x04 << 3) + 0x04; + + // Set stratum to "secondary server" as we have derived time via + // external NTP as well. May be set to 1 if we want to be a primary + // server (synchronized by a hardware clock with GPS, etc.) + send_buf[1] = 0x02; + + // Copy Poll value from client + send_buf[2] = recv_buf[2]; + + // Precision: the precision of the local clock, in seconds to the + // nearest power of two. + // log2(1 usec = 1e-6 s) = -19.931568569324174 + send_buf[3] = (signed char)(-20); + + // Advance 32 bit pointer to the next field + u32p++; + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Root Delay | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Root Dispersion | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Set Root Delay (total roundtrip delay to the primary reference + // source) and Root Dispersion (the nominal error relative to the + // primary reference source) to the values obtained from the upstream + // NTP server. + *u32p++ = htonl(ntp_root_delay); + *u32p++ = htonl(ntp_root_dispersion); + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Reference ID | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Reference ID = 'LOCL" (LOCAL CLOCK) + // A four-octet, left-justified, zero-padded ASCII string assigned to + // the reference clock + memcpy(u32p++, "LOCL", sizeof(uint32_t)); + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// + Reference Timestamp (64) + +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Time when the system clock was last set or corrected, in NTP + // timestamp format. + const uint64_t last_sync = hton64(ntp_last_sync); + memcpy(u32p, &last_sync, sizeof(uint64_t)); + if(config.debug.ntp.v.b) + print_debug_time("Reference Timestamp", u32p, 0); + u32p += 2; + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// + Origin Timestamp (64) + +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Time at the client when the request departed for the server, in NTP + // timestamp format. (this is the client's transmit time) + memcpy(u32p, &u32r[10], sizeof(uint64_t)); + if(config.debug.ntp.v.b) + print_debug_time("Origin Timestamp", u32p, 0); + u32p += 2; + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// + Receive Timestamp (64) + +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Time at the server when the request arrived from the client, in NTP + // timestamp format. (this is the server's receive time) + const uint64_t net_recv_time = hton64(*recv_time); + memcpy(u32p, &net_recv_time, sizeof(uint64_t)); + if(config.debug.ntp.v.b) + print_debug_time("Receive Timestamp", u32p, 0); + u32p += 2; + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// + Transmit Timestamp (64) + +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Time at the server when the response left for the client, in NTP + // timestamp format. (this is the server's transmit time) + const uint64_t transmit_time = gettime64(); + const uint64_t net_transmit_time = hton64(transmit_time); + memcpy(u32p, &net_transmit_time, sizeof(uint64_t)); + if(config.debug.ntp.v.b) + print_debug_time("Transmit Timestamp", u32p, 0); + u32p += 2; + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// . . +// . Extension Field 1 (variable) . +// . . +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// . . +// . Extension Field 2 (variable) . +// . . +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Key Identifier | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// | dgst (128) | +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// Figure 8: Packet Header Format + + // Send the response + errno = 0; + if(sendto(socket_fd, send_buf, sizeof(send_buf), 0, saddr_p, saddrlen) < 48) + { + log_err("NTP send error: %s", strerror(errno)); + return false; + } + + return true; +} + +// Process incoming NTP requests +static void request_process_loop(int fd, const char *ipstr, const int protocol) +{ + log_info("NTP server listening on %s:123 (%s)", ipstr, protocol == AF_INET ? "IPv4" : "IPv6"); + while (true) + { + unsigned char buf[48]; + struct sockaddr src_addr; + socklen_t src_addrlen = sizeof(src_addr); + while(recvfrom(fd, buf, sizeof(buf), 0, &src_addr, &src_addrlen) < 48); // ignore invalid requests + + // Get the current time in NTP format directly after receiving + // the request + const uint64_t recv_time = gettime64(); + + // Print the request + if(config.debug.ntp.v.b) + { + if(protocol == AF_INET6) + { + struct sockaddr_in6 sin6; + memcpy(&sin6, &src_addr, sizeof(sin6)); + + char ip[INET6_ADDRSTRLEN]; + const in_port_t port = ntohs(sin6.sin6_port); + inet_ntop(protocol, &sin6.sin6_addr, ip, sizeof(ip)); + log_debug(DEBUG_NTP, "Received NTP request from [%s]:%u", ip, port); + } + else + { + struct sockaddr_in sin; + memcpy(&sin, &src_addr, sizeof(sin)); + + char ip[INET6_ADDRSTRLEN]; + const in_port_t port = ntohs(sin.sin_port); + inet_ntop(protocol, &sin.sin_addr, ip, sizeof(ip)); + log_debug(DEBUG_NTP, "Received NTP request from %s:%u", ip, port); + } + } + + // Fork a child to handle the request + const pid_t pid = fork(); + if (pid == 0) { + // Child + ntp_reply(fd, &src_addr , src_addrlen, buf, &recv_time); + exit(0); + } else if (pid == -1) { + log_err("fork() error"); + return; + } + // return to parent + } +} + +// Start the NTP server +static void *ntp_bind_and_listen(void *param) +{ + const int protocol = param == 0 ? AF_INET : AF_INET6; + + // Create a socket + errno = 0; + const int s = socket(protocol, SOCK_DGRAM, IPPROTO_UDP); + if(s == -1) + { + char errbuf[1024]; + snprintf(errbuf, sizeof(errbuf), + "Cannot create NTP socket (%s), IPv%i NTP server not available", + strerror(errno), protocol == AF_INET ? 4 : 6); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, true, errbuf); + return NULL; + } + + // Bind the socket to the NTP port + char ipstr[INET6_ADDRSTRLEN + 1]; + memset(ipstr, 0, sizeof(ipstr)); + if(protocol == AF_INET) + { + // IPv4 - set thread name + prctl(PR_SET_NAME, "NTP (IPv4)", 0, 0, 0); + + // Prepare the bind address + struct sockaddr_in bind_addr; + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; // IPv4 + bind_addr.sin_port = htons(123); // NTP port + memcpy(&bind_addr.sin_addr, &config.ntp.ipv4.address.v.in_addr, sizeof(bind_addr.sin_addr)); + inet_ntop(AF_INET, &bind_addr.sin_addr, ipstr, sizeof(ipstr) - 1); + + // Bind the socket + errno = 0; + if(bind(s, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) != 0) + { + char errbuf[1024]; + snprintf(errbuf, sizeof(errbuf), + "Cannot bind to IPv4 address %s:123 (%s), IPv4 NTP server not available", + ipstr, strerror(errno)); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, true, errbuf); + return NULL; + } + } + else + { + // IPv6 - set thread name + prctl(PR_SET_NAME, "NTP (IPv6)", 0, 0, 0); + + // Set socket options to allow IPv6 only, otherwise it will bind + // to both IPv4 and IPv6 and show IPv4 addresses as + // v4-mapped-on-v6 addresses + int opt = 1; + if(setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)) != 0) + { + char errbuf[1024]; + strncpy(errbuf, "Cannot set socket option IPV6_V6ONLY, IPv6 NTP server not available: ", sizeof(errbuf)); + strncat(errbuf, strerror(errno), sizeof(errbuf) - strlen(errbuf) - 1); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, true, errbuf); + return NULL; + } + + // Prepare the bind address + struct sockaddr_in6 bind_addr; + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin6_family = AF_INET6; // IPv6 + bind_addr.sin6_port = htons(123); // NTP port + memcpy(&bind_addr.sin6_addr, &config.ntp.ipv6.address.v.in6_addr, sizeof(bind_addr.sin6_addr)); + inet_ntop(AF_INET6, &bind_addr.sin6_addr, ipstr, sizeof(ipstr) - 1); + + // Bind the socket + errno = 0; + if(bind(s, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) != 0) + { + char errbuf[1024]; + snprintf(errbuf, sizeof(errbuf), + "Cannot bind to IPv6 address %s:123 (%s), IPv6 NTP server not available", + ipstr, strerror(errno)); + errbuf[sizeof(errbuf) - 1] = '\0'; + log_ntp_message(true, true, errbuf); + return NULL; + } + } + + request_process_loop(s, ipstr, protocol); + close(s); + + return NULL; +} + +// Start the NTP server +bool ntp_server_start(pthread_attr_t *attr) +{ + // Spawn two pthreads, one for IPv4 and one for IPv6 + + // IPv4 + if(config.ntp.ipv4.active.v.b) + { + // Create a thread for the IPv4 NTP server + pthread_t thread; + if (pthread_create(&thread, attr, ntp_bind_and_listen, (void *)0) != 0) + { + log_ntp_message(true, true, "Cannot create NTP server thread for IPv4"); + return false; + } + } + + // IPv6 + if(config.ntp.ipv6.active.v.b) + { + // Create a thread for the IPv6 NTP server + pthread_t thread; + if (pthread_create(&thread, attr, ntp_bind_and_listen, (void *)1) != 0) + { + log_ntp_message(true, true, "Cannot create NTP server thread for IPv6"); + return false; + } + } + + return true; +} diff --git a/src/resolve.c b/src/resolve.c index f77cadfb..453e477e 100644 --- a/src/resolve.c +++ b/src/resolve.c @@ -1053,7 +1053,6 @@ static void resolveUpstreams(const bool onlynew) void *DNSclient_thread(void *val) { // Set thread name - thread_names[DNSclient] = "DNS client"; thread_running[DNSclient] = true; prctl(PR_SET_NAME, thread_names[DNSclient], 0, 0, 0); diff --git a/src/signals.c b/src/signals.c index 9c638445..8e14cfe2 100644 --- a/src/signals.c +++ b/src/signals.c @@ -35,7 +35,13 @@ volatile int exit_code = EXIT_SUCCESS; volatile sig_atomic_t thread_cancellable[THREADS_MAX] = { false }; volatile sig_atomic_t thread_running[THREADS_MAX] = { false }; -const char *thread_names[THREADS_MAX] = { "" }; +const char * const thread_names[THREADS_MAX] = { + "database", + "housekeeper", + "dns-client", + "timer", + "ntp-client" + }; // Return the (null-terminated) name of the calling thread // The name is stored in the buffer as well as returned for convenience diff --git a/src/signals.h b/src/signals.h index 4a08e4b9..76664887 100644 --- a/src/signals.h +++ b/src/signals.h @@ -30,6 +30,8 @@ extern volatile sig_atomic_t want_to_reload_lists; extern volatile sig_atomic_t thread_cancellable[THREADS_MAX]; extern volatile sig_atomic_t thread_running[THREADS_MAX]; -extern const char *thread_names[THREADS_MAX]; +extern const char * const thread_names[THREADS_MAX]; + +#define BREAK_IF_KILLED() { if(killed) break; } #endif //SIGNALS_H diff --git a/src/timers.c b/src/timers.c index fe66d760..17b16704 100644 --- a/src/timers.c +++ b/src/timers.c @@ -84,7 +84,8 @@ void get_blockingmode_timer(double *delay, bool *target_status) void *timer(void *val) { // Set thread name - prctl(PR_SET_NAME, "int.timer", 0, 0, 0); + thread_running[GC] = true; + prctl(PR_SET_NAME, thread_names[TIMER], 0, 0, 0); // Save timestamp as we do not want to store immediately // to the database @@ -105,9 +106,11 @@ void *timer(void *val) set_blockingstatus(timer_target_status); timer_delay = -1.0; } - sleepms(SLEEPING_TIME * 1000); + thread_sleepms(TIMER, SLEEPING_TIME * 1000); } + log_info("Terminating timer thread"); + thread_running[GC] = false; return NULL; } diff --git a/src/tools/arp-scan.c b/src/tools/arp-scan.c index 923625a3..c5ac6ec3 100644 --- a/src/tools/arp-scan.c +++ b/src/tools/arp-scan.c @@ -616,6 +616,8 @@ int run_arp_scan(const bool scan_all, const bool extreme_mode) pthread_attr_t attr; // Initialize thread attributes object with default attribute values pthread_attr_init(&attr); + // Set thread attributes to detached mode + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); struct ifaddrs *addrs, *tmp; getifaddrs(&addrs); diff --git a/src/tools/dhcp-discover.c b/src/tools/dhcp-discover.c index c68a74f9..045c8c14 100644 --- a/src/tools/dhcp-discover.c +++ b/src/tools/dhcp-discover.c @@ -725,6 +725,8 @@ int run_dhcp_discover(void) pthread_attr_t attr; // Initialize thread attributes object with default attribute values pthread_attr_init(&attr); + // Set thread attributes to detached mode + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // Create processing/printfing lock pthread_mutexattr_t lock_attr; diff --git a/test/pihole.toml b/test/pihole.toml index d4d2fdf0..f9d0ff62 100644 --- a/test/pihole.toml +++ b/test/pihole.toml @@ -460,6 +460,54 @@ # "[][,id:|*][,set:][,tag:][,][,][,][,ignore]" hosts = [] + [ntp.ipv4] + # Should FTL act as network time protocol (NTP) server (IPv4)? + active = true + + # IPv4 address to listen on for NTP requests + # + # Possible values are: + # or empty string ("") for wildcard (0.0.0.0) + address = "" + + [ntp.ipv6] + # Should FTL act as network time protocol (NTP) server (IPv6)? + active = true + + # IPv6 address to listen on for NTP requests + # + # Possible values are: + # or empty string ("") for wildcard (::) + address = "" + + [ntp.sync] + # NTP upstream server to sync with, e.g., "pool.ntp.org". Note that the NTP server + # should be located as close as possible to you in order to minimize the time offset + # possibly introduced by different routing paths. + # + # Possible values are: + # valid NTP upstream server + server = "pool.ntp.org" + + # Interval in seconds between successive synchronization attempts with the NTP server + interval = 3600 + + # Number of NTP syncs to perform and average before updating the system time + count = 8 + + [ntp.rtc] + # Should FTL update a real-time clock (RTC) if available? + set = true + + # Path to the RTC device to update. Leave empty for auto-discovery + # + # Possible values are: + # Path to the RTC device, e.g., "/dev/rtc0" + device = "" + + # Should the RTC be set to UTC? + utc = true + [resolver] # Should FTL try to resolve IPv4 addresses to hostnames? resolveIPv4 = false ### CHANGED, default = true @@ -1039,14 +1087,17 @@ # Reserved debug flag reserved = true ### CHANGED, default = false + # Print information about NTP synchronization + ntp = true ### CHANGED, default = false + # Set all debug flags at once. This is a convenience option to enable all debug flags # at once. Note that this option is not persistent, setting it to true will enable all # *remaining* debug flags but unsetting it will disable *all* debug flags. all = true ### CHANGED, default = false # Configuration statistics: -# 137 total entries out of which 83 entries are default -# --> 54 entries are modified +# 148 total entries out of which 93 entries are default +# --> 55 entries are modified # 2 entries are forced through environment: # - misc.nice # - debug.api diff --git a/test/test_suite.bats b/test/test_suite.bats index e020846c..c850a709 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -9,7 +9,7 @@ } @test "Running a second instance is detected and prevented" { - run bash -c 'su pihole -s /bin/sh -c "/home/pihole/pihole-FTL -f"' + run bash -c 'su pihole -s /bin/sh -c "./pihole-FTL -f"' printf "%s\n" "${lines[@]}" [[ "${lines[@]}" == *"CRIT: Initialization of shared memory failed."* ]] [[ "${lines[@]}" == *"INFO: pihole-FTL is already running"* ]] @@ -22,12 +22,6 @@ [[ ${lines[1]} == "" ]] } -@test "Starting tests without prior history" { - run bash -c 'grep -c "Total DNS queries: 0" /var/log/pihole/FTL.log' - printf "%s\n" "${lines[@]}" - [[ ${lines[0]} == "1" ]] -} - @test "Initial blocking status is enabled" { run bash -c 'grep -c "Blocking status is enabled" /var/log/pihole/FTL.log' printf "%s\n" "${lines[@]}" @@ -37,10 +31,10 @@ @test "Number of compiled regex filters as expected" { run bash -c 'grep "Compiled [0-9]* allow" /var/log/pihole/FTL.log' printf "%s\n" "${lines[@]}" - [[ ${lines[0]} == *"Compiled 2 allow and 11 deny regex for 1 client in "* ]] + [[ ${lines[0]} == *"Compiled 2 allow and 11 deny regex"* ]] } -@test "denied domain is blocked" { +@test "Denied domain is blocked" { run bash -c "dig denied.ftl @127.0.0.1 +short" printf "%s\n" "${lines[@]}" [[ ${lines[0]} == "0.0.0.0" ]] @@ -473,21 +467,21 @@ } @test "Test fail on invalid CLI argument" { - run bash -c '/home/pihole/pihole-FTL abc' + run bash -c './pihole-FTL abc' printf "%s\n" "${lines[@]}" [[ ${lines[0]} == "pihole-FTL: invalid option -- 'abc'" ]] - [[ ${lines[1]} == "Command: '/home/pihole/pihole-FTL abc'" ]] - [[ ${lines[2]} == "Try '/home/pihole/pihole-FTL --help' for more information" ]] + [[ ${lines[1]} == "Command: './pihole-FTL abc'" ]] + [[ ${lines[2]} == "Try './pihole-FTL --help' for more information" ]] } @test "Help CLI argument return help text" { - run bash -c '/home/pihole/pihole-FTL help' + run bash -c './pihole-FTL help' printf "%s\n" "${lines[@]}" [[ ${lines[0]} == "The Pi-hole FTL engine - "* ]] } @test "No WARNING messages in FTL.log (besides known warnings)" { - run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|(Cannot set process priority)|FTLCONF_"' + run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|CAP_SYS_TIME|(Cannot set process priority)|FTLCONF_"' printf "%s\n" "${lines[@]}" [[ "${lines[@]}" == "" ]] } @@ -1171,7 +1165,7 @@ @test "No ERROR messages in FTL.log (besides known/intended error)" { run bash -c 'grep "ERROR: " /var/log/pihole/FTL.log' printf "%s\n" "${lines[@]}" - run bash -c 'grep "ERROR: " /var/log/pihole/FTL.log | grep -c -v -E "(index\.html)|(Failed to create shared memory object)|(FTLCONF_debug_api is invalid)"' + run bash -c 'grep "ERROR: " /var/log/pihole/FTL.log | grep -c -v -E "(index\.html)|(Failed to create shared memory object)|(FTLCONF_debug_api is invalid)|(Failed to set|adjust time during NTP sync: Insufficient permissions)"' printf "count: %s\n" "${lines[@]}" [[ ${lines[0]} == "0" ]] } @@ -1359,6 +1353,14 @@ [[ ${lines[0]} == '{"error":{"key":"bad_request","message":"Config items set via environment variables cannot be changed via the API","hint":"misc.nice"},"took":'*'}' ]] } +@test "Check NTP server is broadcasting correct time" { + run bash -c './pihole-FTL ntp 127.0.0.1 --dry-run' + printf "%s\n" "${lines[@]}" + [[ $status == 0 ]] +} + +# We cannot easily test IPv6 as it may not be available in docker (CI) + @test "API domain search: Non-existing domain returns expected JSON" { run bash -c 'curl -s 127.0.0.1/api/search/non.existent' printf "%s\n" "${lines[@]}"