Skip to content

Commit 6735cbc

Browse files
committed
confd: initial support for NTP server
Fixes #904 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1 parent 39ffad6 commit 6735cbc

File tree

18 files changed

+3644
-4
lines changed

18 files changed

+3644
-4
lines changed

src/confd/src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ confd_plugin_la_SOURCES = \
4040
if-wifi.c \
4141
keystore.c \
4242
system.c \
43+
ntp.c \
4344
syslog.c \
4445
factory-default.c \
4546
routing.c \

src/confd/src/core.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
206206
if ((rc = keystore_change(session, config, diff, event, confd)))
207207
goto free_diff;
208208

209+
/* ietf-ntp */
210+
if ((rc = ntp_change(session, config, diff, event, confd)))
211+
goto free_diff;
212+
209213
/* infix-services */
210214
if ((rc = services_change(session, config, diff, event, confd)))
211215
goto free_diff;
@@ -244,6 +248,20 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
244248
if ((rc = meta_change_cb(session, config, diff, event, confd)))
245249
goto free_diff;
246250

251+
/*
252+
* Manage chronyd service enable/disable state. Must be done
253+
* after both ietf-system:ntp and ietf-ntp have are done.
254+
*/
255+
if (event == SR_EV_DONE && config) {
256+
bool client = false;
257+
bool server = false;
258+
259+
client = srx_enabled(session, "/ietf-system:system/ntp/enabled");
260+
server = lydx_get_xpathf(config, "/ietf-ntp:ntp") != NULL;
261+
262+
systemf("initctl -nbq %s chronyd", client || server ? "enable" : "disable");
263+
}
264+
247265
if (cfg)
248266
sr_release_data(cfg);
249267

@@ -332,6 +350,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
332350
ERROR("Failed to subscribe to ietf-netconf-acm");
333351
goto err;
334352
}
353+
rc = subscribe_model("ietf-ntp", &confd, 0);
354+
if (rc) {
355+
ERROR("Failed to subscribe to ietf-ntp");
356+
goto err;
357+
}
335358
rc = subscribe_model("infix-dhcp-client", &confd, 0);
336359
if (rc) {
337360
ERROR("Failed to subscribe to infix-dhcp-client");
@@ -432,6 +455,10 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
432455
goto err;
433456

434457
rc = dhcp_server_candidate_init(&confd);
458+
if (rc)
459+
goto err;
460+
461+
rc = ntp_candidate_init(&confd);
435462
if (rc)
436463
goto err;
437464
/* YOUR_INIT GOES HERE */

src/confd/src/core.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,10 @@ int firewall_rpc_init(struct confd *confd);
247247
int firewall_candidate_init(struct confd *confd);
248248
int firewall_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
249249

250+
/* ntp.c */
251+
int ntp_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
252+
int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
253+
const char *path, sr_event_t event, unsigned request_id, void *priv);
254+
int ntp_candidate_init(struct confd *confd);
255+
250256
#endif /* CONFD_CORE_H_ */

src/confd/src/ntp.c

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
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

Comments
 (0)