|
| 1 | +/* SPDX-License-Identifier: BSD-3-Clause */ |
| 2 | + |
| 3 | +#include <assert.h> |
| 4 | +#include <jansson.h> |
| 5 | + |
| 6 | +#include <srx/common.h> |
| 7 | +#include <srx/lyx.h> |
| 8 | +#include <srx/srx_val.h> |
| 9 | + |
| 10 | +#include "core.h" |
| 11 | + |
| 12 | +#define XPATH_NTP_ "/ietf-ntp:ntp" |
| 13 | +#define NTP_CONF "/etc/chrony/conf.d/ntp-server.conf" |
| 14 | +#define NTP_PREV NTP_CONF "-" |
| 15 | +#define NTP_NEXT NTP_CONF "+" |
| 16 | + |
| 17 | +/* |
| 18 | + * NTP Server configuration handler |
| 19 | + * |
| 20 | + * This implements ietf-ntp (RFC 9249) for basic NTP server functionality |
| 21 | + * using chronyd as the underlying daemon. The implementation is minimal, |
| 22 | + * focusing on: |
| 23 | + * |
| 24 | + * - Local reference clock (stratum configuration) |
| 25 | + * - Custom port (if not default 123) |
| 26 | + * - Interface binding (restrict to specific interfaces) |
| 27 | + * - Server mode enabling (allow clients to query) |
| 28 | + * |
| 29 | + * Mutual exclusion with ietf-system:ntp is enforced by YANG when statement, |
| 30 | + * so no runtime validation is needed here. |
| 31 | + */ |
| 32 | +static int change_ntp_server(sr_session_ctx_t *session, |
| 33 | + struct lyd_node *config, |
| 34 | + struct lyd_node *diff, |
| 35 | + sr_event_t event, |
| 36 | + struct confd *confd) |
| 37 | +{ |
| 38 | + int port = 0, stratum = 0; |
| 39 | + sr_val_t *val; |
| 40 | + size_t cnt; |
| 41 | + FILE *fp; |
| 42 | + int rc; |
| 43 | + |
| 44 | + if (diff && !lydx_get_xpathf(diff, XPATH_NTP_)) |
| 45 | + return SR_ERR_OK; |
| 46 | + |
| 47 | + switch (event) { |
| 48 | + case SR_EV_ENABLED: /* first time, on register. */ |
| 49 | + case SR_EV_CHANGE: /* regular change (copy cand running) */ |
| 50 | + /* Generate next config */ |
| 51 | + break; |
| 52 | + |
| 53 | + case SR_EV_ABORT: /* User abort, or other plugin failed */ |
| 54 | + (void)remove(NTP_NEXT); |
| 55 | + return SR_ERR_OK; |
| 56 | + |
| 57 | + case SR_EV_DONE: |
| 58 | + /* Check if NTP container exists (presence container) */ |
| 59 | + if (!lydx_get_xpathf(config, XPATH_NTP_)) { |
| 60 | + DEBUG("NTP server disabled, removing config"); |
| 61 | + systemf("rm -f %s", NTP_CONF); |
| 62 | + |
| 63 | + return SR_ERR_OK; |
| 64 | + } |
| 65 | + |
| 66 | + /* Check if passed validation in previous event */ |
| 67 | + if (!fexist(NTP_NEXT)) |
| 68 | + return SR_ERR_OK; |
| 69 | + |
| 70 | + (void)remove(NTP_PREV); |
| 71 | + (void)rename(NTP_CONF, NTP_PREV); |
| 72 | + (void)rename(NTP_NEXT, NTP_CONF); |
| 73 | + |
| 74 | + /* Reload chronyd to pick up new config */ |
| 75 | + if (systemf("chronyc reload sources >/dev/null 2>&1")) |
| 76 | + ERRNO("Failed reloading chronyd sources"); |
| 77 | + |
| 78 | + systemf("initctl -nbq touch chronyd"); |
| 79 | + return SR_ERR_OK; |
| 80 | + |
| 81 | + default: |
| 82 | + return SR_ERR_OK; |
| 83 | + } |
| 84 | + |
| 85 | + fp = fopen(NTP_NEXT, "w"); |
| 86 | + if (!fp) { |
| 87 | + ERROR("Failed creating %s: %s", NTP_NEXT, strerror(errno)); |
| 88 | + return SR_ERR_SYS; |
| 89 | + } |
| 90 | + |
| 91 | + fprintf(fp, "# Generated by ietf-ntp\n"); |
| 92 | + fprintf(fp, "# This file configures chronyd as an NTP server\n\n"); |
| 93 | + |
| 94 | + /* Port configuration (optional) */ |
| 95 | + SRX_GET_UINT32(session, port, XPATH_NTP_"/port"); |
| 96 | + if (port && port != 123) { |
| 97 | + fprintf(fp, "# Custom NTP port\n"); |
| 98 | + fprintf(fp, "port %d\n\n", port); |
| 99 | + NOTE("NTP server configured on port %d", port); |
| 100 | + } |
| 101 | + |
| 102 | + /* makestep configuration - allow clock stepping for fast initial sync */ |
| 103 | + if (lydx_get_xpathf(config, XPATH_NTP_"/infix-ntp:makestep")) { |
| 104 | + char *threshold_str = srx_get_str(session, XPATH_NTP_"/infix-ntp:makestep/threshold"); |
| 105 | + char *limit_str = srx_get_str(session, XPATH_NTP_"/infix-ntp:makestep/limit"); |
| 106 | + double threshold = 1.0; /* Default */ |
| 107 | + int limit = 3; /* Default */ |
| 108 | + |
| 109 | + if (threshold_str) { |
| 110 | + threshold = atof(threshold_str); |
| 111 | + free(threshold_str); |
| 112 | + } |
| 113 | + if (limit_str) { |
| 114 | + limit = atoi(limit_str); |
| 115 | + free(limit_str); |
| 116 | + } |
| 117 | + |
| 118 | + fprintf(fp, "# Allow clock stepping for fast initial sync\n"); |
| 119 | + fprintf(fp, "makestep %.1f %d\n\n", threshold, limit); |
| 120 | + NOTE("NTP makestep configured: threshold %.1f seconds, limit %d updates", |
| 121 | + threshold, limit); |
| 122 | + } |
| 123 | + |
| 124 | + /* Upstream server/peer configuration (unicast-configuration) */ |
| 125 | + rc = sr_get_items(session, XPATH_NTP_"/unicast-configuration", 0, 0, &val, &cnt); |
| 126 | + if (rc == SR_ERR_OK && cnt > 0) { |
| 127 | + fprintf(fp, "# Upstream NTP servers and peers\n"); |
| 128 | + |
| 129 | + for (size_t i = 0; i < cnt; i++) { |
| 130 | + char *address = srx_get_str(session, "%s/address", val[i].xpath); |
| 131 | + char *type = srx_get_str(session, "%s/type", val[i].xpath); |
| 132 | + const char *directive = NULL; |
| 133 | + |
| 134 | + if (address) { |
| 135 | + char *minpoll = srx_get_str(session, "%s/minpoll", val[i].xpath); |
| 136 | + char *maxpoll = srx_get_str(session, "%s/maxpoll", val[i].xpath); |
| 137 | + char *version = srx_get_str(session, "%s/version", val[i].xpath); |
| 138 | + char *srcport = srx_get_str(session, "%s/port", val[i].xpath); |
| 139 | + bool prefer = srx_enabled(session, "%s/prefer", val[i].xpath); |
| 140 | + bool burst = srx_enabled(session, "%s/burst", val[i].xpath); |
| 141 | + bool iburst = srx_enabled(session, "%s/iburst", val[i].xpath); |
| 142 | + |
| 143 | + if (type && strstr(type, "uc-server")) |
| 144 | + directive = "server"; |
| 145 | + else if (type && strstr(type, "uc-peer")) |
| 146 | + directive = "peer"; |
| 147 | + |
| 148 | + if (directive) { |
| 149 | + fprintf(fp, "%s %s", directive, address); |
| 150 | + if (srcport) |
| 151 | + fprintf(fp, " port %s", srcport); |
| 152 | + if (iburst) |
| 153 | + fprintf(fp, " iburst"); |
| 154 | + if (burst) |
| 155 | + fprintf(fp, " burst"); |
| 156 | + if (prefer) |
| 157 | + fprintf(fp, " prefer"); |
| 158 | + if (minpoll) |
| 159 | + fprintf(fp, " minpoll %s", minpoll); |
| 160 | + if (maxpoll) |
| 161 | + fprintf(fp, " maxpoll %s", maxpoll); |
| 162 | + if (version) |
| 163 | + fprintf(fp, " version %s", version); |
| 164 | + fprintf(fp, "\n"); |
| 165 | + |
| 166 | + DEBUG("NTP %s: %s port %s%s%s%s minpoll %s maxpoll %s version %s", directive, address, |
| 167 | + srcport ?: "123", |
| 168 | + iburst ? " iburst" : "", |
| 169 | + burst ? " burst " : "", |
| 170 | + prefer ? " prefer" : "", |
| 171 | + minpoll ?: "6", |
| 172 | + maxpoll ?: "10", |
| 173 | + version ?: "4"); |
| 174 | + } |
| 175 | + |
| 176 | + if (minpoll) |
| 177 | + free(minpoll); |
| 178 | + if (maxpoll) |
| 179 | + free(maxpoll); |
| 180 | + if (version) |
| 181 | + free(version); |
| 182 | + if (srcport) |
| 183 | + free(srcport); |
| 184 | + free(address); |
| 185 | + } |
| 186 | + if (type) |
| 187 | + free(type); |
| 188 | + } |
| 189 | + fprintf(fp, "\n"); |
| 190 | + sr_free_values(val, cnt); |
| 191 | + } |
| 192 | + |
| 193 | + /* Reference clock (local stratum) - fallback time source */ |
| 194 | + if (lydx_get_xpathf(config, XPATH_NTP_"/refclock-master")) { |
| 195 | + SRX_GET_UINT8(session, stratum, XPATH_NTP_"/refclock-master/master-stratum"); |
| 196 | + if (!stratum) |
| 197 | + stratum = 10; /* Default from RFC 9249 */ |
| 198 | + |
| 199 | + fprintf(fp, "# Local reference clock - fallback stratum %d source\n", stratum); |
| 200 | + fprintf(fp, "local stratum %d orphan\n\n", stratum); |
| 201 | + NOTE("NTP server configured with stratum %d fallback", stratum); |
| 202 | + } else { |
| 203 | + /* No refclock-master means server mode without local clock */ |
| 204 | + /* This is valid - device can still serve time from upstream servers */ |
| 205 | + DEBUG("NTP server without local reference clock"); |
| 206 | + } |
| 207 | + |
| 208 | + /* |
| 209 | + * Enable NTP server mode - allow clients to query us |
| 210 | + * |
| 211 | + * Using 'allow' without arguments permits all clients. |
| 212 | + * In a future version with access-rules support, we could |
| 213 | + * restrict to specific subnets. |
| 214 | + */ |
| 215 | + fprintf(fp, "# Allow NTP clients to query this server\n"); |
| 216 | + fprintf(fp, "allow\n\n"); |
| 217 | + |
| 218 | + /* |
| 219 | + * Enable RTC synchronization |
| 220 | + * |
| 221 | + * On Linux, the kernel will copy system time to the hardware RTC |
| 222 | + * every 11 minutes when the clock is synchronized. This keeps the |
| 223 | + * RTC accurate for the next boot, which is important for embedded |
| 224 | + * systems without continuous network connectivity. |
| 225 | + */ |
| 226 | + fprintf(fp, "# Synchronize system time to hardware RTC\n"); |
| 227 | + fprintf(fp, "rtcsync\n"); |
| 228 | + |
| 229 | + fclose(fp); |
| 230 | + return SR_ERR_OK; |
| 231 | +} |
| 232 | + |
| 233 | +/* |
| 234 | + * Inference callback for NTP server configuration |
| 235 | + * |
| 236 | + * When a user creates the /ietf-ntp:ntp presence container without |
| 237 | + * any configuration, we infer sensible defaults: |
| 238 | + * |
| 239 | + * - refclock-master with stratum 10 (local reference clock fallback) |
| 240 | + * - makestep with threshold 1.0 and limit 3 (fast initial sync for embedded) |
| 241 | + * |
| 242 | + * This matches the DHCP client inference pattern where an empty |
| 243 | + * container gets reasonable defaults. |
| 244 | + */ |
| 245 | +static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, |
| 246 | + const char *path, sr_event_t event, unsigned request_id, void *priv) |
| 247 | +{ |
| 248 | + sr_val_t inferred_uint8 = { .type = SR_UINT8_T }; |
| 249 | + sr_val_t inferred_dec64 = { .type = SR_DECIMAL64_T }; |
| 250 | + sr_val_t inferred_int32 = { .type = SR_INT32_T }; |
| 251 | + size_t cnt = 0; |
| 252 | + |
| 253 | + if (event != SR_EV_UPDATE && event != SR_EV_CHANGE) |
| 254 | + return SR_ERR_OK; |
| 255 | + |
| 256 | + /* Check if NTP container exists */ |
| 257 | + if (srx_nitems(session, &cnt, XPATH_NTP_) || !cnt) |
| 258 | + return SR_ERR_OK; |
| 259 | + |
| 260 | + /* Check if refclock-master already configured */ |
| 261 | + if (!srx_nitems(session, &cnt, XPATH_NTP_"/refclock-master") && cnt > 0) |
| 262 | + return SR_ERR_OK; |
| 263 | + |
| 264 | + /* Check if unicast-configuration already configured */ |
| 265 | + if (!srx_nitems(session, &cnt, XPATH_NTP_"/unicast-configuration") && cnt > 0) |
| 266 | + return SR_ERR_OK; |
| 267 | + |
| 268 | + /* Infer refclock-master with stratum 10 (safe fallback) */ |
| 269 | + DEBUG("Inferring NTP refclock-master with stratum 10"); |
| 270 | + inferred_uint8.data.uint8_val = 10; |
| 271 | + srx_set_item(session, &inferred_uint8, 0, XPATH_NTP_"/refclock-master/master-stratum"); |
| 272 | + |
| 273 | + /* Infer makestep for fast initial sync (critical for embedded systems) */ |
| 274 | + if (!srx_nitems(session, &cnt, XPATH_NTP_"/infix-ntp:makestep") || cnt == 0) { |
| 275 | + DEBUG("Inferring NTP makestep with threshold 1.0, limit 3"); |
| 276 | + |
| 277 | + /* Create presence container by setting threshold */ |
| 278 | + inferred_dec64.data.decimal64_val = 10; /* 1.0 with fraction-digits 1 */ |
| 279 | + srx_set_item(session, &inferred_dec64, 0, XPATH_NTP_"/infix-ntp:makestep/threshold"); |
| 280 | + |
| 281 | + /* Set limit */ |
| 282 | + inferred_int32.data.int32_val = 3; |
| 283 | + srx_set_item(session, &inferred_int32, 0, XPATH_NTP_"/infix-ntp:makestep/limit"); |
| 284 | + } |
| 285 | + |
| 286 | + return SR_ERR_OK; |
| 287 | +} |
| 288 | + |
| 289 | +int ntp_change(sr_session_ctx_t *session, struct lyd_node *config, |
| 290 | + struct lyd_node *diff, sr_event_t event, struct confd *confd) |
| 291 | +{ |
| 292 | + return change_ntp_server(session, config, diff, event, confd); |
| 293 | +} |
| 294 | + |
| 295 | +int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, |
| 296 | + const char *path, sr_event_t event, unsigned request_id, void *priv) |
| 297 | +{ |
| 298 | + return cand(session, sub_id, module, path, event, request_id, priv); |
| 299 | +} |
| 300 | + |
| 301 | +int ntp_candidate_init(struct confd *confd) |
| 302 | +{ |
| 303 | + int rc; |
| 304 | + |
| 305 | + REGISTER_CHANGE(confd->cand, "ietf-ntp", XPATH_NTP_ "//.", SR_SUBSCR_UPDATE, ntp_cand, confd, &confd->sub); |
| 306 | + |
| 307 | + return SR_ERR_OK; |
| 308 | +fail: |
| 309 | + ERROR("init failed: %s", sr_strerror(rc)); |
| 310 | + return rc; |
| 311 | +} |
0 commit comments