Skip to content

Commit

Permalink
Add RTC synchronization
Browse files Browse the repository at this point in the history
Signed-off-by: DL6ER <dl6er@dl6er.de>
  • Loading branch information
DL6ER committed Jun 8, 2024
1 parent fb0b06e commit f3de0c2
Show file tree
Hide file tree
Showing 8 changed files with 355 additions and 1 deletion.
13 changes: 13 additions & 0 deletions src/api/docs/content/specs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,15 @@ components:
type: integer
count:
type: integer
rtc:
type: object
properties:
set:
type: boolean
device:
type: string
utc:
type: boolean
resolver:
type: object
properties:
Expand Down Expand Up @@ -700,6 +709,10 @@ components:
server: "pool.ntp.org"
interval: 3600
count: 8
rtc:
set: true
device: ""
utc: true
resolver:
resolveIPv4: true
resolveIPv6: true
Expand Down
19 changes: 19 additions & 0 deletions src/config/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,25 @@ void initConfig(struct config *conf)
conf->ntp.sync.count.d.ui = 8;
conf->ntp.sync.count.c = validate_stub; // Only type-based checking

conf->ntp.rtc.set.k = "ntp.rtc.set";
conf->ntp.rtc.set.h = "Should FTL update a real-time clock (RTC) if available?";
conf->ntp.rtc.set.t = CONF_BOOL;
conf->ntp.rtc.set.d.b = true;
conf->ntp.rtc.set.c = validate_stub; // Only type-based checking

conf->ntp.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";
Expand Down
5 changes: 5 additions & 0 deletions src/config/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ struct config {
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 {
Expand Down
1 change: 1 addition & 0 deletions src/ntp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
set(ntp_sources
server.c
client.c
rtc.c
ntp.h
)

Expand Down
4 changes: 4 additions & 0 deletions src/ntp/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ static bool settime_step(const double offset)
return false;
}

// Adjust RTC if available
if(config.ntp.rtc.set.v.b)
ntp_sync_rtc();

return true;
}

Expand Down
3 changes: 3 additions & 0 deletions src/ntp/ntp.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ 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
Expand Down
296 changes: 296 additions & 0 deletions src/ntp/rtc.c
Original file line number Diff line number Diff line change
@@ -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 <sys/ioctl.h>
// RTC
#include <linux/rtc.h>
// O_WRONLY
#include <fcntl.h>
// 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, "open(\"%s\", O_RDONLY) succeeded",
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, "open(\"%s\", O_RDONLY) succeeded",
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
return rtc_fd;
}
}

log_debug(DEBUG_NTP, "open(\"%s\", O_RDONLY) failed: %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, "open(\"%s\", O_RDONLY) succeeded",
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, "open(\"%s\", O_RDONLY) succeeded",
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
return rtc_fd;
}
}

log_debug(DEBUG_NTP, "open(\"%s\", O_RDONLY) failed: %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("RTC time is", tm);

// Close the RTC device
close(rtc_fd);

return true;
}

// Set the Hardware Clock to the broken down time <new_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;
}
Loading

0 comments on commit f3de0c2

Please sign in to comment.