Skip to content

Commit c1dfc31

Browse files
committed
statd: add last-change to interface operational status
The ietf-interfaces leaf last-change, the time an interface entered its current operational state, was missing. Nothing on the system keeps it: the kernel has no ifLastChange and statd only reads state on demand. Track it in statd. A netlink socket in the RTMGRP_LINK group feeds an in-memory map keyed by interface name, stamping every operational state change with the monotonic clock. The interfaces callback adds the leaf to yanger's output for each interface with a recorded change, converting to wall-clock time at that point so a later NTP step does not skew old stamps. Interfaces whose state predates statd get no last-change, as RFC 8343 requires. Part of issue #514. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1 parent b8a4291 commit c1dfc31

9 files changed

Lines changed: 372 additions & 6 deletions

File tree

‎doc/ChangeLog.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ All notable changes to the project are documented in this file.
3939
- Add `higher-layer-if` and `lower-layer-if` to interface operational
4040
status, listing the interfaces stacked directly on top of or beneath
4141
each one, e.g., a VLAN interface and its parent, issue #514
42+
- Add `last-change` to interface operational status: the time the
43+
interface entered its current operational state, issue #514
4244

4345
### Fixes
4446

‎src/statd/Makefile.am‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ DISTCLEANFILES = *~ *.d
22
ACLOCAL_AMFLAGS = -I m4
33

44
sbin_PROGRAMS = statd
5-
statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h avahi.c avahi.h
5+
statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h avahi.c avahi.h iface.c iface.h
66
statd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
77
statd_CPPFLAGS += -DSTATD_VERSION=\"$(PACKAGE_VERSION)\"
88
statd_CFLAGS = -W -Wall -Wextra

‎src/statd/iface.c‎

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/* SPDX-License-Identifier: BSD-3-Clause */
2+
/*
3+
* Track interface state changes over rtnetlink to provide the
4+
* ietf-interfaces last-change leaf, see issue #514.
5+
*/
6+
7+
#include <errno.h>
8+
#include <stdio.h>
9+
#include <stdlib.h>
10+
#include <string.h>
11+
#include <time.h>
12+
#include <unistd.h>
13+
#include <sys/socket.h>
14+
#include <linux/netlink.h>
15+
#include <linux/rtnetlink.h>
16+
17+
#include <srx/common.h>
18+
#include <srx/lyx.h>
19+
20+
#include "iface.h"
21+
#include "shared.h"
22+
23+
static time_t monotonic(void)
24+
{
25+
struct timespec ts;
26+
27+
clock_gettime(CLOCK_MONOTONIC, &ts);
28+
return ts.tv_sec;
29+
}
30+
31+
static struct iface *iface_find(struct iface_ctx *ctx, const char *name)
32+
{
33+
struct iface *l;
34+
35+
TAILQ_FOREACH(l, &ctx->ifaces, entries) {
36+
if (!strcmp(l->name, name))
37+
return l;
38+
}
39+
40+
return NULL;
41+
}
42+
43+
static struct iface *iface_find_index(struct iface_ctx *ctx, int ifindex)
44+
{
45+
struct iface *l;
46+
47+
TAILQ_FOREACH(l, &ctx->ifaces, entries) {
48+
if (l->ifindex == ifindex)
49+
return l;
50+
}
51+
52+
return NULL;
53+
}
54+
55+
static void iface_del(struct iface_ctx *ctx, struct iface *l)
56+
{
57+
TAILQ_REMOVE(&ctx->ifaces, l, entries);
58+
free(l);
59+
}
60+
61+
static void iface_update(struct iface_ctx *ctx, int ifindex, const char *name,
62+
uint8_t operstate, int dump)
63+
{
64+
struct iface *l;
65+
66+
l = iface_find(ctx, name);
67+
if (!l) {
68+
l = iface_find_index(ctx, ifindex);
69+
if (l) {
70+
DEBUG("Link %s renamed %s", l->name, name);
71+
snprintf(l->name, sizeof(l->name), "%s", name);
72+
}
73+
}
74+
75+
if (!l) {
76+
l = calloc(1, sizeof(*l));
77+
if (!l) {
78+
ERRNO("Failed allocating link %s", name);
79+
return;
80+
}
81+
82+
snprintf(l->name, sizeof(l->name), "%s", name);
83+
l->operstate = operstate;
84+
if (!dump)
85+
l->changed = monotonic();
86+
TAILQ_INSERT_TAIL(&ctx->ifaces, l, entries);
87+
DEBUG("Link %s added, operstate %u", name, operstate);
88+
} else if (l->operstate != operstate) {
89+
DEBUG("Link %s operstate %u -> %u", name, l->operstate, operstate);
90+
l->operstate = operstate;
91+
l->changed = monotonic();
92+
}
93+
94+
l->ifindex = ifindex;
95+
}
96+
97+
static void iface_parse(struct iface_ctx *ctx, struct nlmsghdr *nlh, int dump)
98+
{
99+
struct ifinfomsg *ifi = NLMSG_DATA(nlh);
100+
int len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*ifi));
101+
uint8_t operstate = IF_OPER_UNKNOWN;
102+
const char *name = NULL;
103+
struct rtattr *rta;
104+
struct iface *l;
105+
106+
for (rta = IFLA_RTA(ifi); RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) {
107+
switch (rta->rta_type & NLA_TYPE_MASK) {
108+
case IFLA_IFNAME:
109+
name = RTA_DATA(rta);
110+
break;
111+
case IFLA_OPERSTATE:
112+
operstate = *(uint8_t *)RTA_DATA(rta);
113+
break;
114+
}
115+
}
116+
117+
if (!name)
118+
return;
119+
120+
if (nlh->nlmsg_type == RTM_DELLINK) {
121+
l = iface_find(ctx, name);
122+
if (l) {
123+
DEBUG("Link %s removed", name);
124+
iface_del(ctx, l);
125+
}
126+
return;
127+
}
128+
129+
iface_update(ctx, ifi->ifi_index, name, operstate, dump);
130+
}
131+
132+
static void iface_purge(struct iface_ctx *ctx)
133+
{
134+
struct iface *l;
135+
136+
while ((l = TAILQ_FIRST(&ctx->ifaces)))
137+
iface_del(ctx, l);
138+
}
139+
140+
/* Request a full link dump, replies are told apart from events by sequence */
141+
static int iface_dump(struct iface_ctx *ctx)
142+
{
143+
struct {
144+
struct nlmsghdr nlh;
145+
struct ifinfomsg ifi;
146+
} req = {
147+
.nlh = {
148+
.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
149+
.nlmsg_type = RTM_GETLINK,
150+
.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
151+
},
152+
.ifi = {
153+
.ifi_family = AF_UNSPEC,
154+
},
155+
};
156+
157+
if (!++ctx->seq)
158+
ctx->seq = 1;
159+
req.nlh.nlmsg_seq = ctx->seq;
160+
161+
if (send(ctx->sd, &req, req.nlh.nlmsg_len, 0) < 0) {
162+
ERRNO("Failed requesting link dump");
163+
ctx->seq = 0;
164+
return -1;
165+
}
166+
167+
return 0;
168+
}
169+
170+
static void iface_io_cb(struct ev_loop *, ev_io *w, int)
171+
{
172+
struct iface_ctx *ctx = (struct iface_ctx *)w;
173+
static char buf[32768];
174+
struct nlmsghdr *nlh;
175+
int len;
176+
177+
for (;;) {
178+
len = recv(ctx->sd, buf, sizeof(buf), MSG_DONTWAIT);
179+
if (len < 0) {
180+
if (errno == EINTR)
181+
continue;
182+
if (errno == EAGAIN)
183+
break;
184+
if (errno == ENOBUFS) {
185+
WARN("Link event overrun, resyncing");
186+
iface_purge(ctx);
187+
iface_dump(ctx);
188+
continue;
189+
}
190+
191+
ERRNO("Failed reading link events");
192+
break;
193+
}
194+
195+
for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
196+
int dump = ctx->seq && nlh->nlmsg_seq == ctx->seq;
197+
198+
switch (nlh->nlmsg_type) {
199+
case RTM_NEWLINK:
200+
case RTM_DELLINK:
201+
iface_parse(ctx, nlh, dump);
202+
break;
203+
case NLMSG_DONE:
204+
if (dump)
205+
ctx->seq = 0;
206+
break;
207+
case NLMSG_ERROR:
208+
if (dump) {
209+
struct nlmsgerr *e = NLMSG_DATA(nlh);
210+
211+
errno = -e->error;
212+
ERRNO("Link dump failed");
213+
ctx->seq = 0;
214+
}
215+
break;
216+
default:
217+
break;
218+
}
219+
}
220+
}
221+
}
222+
223+
int iface_ctx_init(struct iface_ctx *ctx, struct ev_loop *loop)
224+
{
225+
struct sockaddr_nl sa = {
226+
.nl_family = AF_NETLINK,
227+
.nl_groups = RTMGRP_LINK,
228+
};
229+
230+
TAILQ_INIT(&ctx->ifaces);
231+
ctx->loop = loop;
232+
233+
ctx->sd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE);
234+
if (ctx->sd < 0) {
235+
ERRNO("Failed opening link event socket");
236+
return -1;
237+
}
238+
239+
if (bind(ctx->sd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
240+
ERRNO("Failed subscribing to link events");
241+
close(ctx->sd);
242+
ctx->sd = -1;
243+
return -1;
244+
}
245+
246+
ev_io_init(&ctx->io, iface_io_cb, ctx->sd, EV_READ);
247+
ev_io_start(loop, &ctx->io);
248+
249+
return iface_dump(ctx);
250+
}
251+
252+
void iface_ctx_exit(struct iface_ctx *ctx)
253+
{
254+
if (ctx->sd >= 0) {
255+
ev_io_stop(ctx->loop, &ctx->io);
256+
close(ctx->sd);
257+
ctx->sd = -1;
258+
}
259+
260+
iface_purge(ctx);
261+
}
262+
263+
/* Add last-change to every interface in tree whose state changed since start */
264+
void iface_annotate(struct iface_ctx *ctx, struct lyd_node *tree)
265+
{
266+
time_t now = time(NULL), mono = monotonic();
267+
struct lyd_node *iface;
268+
269+
if (!tree)
270+
return;
271+
272+
LY_LIST_FOR(lyd_child(tree), iface) {
273+
struct iface *l;
274+
char buf[32];
275+
276+
l = iface_find(ctx, lydx_get_cattr(iface, "name") ?: "");
277+
if (!l || !l->changed)
278+
continue;
279+
280+
format_timestamp(now - (mono - l->changed), buf, sizeof(buf));
281+
if (lyd_new_term(iface, NULL, "last-change", buf, 0, NULL))
282+
WARN("Failed adding last-change to interface %s", l->name);
283+
}
284+
}

‎src/statd/iface.h‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* SPDX-License-Identifier: BSD-3-Clause */
2+
3+
#ifndef STATD_IFACE_H_
4+
#define STATD_IFACE_H_
5+
6+
#include <stdint.h>
7+
#include <time.h>
8+
#include <sys/queue.h>
9+
#include <linux/if.h>
10+
11+
#include <ev.h>
12+
#include <libyang/libyang.h>
13+
14+
/*
15+
* In-memory link state, keyed by interface name, tracked over rtnetlink.
16+
* Renames are recognized by ifindex, which the kernel keeps across them.
17+
*/
18+
19+
struct iface {
20+
char name[IFNAMSIZ];
21+
int ifindex;
22+
uint8_t operstate; /* IF_OPER_* */
23+
time_t changed; /* CLOCK_MONOTONIC, 0: state predates statd */
24+
TAILQ_ENTRY(iface) entries;
25+
};
26+
27+
struct iface_ctx {
28+
ev_io io; /* MUST be first (cast from ev_io *) */
29+
struct ev_loop *loop;
30+
int sd;
31+
uint32_t seq; /* sequence of ongoing dump, 0: none */
32+
TAILQ_HEAD(, iface) ifaces;
33+
};
34+
35+
int iface_ctx_init(struct iface_ctx *ctx, struct ev_loop *loop);
36+
void iface_ctx_exit(struct iface_ctx *ctx);
37+
38+
void iface_annotate(struct iface_ctx *ctx, struct lyd_node *tree);
39+
40+
#endif

‎src/statd/python/cli_pretty/cli_pretty.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,7 @@ def __init__(self, data):
10441044
self.type = data.get('type', '')
10451045
self.index = data.get('if-index', '')
10461046
self.oper_status = data.get('oper-status', '')
1047+
self.last_change = data.get('last-change', '')
10471048
self.higher_layer = data.get('higher-layer-if', [])
10481049
self.lower_layer = data.get('lower-layer-if', [])
10491050
self.autoneg = get_json_data('unknown', self.data, 'ieee802-ethernet-interface:ethernet',
@@ -1668,6 +1669,8 @@ def pr_iface(self):
16681669
print(f"{'mtu':<{19}}: {self.mtu}")
16691670
if self.oper():
16701671
print(f"{'operational status':<{19}}: {self.oper(detail=True)}")
1672+
if self.last_change:
1673+
print(f"{'last change':<{19}}: {Date.from_yang(self.last_change).pretty()}")
16711674

16721675
forwarding = "enabled" if self.name in Iface._routing_ifaces else "disabled"
16731676
print(f"{'ip forwarding':<{19}}: {forwarding}")

0 commit comments

Comments
 (0)