mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
statd: add operational support for mDNS neighbors
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -10,7 +10,7 @@ STATD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/statd
|
||||
STATD_LICENSE = BSD-3-Clause
|
||||
STATD_LICENSE_FILES = LICENSE
|
||||
STATD_REDISTRIBUTE = NO
|
||||
STATD_DEPENDENCIES = sysrepo libev libsrx jansson libyang libite \
|
||||
STATD_DEPENDENCIES = sysrepo libev libsrx jansson libyang libite avahi \
|
||||
host-python3 python3 host-python-pypa-build host-python-installer \
|
||||
host-python-poetry-core dbus-python
|
||||
STATD_AUTORECONF = YES
|
||||
|
||||
@@ -513,6 +513,31 @@ def lldp(args: List[str]):
|
||||
cli_pretty(data, "show-lldp")
|
||||
|
||||
|
||||
def mdns(args: List[str]) -> None:
|
||||
# Fetch config from running DS (enabled, domain, hostname, reflector)
|
||||
cfg = get_json("/infix-services:mdns", "running")
|
||||
# Fetch live state from operational DS (neighbors pushed by avahi)
|
||||
oper = get_json("/infix-services:mdns")
|
||||
|
||||
data: dict = {}
|
||||
if cfg:
|
||||
data.update(cfg)
|
||||
if oper:
|
||||
oper_mdns = oper.get("infix-services:mdns", {})
|
||||
data_mdns = data.setdefault("infix-services:mdns", {})
|
||||
data_mdns.update(oper_mdns)
|
||||
|
||||
if not data:
|
||||
print("No mDNS data available.")
|
||||
return
|
||||
|
||||
if RAW_OUTPUT:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
cli_pretty(data, "show-mdns")
|
||||
|
||||
|
||||
def system(args: List[str]) -> None:
|
||||
# Get system state from sysrepo
|
||||
data = get_json("/ietf-system:system-state")
|
||||
@@ -696,6 +721,7 @@ def execute_command(command: str, args: List[str]):
|
||||
'interface': interface,
|
||||
'keystore': keystore,
|
||||
'lldp': lldp,
|
||||
'mdns': mdns,
|
||||
'nacm': nacm,
|
||||
'ntp': ntp,
|
||||
'ospf': ospf,
|
||||
|
||||
@@ -43,7 +43,7 @@ MODULES=(
|
||||
"infix-firewall-icmp-types@2025-04-26.yang"
|
||||
"infix-meta@2025-12-10.yang"
|
||||
"infix-system@2026-03-09.yang"
|
||||
"infix-services@2026-03-03.yang"
|
||||
"infix-services@2026-03-04.yang"
|
||||
"ieee802-ethernet-interface@2019-06-21.yang"
|
||||
"infix-ethernet-interface@2024-02-27.yang"
|
||||
"infix-factory-default@2023-06-28.yang"
|
||||
|
||||
@@ -16,6 +16,9 @@ module infix-services {
|
||||
reference
|
||||
"RFC 9640: YANG Data Types and Groupings for Cryptography";
|
||||
}
|
||||
import ietf-yang-types {
|
||||
prefix yang;
|
||||
}
|
||||
import infix-system {
|
||||
prefix infix-sys;
|
||||
}
|
||||
@@ -28,9 +31,9 @@ module infix-services {
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "Infix services, generic.";
|
||||
|
||||
revision 2026-03-03 {
|
||||
description "Add hostname leaf to mdns container for avahi host-name override.";
|
||||
reference "internal";
|
||||
revision 2026-03-04 {
|
||||
description "Add hostname leaf to mdns container for avahi host-name override.
|
||||
Add neighbors container to mdns for mDNS-SD neighbor table.";
|
||||
}
|
||||
revision 2025-12-10 {
|
||||
description "Adapt to changes in final version of ietf-keystore";
|
||||
@@ -146,6 +149,62 @@ module infix-services {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
|
||||
container neighbors {
|
||||
config false;
|
||||
description "mDNS neighbor table populated by statd on avahi D-Bus events.
|
||||
All DNS-SD hosts are included; tools that need to identify Infix
|
||||
devices check for 'vv=1' in the txt leaf-list themselves.
|
||||
Addresses and TXT records are stored in raw form from avahi.";
|
||||
|
||||
list neighbor {
|
||||
key "hostname";
|
||||
description "A DNS-SD host discovered via avahi service browsing.";
|
||||
|
||||
leaf hostname {
|
||||
type string;
|
||||
description "mDNS hostname as reported by avahi, e.g. 'infix-2.local'.";
|
||||
}
|
||||
|
||||
leaf-list address {
|
||||
type inet:ip-address;
|
||||
description "All addresses reported by avahi for this host.
|
||||
Loopback (127.x/::1) addresses are excluded.";
|
||||
}
|
||||
|
||||
leaf last-seen {
|
||||
type yang:date-and-time;
|
||||
description "Timestamp of the most recent avahi resolver event for this host.";
|
||||
}
|
||||
|
||||
list service {
|
||||
key "name";
|
||||
description "DNS-SD services advertised by this neighbor.";
|
||||
|
||||
leaf name {
|
||||
type string;
|
||||
description "Service instance name as announced, e.g. 'infix-2 Web'.";
|
||||
}
|
||||
|
||||
leaf type {
|
||||
type string;
|
||||
description "Raw mDNS service type, e.g. '_https._tcp', '_ssh._tcp'.";
|
||||
}
|
||||
|
||||
leaf port {
|
||||
type inet:port-number;
|
||||
description "TCP or UDP port number for this service.";
|
||||
}
|
||||
|
||||
leaf-list txt {
|
||||
type string;
|
||||
description "Raw DNS-SD TXT record strings for this service instance,
|
||||
e.g. 'vv=1', 'product=Infix', 'ov=25.01.0'.
|
||||
Stored verbatim; callers split on '=' to extract key-value pairs.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
container ssh {
|
||||
description "Configuration for the SSH daemon";
|
||||
|
||||
@@ -529,6 +529,10 @@ echo "Public: $pub"
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="mdns" help="Show mDNS configuration and neighbors">
|
||||
<ACTION sym="script">show mdns</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="hardware" help="Show hardware information">
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
show hardware |pager
|
||||
|
||||
@@ -2,13 +2,15 @@ DISTCLEANFILES = *~ *.d
|
||||
ACLOCAL_AMFLAGS = -I m4
|
||||
|
||||
sbin_PROGRAMS = statd
|
||||
statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h
|
||||
statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h avahi.c avahi.h
|
||||
statd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
|
||||
statd_CFLAGS = -W -Wall -Wextra
|
||||
statd_CFLAGS += $(jansson_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS)
|
||||
statd_CFLAGS += $(libsrx_CFLAGS) $(libite_CFLAGS)
|
||||
statd_CFLAGS += $(avahi_client_CFLAGS)
|
||||
statd_LDADD = $(jansson_LIBS) $(libyang_LIBS) $(sysrepo_LIBS)
|
||||
statd_LDADD += $(libsrx_LIBS) $(libite_LIBS) $(EV_LIBS) -lz
|
||||
statd_LDADD += $(avahi_client_LIBS)
|
||||
|
||||
# Test stub for journal retention policy (no dependencies, standalone)
|
||||
noinst_PROGRAMS = journal_retention_stub
|
||||
|
||||
@@ -0,0 +1,817 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
/*
|
||||
* avahi.c - mDNS neighbor table for statd using libavahi-client + libev.
|
||||
*
|
||||
* Discovery flow:
|
||||
* 1. AvahiServiceTypeBrowser finds all service types on the local network.
|
||||
* 2. For each type, an AvahiServiceBrowser enumerates service instances.
|
||||
* 3. For each instance, a transient AvahiServiceResolver resolves hostname,
|
||||
* address, port and TXT records.
|
||||
* 4. Resolved data is pushed to SR_DS_OPERATIONAL under
|
||||
* /infix-services:mdns/neighbors via sr_set_item_str() + sr_apply_changes().
|
||||
* 5. On BROWSER_REMOVE, the corresponding DS subtree is deleted.
|
||||
*
|
||||
* The AvahiPoll vtable bridges avahi's event model to the main libev loop
|
||||
* (same thread — no locking required).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/queue.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <ev.h>
|
||||
#include <avahi-client/client.h>
|
||||
#include <avahi-client/lookup.h>
|
||||
#include <avahi-common/address.h>
|
||||
#include <avahi-common/error.h>
|
||||
#include <avahi-common/watch.h>
|
||||
#include <sysrepo.h>
|
||||
|
||||
#include <srx/common.h>
|
||||
|
||||
#include "avahi.h"
|
||||
|
||||
/* Complete the opaque avahi types declared in avahi-common/watch.h */
|
||||
struct AvahiWatch {
|
||||
ev_io io; /* MUST be first (cast from ev_io *) */
|
||||
AvahiWatchEvent last_event;
|
||||
AvahiWatchCallback callback;
|
||||
void *userdata;
|
||||
struct avahi_ctx *ctx;
|
||||
};
|
||||
|
||||
struct AvahiTimeout {
|
||||
ev_timer timer; /* MUST be first */
|
||||
AvahiTimeoutCallback callback;
|
||||
void *userdata;
|
||||
struct avahi_ctx *ctx;
|
||||
};
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* libev-backed AvahiPoll vtable
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
static void watch_io_cb(struct ev_loop *loop, ev_io *w, int events)
|
||||
{
|
||||
struct AvahiWatch *watch = (struct AvahiWatch *)w;
|
||||
AvahiWatchEvent av = 0;
|
||||
|
||||
(void)loop;
|
||||
if (events & EV_READ) av |= AVAHI_WATCH_IN;
|
||||
if (events & EV_WRITE) av |= AVAHI_WATCH_OUT;
|
||||
if (events & EV_ERROR) av |= AVAHI_WATCH_ERR;
|
||||
watch->last_event = av;
|
||||
watch->callback(watch, w->fd, av, watch->userdata);
|
||||
}
|
||||
|
||||
static AvahiWatch *watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent event,
|
||||
AvahiWatchCallback callback, void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = api->userdata;
|
||||
struct AvahiWatch *w;
|
||||
int ev_events = 0;
|
||||
|
||||
w = calloc(1, sizeof(*w));
|
||||
if (!w)
|
||||
return NULL;
|
||||
|
||||
w->callback = callback;
|
||||
w->userdata = userdata;
|
||||
w->ctx = ctx;
|
||||
|
||||
if (event & AVAHI_WATCH_IN) ev_events |= EV_READ;
|
||||
if (event & AVAHI_WATCH_OUT) ev_events |= EV_WRITE;
|
||||
|
||||
ev_io_init(&w->io, watch_io_cb, fd, ev_events);
|
||||
if (ev_events)
|
||||
ev_io_start(ctx->loop, &w->io);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
static void watch_update(AvahiWatch *w, AvahiWatchEvent event)
|
||||
{
|
||||
int ev_events = 0;
|
||||
|
||||
ev_io_stop(w->ctx->loop, &w->io);
|
||||
if (event & AVAHI_WATCH_IN) ev_events |= EV_READ;
|
||||
if (event & AVAHI_WATCH_OUT) ev_events |= EV_WRITE;
|
||||
ev_io_set(&w->io, w->io.fd, ev_events);
|
||||
if (ev_events)
|
||||
ev_io_start(w->ctx->loop, &w->io);
|
||||
}
|
||||
|
||||
static AvahiWatchEvent watch_get_events(AvahiWatch *w)
|
||||
{
|
||||
return w->last_event;
|
||||
}
|
||||
|
||||
static void watch_free(AvahiWatch *w)
|
||||
{
|
||||
ev_io_stop(w->ctx->loop, &w->io);
|
||||
free(w);
|
||||
}
|
||||
|
||||
static void timeout_cb(struct ev_loop *loop, ev_timer *t, int events)
|
||||
{
|
||||
struct AvahiTimeout *timeout = (struct AvahiTimeout *)t;
|
||||
|
||||
(void)loop;
|
||||
(void)events;
|
||||
timeout->callback(timeout, timeout->userdata);
|
||||
}
|
||||
|
||||
static AvahiTimeout *timeout_new(const AvahiPoll *api, const struct timeval *tv,
|
||||
AvahiTimeoutCallback callback, void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = api->userdata;
|
||||
struct AvahiTimeout *t;
|
||||
|
||||
t = calloc(1, sizeof(*t));
|
||||
if (!t)
|
||||
return NULL;
|
||||
|
||||
t->callback = callback;
|
||||
t->userdata = userdata;
|
||||
t->ctx = ctx;
|
||||
|
||||
ev_timer_init(&t->timer, timeout_cb, 0.0, 0.0);
|
||||
|
||||
if (tv) {
|
||||
struct timeval now;
|
||||
double delay;
|
||||
|
||||
gettimeofday(&now, NULL);
|
||||
delay = (double)(tv->tv_sec - now.tv_sec) +
|
||||
(double)(tv->tv_usec - now.tv_usec) / 1e6;
|
||||
if (delay < 0.0)
|
||||
delay = 0.0;
|
||||
ev_timer_set(&t->timer, delay, 0.0);
|
||||
ev_timer_start(ctx->loop, &t->timer);
|
||||
}
|
||||
/* NULL tv means disabled timer — do not start */
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
static void timeout_update(AvahiTimeout *t, const struct timeval *tv)
|
||||
{
|
||||
ev_timer_stop(t->ctx->loop, &t->timer);
|
||||
|
||||
if (tv) {
|
||||
struct timeval now;
|
||||
double delay;
|
||||
|
||||
gettimeofday(&now, NULL);
|
||||
delay = (double)(tv->tv_sec - now.tv_sec) +
|
||||
(double)(tv->tv_usec - now.tv_usec) / 1e6;
|
||||
if (delay < 0.0)
|
||||
delay = 0.0;
|
||||
ev_timer_set(&t->timer, delay, 0.0);
|
||||
ev_timer_start(t->ctx->loop, &t->timer);
|
||||
}
|
||||
}
|
||||
|
||||
static void timeout_free(AvahiTimeout *t)
|
||||
{
|
||||
ev_timer_stop(t->ctx->loop, &t->timer);
|
||||
free(t);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* In-memory state helpers
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
static struct avahi_neighbor *find_neighbor(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
struct avahi_neighbor *n;
|
||||
|
||||
LIST_FOREACH(n, &ctx->neighbors, link) {
|
||||
if (!strcmp(n->hostname, hostname))
|
||||
return n;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct avahi_neighbor *get_neighbor(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
struct avahi_neighbor *n = find_neighbor(ctx, hostname);
|
||||
|
||||
if (n)
|
||||
return n;
|
||||
|
||||
n = calloc(1, sizeof(*n));
|
||||
if (!n)
|
||||
return NULL;
|
||||
|
||||
snprintf(n->hostname, sizeof(n->hostname), "%s", hostname);
|
||||
LIST_INIT(&n->addrs);
|
||||
LIST_INSERT_HEAD(&ctx->neighbors, n, link);
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
static int has_addr(struct avahi_neighbor *n, const char *addr)
|
||||
{
|
||||
struct avahi_addr *a;
|
||||
|
||||
LIST_FOREACH(a, &n->addrs, link) {
|
||||
if (!strcmp(a->val, addr))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void add_addr(struct avahi_neighbor *n, const char *addr)
|
||||
{
|
||||
struct avahi_addr *a = calloc(1, sizeof(*a));
|
||||
|
||||
if (!a)
|
||||
return;
|
||||
snprintf(a->val, sizeof(a->val), "%s", addr);
|
||||
LIST_INSERT_HEAD(&n->addrs, a, link);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find service in flat list by 5-tuple (ifindex, proto, name, type, domain).
|
||||
*/
|
||||
static struct avahi_service *find_service(struct avahi_ctx *ctx,
|
||||
int ifindex, AvahiProtocol proto,
|
||||
const char *name, const char *type,
|
||||
const char *domain)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
|
||||
LIST_FOREACH(s, &ctx->services, link) {
|
||||
if (s->ifindex == ifindex && s->proto == proto &&
|
||||
!strcmp(s->name, name) && !strcmp(s->type, type) &&
|
||||
!strcmp(s->domain, domain))
|
||||
return s;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check whether any service in the flat list matches (hostname, name) —
|
||||
* used after removing one 5-tuple entry to decide if the DS entry should
|
||||
* be removed too (another interface may still have the same service).
|
||||
*/
|
||||
static int svc_ds_entry_exists(struct avahi_ctx *ctx, const char *hostname, const char *name)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
|
||||
LIST_FOREACH(s, &ctx->services, link) {
|
||||
if (!strcmp(s->hostname, hostname) && !strcmp(s->name, name))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int neighbor_has_services(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
|
||||
LIST_FOREACH(s, &ctx->services, link) {
|
||||
if (!strcmp(s->hostname, hostname))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void free_txts(struct avahi_service *svc)
|
||||
{
|
||||
struct avahi_txt *t;
|
||||
|
||||
while (!LIST_EMPTY(&svc->txts)) {
|
||||
t = LIST_FIRST(&svc->txts);
|
||||
LIST_REMOVE(t, link);
|
||||
free(t);
|
||||
}
|
||||
}
|
||||
|
||||
static void free_service(struct avahi_service *svc)
|
||||
{
|
||||
free_txts(svc);
|
||||
LIST_REMOVE(svc, link);
|
||||
free(svc);
|
||||
}
|
||||
|
||||
static void free_neighbor(struct avahi_neighbor *n)
|
||||
{
|
||||
struct avahi_addr *a;
|
||||
|
||||
while (!LIST_EMPTY(&n->addrs)) {
|
||||
a = LIST_FIRST(&n->addrs);
|
||||
LIST_REMOVE(a, link);
|
||||
free(a);
|
||||
}
|
||||
LIST_REMOVE(n, link);
|
||||
free(n);
|
||||
}
|
||||
|
||||
static void free_all(struct avahi_ctx *ctx)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
struct avahi_neighbor *n;
|
||||
|
||||
while (!LIST_EMPTY(&ctx->services)) {
|
||||
s = LIST_FIRST(&ctx->services);
|
||||
free_service(s);
|
||||
}
|
||||
while (!LIST_EMPTY(&ctx->neighbors)) {
|
||||
n = LIST_FIRST(&ctx->neighbors);
|
||||
free_neighbor(n);
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* sysrepo push helpers
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
#define XPATH_BASE "/infix-services:mdns/neighbors"
|
||||
|
||||
static void format_timestamp(char *buf, size_t sz)
|
||||
{
|
||||
struct tm tm;
|
||||
time_t now = time(NULL);
|
||||
|
||||
gmtime_r(&now, &tm);
|
||||
strftime(buf, sz, "%Y-%m-%dT%H:%M:%S+00:00", &tm);
|
||||
}
|
||||
|
||||
static int sr_setstr(sr_session_ctx_t *ses, const char *xpath, const char *val)
|
||||
{
|
||||
int err = sr_set_item_str(ses, xpath, val, NULL, 0);
|
||||
|
||||
if (err)
|
||||
ERROR("avahi: sr_set_item_str(%s): %s", xpath, sr_strerror(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return an XPath string literal quoting val: single-quoted unless val
|
||||
* contains a single quote, in which case double quotes are used instead.
|
||||
* buf must be at least strlen(val)+3 bytes.
|
||||
*/
|
||||
static const char *xpath_str(char *buf, size_t sz, const char *val)
|
||||
{
|
||||
if (strchr(val, '\''))
|
||||
snprintf(buf, sz, "\"%s\"", val);
|
||||
else
|
||||
snprintf(buf, sz, "'%s'", val);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/*
|
||||
* Push a resolver result to the operational DS.
|
||||
* new_addr is non-NULL only when a new address was just added in memory.
|
||||
*/
|
||||
static void ds_push_resolver(struct avahi_ctx *ctx, struct avahi_service *svc,
|
||||
const char *new_addr)
|
||||
{
|
||||
char qname[258]; /* quoted svc->name for safe XPath predicates */
|
||||
char xpath[640];
|
||||
char val[64];
|
||||
struct avahi_txt *t;
|
||||
char ts[32];
|
||||
int err = 0;
|
||||
|
||||
xpath_str(qname, sizeof(qname), svc->name);
|
||||
|
||||
/* Create neighbor list instance (key embedded in predicate; sysrepo 4.x
|
||||
* rejects editing list-key leaves directly — set the list entry instead) */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']", svc->hostname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, NULL);
|
||||
|
||||
/* address (only if a new one was added) */
|
||||
if (new_addr) {
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/address", svc->hostname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, new_addr);
|
||||
}
|
||||
|
||||
/* last-seen */
|
||||
format_timestamp(ts, sizeof(ts));
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/last-seen", svc->hostname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, ts);
|
||||
|
||||
/* Delete and recreate service entry so TXT records are always fresh */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]",
|
||||
svc->hostname, qname);
|
||||
sr_delete_item(ctx->sr_ses, xpath, 0);
|
||||
|
||||
/* Create service list instance (same pattern — key in predicate) */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, NULL);
|
||||
|
||||
/* service/type */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]/type",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, svc->type);
|
||||
|
||||
/* service/port */
|
||||
snprintf(val, sizeof(val), "%u", (unsigned)svc->port);
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]/port",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, val);
|
||||
|
||||
/* service/txt (leaf-list) */
|
||||
LIST_FOREACH(t, &svc->txts, link) {
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]/txt",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, t->val);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
sr_discard_changes(ctx->sr_ses);
|
||||
return;
|
||||
}
|
||||
|
||||
err = sr_apply_changes(ctx->sr_ses, 0);
|
||||
if (err)
|
||||
ERROR("avahi: sr_apply_changes: %s", sr_strerror(err));
|
||||
}
|
||||
|
||||
static void ds_delete_service(struct avahi_ctx *ctx, const char *hostname, const char *name)
|
||||
{
|
||||
char qname[258];
|
||||
char xpath[512];
|
||||
|
||||
xpath_str(qname, sizeof(qname), name);
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]",
|
||||
hostname, qname);
|
||||
sr_delete_item(ctx->sr_ses, xpath, 0);
|
||||
}
|
||||
|
||||
static void ds_delete_neighbor(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
char xpath[512];
|
||||
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']", hostname);
|
||||
sr_delete_item(ctx->sr_ses, xpath, 0);
|
||||
}
|
||||
|
||||
static void ds_clear_all(struct avahi_ctx *ctx)
|
||||
{
|
||||
sr_delete_item(ctx->sr_ses, XPATH_BASE, 0);
|
||||
sr_apply_changes(ctx->sr_ses, 0);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Avahi callbacks
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
static void resolver_cb(AvahiServiceResolver *r,
|
||||
AvahiIfIndex iface, AvahiProtocol proto,
|
||||
AvahiResolverEvent event,
|
||||
const char *name, const char *type, const char *domain,
|
||||
const char *hostname, const AvahiAddress *addr,
|
||||
uint16_t port, AvahiStringList *txtlist,
|
||||
AvahiLookupResultFlags flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
char addrstr[AVAHI_ADDRESS_STR_MAX] = "";
|
||||
struct avahi_neighbor *n;
|
||||
struct avahi_service *svc;
|
||||
const char *new_addr = NULL;
|
||||
AvahiStringList *s;
|
||||
struct avahi_txt *t;
|
||||
int is_loopback;
|
||||
|
||||
(void)flags;
|
||||
|
||||
if (event != AVAHI_RESOLVER_FOUND)
|
||||
goto done;
|
||||
|
||||
if (addr)
|
||||
avahi_address_snprint(addrstr, sizeof(addrstr), addr);
|
||||
|
||||
is_loopback = (!strcmp(addrstr, "127.0.0.1") ||
|
||||
!strcmp(addrstr, "::1") ||
|
||||
!strncmp(addrstr, "127.", 4));
|
||||
|
||||
/* Find or create neighbor (tracks addresses) */
|
||||
n = get_neighbor(ctx, hostname);
|
||||
if (!n) {
|
||||
ERROR("avahi: out of memory for neighbor '%s'", hostname);
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Add address only if new and not loopback */
|
||||
if (!is_loopback && addrstr[0] && !has_addr(n, addrstr)) {
|
||||
add_addr(n, addrstr);
|
||||
new_addr = addrstr;
|
||||
}
|
||||
|
||||
/* Find or create service entry in flat list */
|
||||
svc = find_service(ctx, iface, proto, name, type, domain);
|
||||
if (!svc) {
|
||||
svc = calloc(1, sizeof(*svc));
|
||||
if (!svc) {
|
||||
ERROR("avahi: out of memory for service '%s'", name);
|
||||
goto done;
|
||||
}
|
||||
svc->ifindex = iface;
|
||||
svc->proto = proto;
|
||||
snprintf(svc->name, sizeof(svc->name), "%s", name);
|
||||
snprintf(svc->type, sizeof(svc->type), "%s", type);
|
||||
snprintf(svc->domain, sizeof(svc->domain), "%s", domain);
|
||||
snprintf(svc->hostname, sizeof(svc->hostname), "%s", hostname);
|
||||
LIST_INIT(&svc->txts);
|
||||
LIST_INSERT_HEAD(&ctx->services, svc, link);
|
||||
} else {
|
||||
free_txts(svc);
|
||||
}
|
||||
|
||||
svc->port = port;
|
||||
|
||||
/* Copy TXT records verbatim */
|
||||
for (s = txtlist; s; s = avahi_string_list_get_next(s)) {
|
||||
uint8_t *data = avahi_string_list_get_text(s);
|
||||
size_t len = avahi_string_list_get_size(s);
|
||||
|
||||
t = calloc(1, sizeof(*t));
|
||||
if (!t)
|
||||
break;
|
||||
snprintf(t->val, sizeof(t->val), "%.*s", (int)len, (char *)data);
|
||||
LIST_INSERT_HEAD(&svc->txts, t, link);
|
||||
}
|
||||
|
||||
ds_push_resolver(ctx, svc, new_addr);
|
||||
|
||||
done:
|
||||
avahi_service_resolver_free(r);
|
||||
}
|
||||
|
||||
static void service_browser_cb(AvahiServiceBrowser *b,
|
||||
AvahiIfIndex iface, AvahiProtocol proto,
|
||||
AvahiBrowserEvent event,
|
||||
const char *name, const char *type, const char *domain,
|
||||
AvahiLookupResultFlags flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
|
||||
(void)b;
|
||||
(void)flags;
|
||||
|
||||
switch (event) {
|
||||
case AVAHI_BROWSER_NEW:
|
||||
if (!avahi_service_resolver_new(ctx->client, iface, proto,
|
||||
name, type, domain,
|
||||
AVAHI_PROTO_UNSPEC, 0,
|
||||
resolver_cb, ctx))
|
||||
DEBUG("avahi: resolver_new(%s) failed: %s", name,
|
||||
avahi_strerror(avahi_client_errno(ctx->client)));
|
||||
break;
|
||||
|
||||
case AVAHI_BROWSER_REMOVE: {
|
||||
struct avahi_service *svc;
|
||||
char hostname[256];
|
||||
char svc_name[256];
|
||||
|
||||
svc = find_service(ctx, iface, proto, name, type, domain);
|
||||
if (!svc)
|
||||
break;
|
||||
|
||||
snprintf(hostname, sizeof(hostname), "%s", svc->hostname);
|
||||
snprintf(svc_name, sizeof(svc_name), "%s", svc->name);
|
||||
free_service(svc);
|
||||
|
||||
/* Remove DS service entry if no other iface/proto instance remains */
|
||||
if (!svc_ds_entry_exists(ctx, hostname, svc_name)) {
|
||||
ds_delete_service(ctx, hostname, svc_name);
|
||||
|
||||
/* Remove neighbor if it has no more services */
|
||||
if (!neighbor_has_services(ctx, hostname)) {
|
||||
ds_delete_neighbor(ctx, hostname);
|
||||
struct avahi_neighbor *n = find_neighbor(ctx, hostname);
|
||||
if (n)
|
||||
free_neighbor(n);
|
||||
}
|
||||
}
|
||||
|
||||
sr_apply_changes(ctx->sr_ses, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
case AVAHI_BROWSER_ALL_FOR_NOW:
|
||||
case AVAHI_BROWSER_CACHE_EXHAUSTED:
|
||||
case AVAHI_BROWSER_FAILURE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void type_browser_cb(AvahiServiceTypeBrowser *b,
|
||||
AvahiIfIndex iface, AvahiProtocol proto,
|
||||
AvahiBrowserEvent event,
|
||||
const char *type, const char *domain,
|
||||
AvahiLookupResultFlags flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
|
||||
(void)b;
|
||||
(void)flags;
|
||||
|
||||
switch (event) {
|
||||
case AVAHI_BROWSER_NEW: {
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
/* Only create one browser per service type */
|
||||
LIST_FOREACH(te, &ctx->type_entries, link) {
|
||||
if (!strcmp(te->type, type))
|
||||
return;
|
||||
}
|
||||
|
||||
te = calloc(1, sizeof(*te));
|
||||
if (!te)
|
||||
return;
|
||||
|
||||
snprintf(te->type, sizeof(te->type), "%s", type);
|
||||
te->browser = avahi_service_browser_new(ctx->client,
|
||||
AVAHI_IF_UNSPEC,
|
||||
AVAHI_PROTO_UNSPEC,
|
||||
type, domain,
|
||||
0,
|
||||
service_browser_cb, ctx);
|
||||
if (!te->browser) {
|
||||
DEBUG("avahi: service_browser_new(%s) failed: %s", type,
|
||||
avahi_strerror(avahi_client_errno(ctx->client)));
|
||||
free(te);
|
||||
return;
|
||||
}
|
||||
|
||||
LIST_INSERT_HEAD(&ctx->type_entries, te, link);
|
||||
DEBUG("avahi: browsing service type %s", type);
|
||||
break;
|
||||
}
|
||||
|
||||
case AVAHI_BROWSER_REMOVE: {
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
LIST_FOREACH(te, &ctx->type_entries, link) {
|
||||
if (!strcmp(te->type, type)) {
|
||||
avahi_service_browser_free(te->browser);
|
||||
LIST_REMOVE(te, link);
|
||||
free(te);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AVAHI_BROWSER_ALL_FOR_NOW:
|
||||
case AVAHI_BROWSER_CACHE_EXHAUSTED:
|
||||
case AVAHI_BROWSER_FAILURE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
|
||||
ctx->client = c;
|
||||
|
||||
switch (state) {
|
||||
case AVAHI_CLIENT_S_RUNNING:
|
||||
INFO("avahi: client running");
|
||||
if (ctx->type_browser)
|
||||
break; /* Already browsing */
|
||||
|
||||
ctx->type_browser = avahi_service_type_browser_new(
|
||||
ctx->client,
|
||||
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
|
||||
NULL, /* domain = NULL → "local" */
|
||||
0,
|
||||
type_browser_cb, ctx);
|
||||
if (!ctx->type_browser)
|
||||
ERROR("avahi: service_type_browser_new failed: %s",
|
||||
avahi_strerror(avahi_client_errno(ctx->client)));
|
||||
break;
|
||||
|
||||
case AVAHI_CLIENT_FAILURE:
|
||||
ERROR("avahi: client failure: %s",
|
||||
avahi_strerror(avahi_client_errno(c)));
|
||||
|
||||
/*
|
||||
* Browsers are internally invalidated when the daemon dies.
|
||||
* Free them explicitly here so they're recreated on reconnect.
|
||||
*/
|
||||
{
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
while (!LIST_EMPTY(&ctx->type_entries)) {
|
||||
te = LIST_FIRST(&ctx->type_entries);
|
||||
avahi_service_browser_free(te->browser);
|
||||
LIST_REMOVE(te, link);
|
||||
free(te);
|
||||
}
|
||||
}
|
||||
if (ctx->type_browser) {
|
||||
avahi_service_type_browser_free(ctx->type_browser);
|
||||
ctx->type_browser = NULL;
|
||||
}
|
||||
|
||||
free_all(ctx);
|
||||
ds_clear_all(ctx);
|
||||
break;
|
||||
|
||||
case AVAHI_CLIENT_S_COLLISION:
|
||||
case AVAHI_CLIENT_S_REGISTERING:
|
||||
case AVAHI_CLIENT_CONNECTING:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Public interface
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn)
|
||||
{
|
||||
int avahi_err;
|
||||
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
ctx->loop = loop;
|
||||
LIST_INIT(&ctx->neighbors);
|
||||
LIST_INIT(&ctx->services);
|
||||
LIST_INIT(&ctx->type_entries);
|
||||
|
||||
/* Dedicated operational session for push writes (avoids sharing
|
||||
* sr_query_ses which the journal thread also uses). */
|
||||
if (sr_session_start(sr_conn, SR_DS_OPERATIONAL, &ctx->sr_ses)) {
|
||||
ERROR("avahi: failed to start sysrepo session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Wire up libev-backed AvahiPoll vtable */
|
||||
ctx->poll_api.userdata = ctx;
|
||||
ctx->poll_api.watch_new = watch_new;
|
||||
ctx->poll_api.watch_update = watch_update;
|
||||
ctx->poll_api.watch_get_events = watch_get_events;
|
||||
ctx->poll_api.watch_free = watch_free;
|
||||
ctx->poll_api.timeout_new = timeout_new;
|
||||
ctx->poll_api.timeout_update = timeout_update;
|
||||
ctx->poll_api.timeout_free = timeout_free;
|
||||
|
||||
ctx->client = avahi_client_new(&ctx->poll_api,
|
||||
AVAHI_CLIENT_NO_FAIL,
|
||||
client_cb, ctx,
|
||||
&avahi_err);
|
||||
if (!ctx->client) {
|
||||
ERROR("avahi: client_new failed: %s", avahi_strerror(avahi_err));
|
||||
sr_session_stop(ctx->sr_ses);
|
||||
ctx->sr_ses = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
INFO("avahi: mDNS neighbor monitor initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void avahi_ctx_exit(struct avahi_ctx *ctx)
|
||||
{
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
/* Free browsers explicitly before freeing the client */
|
||||
while (!LIST_EMPTY(&ctx->type_entries)) {
|
||||
te = LIST_FIRST(&ctx->type_entries);
|
||||
avahi_service_browser_free(te->browser);
|
||||
LIST_REMOVE(te, link);
|
||||
free(te);
|
||||
}
|
||||
if (ctx->type_browser) {
|
||||
avahi_service_type_browser_free(ctx->type_browser);
|
||||
ctx->type_browser = NULL;
|
||||
}
|
||||
if (ctx->client) {
|
||||
avahi_client_free(ctx->client);
|
||||
ctx->client = NULL;
|
||||
}
|
||||
|
||||
if (ctx->sr_ses) {
|
||||
ds_clear_all(ctx);
|
||||
sr_session_stop(ctx->sr_ses);
|
||||
ctx->sr_ses = NULL;
|
||||
}
|
||||
|
||||
free_all(ctx);
|
||||
INFO("avahi: mDNS neighbor monitor stopped");
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#ifndef STATD_AVAHI_H_
|
||||
#define STATD_AVAHI_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sys/queue.h>
|
||||
|
||||
#include <ev.h>
|
||||
#include <sysrepo.h>
|
||||
#include <avahi-client/client.h>
|
||||
#include <avahi-client/lookup.h>
|
||||
#include <avahi-common/watch.h>
|
||||
|
||||
/*
|
||||
* In-memory state for avahi mDNS neighbor tracking.
|
||||
* Services are kept in a flat list; neighbors track addresses only.
|
||||
*/
|
||||
|
||||
struct avahi_addr {
|
||||
char val[64];
|
||||
LIST_ENTRY(avahi_addr) link;
|
||||
};
|
||||
|
||||
struct avahi_txt {
|
||||
char val[256];
|
||||
LIST_ENTRY(avahi_txt) link;
|
||||
};
|
||||
|
||||
struct avahi_service {
|
||||
int ifindex;
|
||||
AvahiProtocol proto;
|
||||
char name[256];
|
||||
char type[64];
|
||||
char domain[64];
|
||||
char hostname[256];
|
||||
uint16_t port;
|
||||
LIST_HEAD(, avahi_txt) txts;
|
||||
LIST_ENTRY(avahi_service) link;
|
||||
};
|
||||
|
||||
struct avahi_neighbor {
|
||||
char hostname[256];
|
||||
LIST_HEAD(, avahi_addr) addrs;
|
||||
LIST_ENTRY(avahi_neighbor) link;
|
||||
};
|
||||
|
||||
struct avahi_type_entry {
|
||||
AvahiServiceBrowser *browser;
|
||||
char type[64];
|
||||
LIST_ENTRY(avahi_type_entry) link;
|
||||
};
|
||||
|
||||
struct avahi_ctx {
|
||||
struct ev_loop *loop;
|
||||
sr_session_ctx_t *sr_ses; /* Dedicated operational DS write session */
|
||||
AvahiClient *client;
|
||||
AvahiServiceTypeBrowser *type_browser;
|
||||
AvahiPoll poll_api; /* libev-backed vtable */
|
||||
LIST_HEAD(, avahi_neighbor) neighbors;
|
||||
LIST_HEAD(, avahi_service) services; /* Flat list; keyed by 5-tuple */
|
||||
LIST_HEAD(, avahi_type_entry) type_entries;
|
||||
};
|
||||
|
||||
int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn);
|
||||
void avahi_ctx_exit(struct avahi_ctx *ctx);
|
||||
|
||||
#endif
|
||||
@@ -36,11 +36,12 @@ AM_CONDITIONAL(CONTAINERS, [test "x$enable_containers" != "xno"])
|
||||
# Check for pkg-config first, warn if it's not installed
|
||||
PKG_PROG_PKG_CONFIG
|
||||
|
||||
PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0])
|
||||
PKG_CHECK_MODULES([libite], [libite >= 2.6.1])
|
||||
PKG_CHECK_MODULES([libyang], [libyang >= 2.1.80])
|
||||
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36])
|
||||
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
|
||||
PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0])
|
||||
PKG_CHECK_MODULES([libite], [libite >= 2.6.1])
|
||||
PKG_CHECK_MODULES([libyang], [libyang >= 2.1.80])
|
||||
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36])
|
||||
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
|
||||
PKG_CHECK_MODULES([avahi_client], [avahi-client >= 0.7])
|
||||
|
||||
AC_CHECK_HEADER([ev.h],
|
||||
[saved_LIBS="$LIBS"
|
||||
|
||||
@@ -3771,6 +3771,91 @@ def show_lldp(json):
|
||||
entry.print()
|
||||
|
||||
|
||||
def _mdns_sort_addrs(addresses):
|
||||
"""Sort addresses: IPv4 first, then non-link-local IPv6, then link-local IPv6."""
|
||||
def key(a):
|
||||
if ":" not in a:
|
||||
return 0
|
||||
if a.lower().startswith("fe80:"):
|
||||
return 2
|
||||
return 1
|
||||
return sorted(addresses, key=key)
|
||||
|
||||
|
||||
def _mdns_last_seen(ts):
|
||||
"""Extract HH:MM:SS from RFC 3339 timestamp."""
|
||||
if not ts:
|
||||
return "-"
|
||||
try:
|
||||
return ts.split("T")[1][:8]
|
||||
except (IndexError, AttributeError):
|
||||
return "-"
|
||||
|
||||
|
||||
def _mdns_svc_name(stype):
|
||||
"""'_https._tcp' → 'https', '_netconf-ssh._tcp' → 'netconf-ssh'."""
|
||||
return stype.lstrip("_").split("._")[0]
|
||||
|
||||
|
||||
def show_mdns(json):
|
||||
mdns = json.get("infix-services:mdns", {})
|
||||
if not mdns:
|
||||
print("mDNS not configured.")
|
||||
return
|
||||
|
||||
# Configuration
|
||||
enabled = mdns.get("enabled")
|
||||
domain = mdns.get("domain", "local")
|
||||
hostname = mdns.get("hostname")
|
||||
reflector_on = mdns.get("reflector", {}).get("enabled")
|
||||
|
||||
if enabled is not None:
|
||||
print(f"{'Enabled':<16}: {'yes' if enabled else 'no'}")
|
||||
print(f"{'Domain':<16}: {domain}")
|
||||
if hostname:
|
||||
print(f"{'Hostname':<16}: {hostname}")
|
||||
|
||||
ifaces = mdns.get("interfaces", {})
|
||||
if ifaces.get("allow"):
|
||||
print(f"{'Allow':<16}: {', '.join(ifaces['allow'])}")
|
||||
if ifaces.get("deny"):
|
||||
print(f"{'Deny':<16}: {', '.join(ifaces['deny'])}")
|
||||
|
||||
reflector = mdns.get("reflector", {})
|
||||
if reflector_on is not None:
|
||||
print(f"{'Reflector':<16}: {'yes' if reflector_on else 'no'}")
|
||||
if reflector.get("service-filter"):
|
||||
print(f"{'Svc filter':<16}: {', '.join(reflector['service-filter'])}")
|
||||
|
||||
# Neighbors
|
||||
neighbors = mdns.get("neighbors", {}).get("neighbor", [])
|
||||
if not neighbors:
|
||||
print("\nNo mDNS neighbors.")
|
||||
return
|
||||
|
||||
print()
|
||||
table = SimpleTable([
|
||||
Column("HOSTNAME", flexible=True),
|
||||
Column("ADDRESS"),
|
||||
Column("LAST SEEN"),
|
||||
Column("SERVICES"),
|
||||
])
|
||||
|
||||
for nbr in sorted(neighbors, key=lambda n: n.get("hostname", "")):
|
||||
addrs = _mdns_sort_addrs(nbr.get("address", []))
|
||||
ts = _mdns_last_seen(nbr.get("last-seen", ""))
|
||||
svcs = nbr.get("service", [])
|
||||
svc_str = " ".join(
|
||||
f"{_mdns_svc_name(s.get('type', '?'))}({s.get('port', 0)})"
|
||||
for s in svcs
|
||||
) if svcs else "-"
|
||||
table.row(nbr.get("hostname", "?"), addrs[0] if addrs else "-", ts, svc_str)
|
||||
for addr in addrs[1:]:
|
||||
table.row("", addr, "", "")
|
||||
|
||||
table.print()
|
||||
|
||||
|
||||
def parse_firewall_log_line(line):
|
||||
"""Parse a single firewall log line into structured data"""
|
||||
|
||||
@@ -5566,6 +5651,8 @@ def main():
|
||||
|
||||
subparsers.add_parser('show-lldp', help='Show LLDP neighbors')
|
||||
|
||||
subparsers.add_parser('show-mdns', help='Show mDNS configuration and neighbors')
|
||||
|
||||
subparsers.add_parser('show-firewall', help='Show firewall overview')
|
||||
subparsers.add_parser('show-firewall-matrix', help='Show firewall matrix')
|
||||
subparsers.add_parser('show-firewall-zone', help='Show firewall zones') \
|
||||
@@ -5636,6 +5723,8 @@ def main():
|
||||
show_interfaces(json_data, args.name)
|
||||
elif args.command == "show-lldp":
|
||||
show_lldp(json_data)
|
||||
elif args.command == "show-mdns":
|
||||
show_mdns(json_data)
|
||||
elif args.command == "show-firewall":
|
||||
show_firewall(json_data)
|
||||
elif args.command == "show-firewall-matrix":
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "shared.h"
|
||||
#include "journal.h"
|
||||
#include "avahi.h"
|
||||
|
||||
/* New kernel feature, not in sys/mman.h yet */
|
||||
#ifndef MFD_NOEXEC_SEAL
|
||||
@@ -69,6 +70,7 @@ struct statd {
|
||||
sr_conn_ctx_t *sr_conn; /* Connection (owns YANG context) */
|
||||
struct ev_loop *ev_loop;
|
||||
struct journal_ctx journal; /* Journal thread context */
|
||||
struct avahi_ctx avahi; /* mDNS neighbor monitor */
|
||||
};
|
||||
|
||||
static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent,
|
||||
@@ -522,6 +524,9 @@ int main(int argc, char *argv[])
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (avahi_ctx_init(&statd.avahi, statd.ev_loop, statd.sr_conn))
|
||||
INFO("mDNS neighbor monitoring not available");
|
||||
|
||||
/* Signal readiness to Finit */
|
||||
pidfile(NULL);
|
||||
|
||||
@@ -531,6 +536,7 @@ int main(int argc, char *argv[])
|
||||
/* We should never get here during normal operation */
|
||||
INFO("Status daemon shutting down");
|
||||
|
||||
avahi_ctx_exit(&statd.avahi);
|
||||
journal_stop(&statd.journal);
|
||||
|
||||
unsub_to_all(&statd);
|
||||
|
||||
Reference in New Issue
Block a user