Merge pull request #1425 from kernelkit/mdns-fixes

Mdns fixes
This commit is contained in:
Joachim Wiberg
2026-03-09 19:26:41 +01:00
committed by GitHub
77 changed files with 3397 additions and 1116 deletions
+1
View File
@@ -1,5 +1,6 @@
config BR2_PACKAGE_ALDER_ALDER
bool "Alder"
depends on BR2_aarch64
select BR2_PACKAGE_LINUX_FIRMWARE_INSIDE_SECURE_MINIFW
help
Alder
@@ -1,5 +1,6 @@
config BR2_PACKAGE_MARVELL_CN9130_CRB
bool "Marvell CN9130-CRB"
depends on BR2_aarch64
select BR2_PACKAGE_LINUX_FIRMWARE_INSIDE_SECURE_MINIFW
help
Customer Reference Board for CN9130
@@ -1,5 +1,6 @@
config BR2_PACKAGE_MARVELL_ESPRESSOBIN
bool "Marvell ESPRESSObin"
depends on BR2_aarch64
select BR2_PACKAGE_LINUX_FIRMWARE_INSIDE_SECURE_MINIFW
help
Marvell ESPRESSObin
+1
View File
@@ -1,5 +1,6 @@
config BR2_PACKAGE_STYX_DCP_SC_28P
bool "Styx DCP-SC-28P"
depends on BR2_aarch64
select BR2_PACKAGE_LINUX_FIRMWARE_INSIDE_SECURE_MINIFW
help
Styx DCP-SC-28P
@@ -1,3 +1,13 @@
server {
listen 80;
listen [::]:80;
server_name network.local;
location / {
include /etc/nginx/netbrowse.conf;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
@@ -62,8 +62,8 @@ if ! runlevel >/dev/null 2>&1; then
fi
initctl -bq status lldpd && lldpcli configure system hostname "$new_hostname" 2>/dev/null
initctl -bq status mdns && avahi-set-host-name "$new_hostname" 2>/dev/null
initctl -bq touch netbrowse 2>/dev/null
initctl -bq touch mdns-alias 2>/dev/null
initctl -bq touch netbrowse 2>/dev/null
# If called from dhcp script we need to reload to activate new name in syslogd
# Otherwise we're called from confd, which does the reload when all is done.
-1
View File
@@ -50,7 +50,6 @@ BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y
BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y
BR2_PACKAGE_PYTHON_GUNICORN=y
BR2_PACKAGE_LIBSSH_OPENSSL=y
BR2_PACKAGE_LIBSSH2=y
BR2_PACKAGE_LIBSSH2_OPENSSL=y
-1
View File
@@ -52,7 +52,6 @@ BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y
BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y
BR2_PACKAGE_PYTHON_GUNICORN=y
BR2_PACKAGE_LIBSSH2=y
BR2_PACKAGE_LIBOPENSSL_BIN=y
BR2_PACKAGE_LIBINPUT=y
-1
View File
@@ -60,7 +60,6 @@ BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y
BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y
BR2_PACKAGE_PYTHON_GUNICORN=y
BR2_PACKAGE_LIBSSH_OPENSSL=y
BR2_PACKAGE_LIBSSH2=y
BR2_PACKAGE_LIBSSH2_OPENSSL=y
-1
View File
@@ -49,7 +49,6 @@ BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y
BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y
BR2_PACKAGE_PYTHON_GUNICORN=y
BR2_PACKAGE_LIBSSH_OPENSSL=y
BR2_PACKAGE_LIBSSH2=y
BR2_PACKAGE_LIBSSH2_OPENSSL=y
+33
View File
@@ -3,6 +3,38 @@ Change Log
All notable changes to the project are documented in this file.
[v26.03.0][UNRELEASED]
-------------------------
### Changes
- **Revamped <http://network.local> device browser**. Device cards now
show the IP address, product name, and firmware version from mDNS TXT
records. the mDNS browser is now also available over plain HTTP
- mDNS service records now embed the per-device hostname (using avahi's `%h`
wildcard), so devices avoid the `#2`, `#3` suffix collisions on service names
when multiple Infix devices share the same LAN
- Add configurable mDNS hostname: set `services mdns hostname` to override the
mDNS name advertised in A/AAAA records. Supports `%h` (default hostname),
`%i` (hardware ID), and `%m` (MAC address) format specifiers
- cli: new `show mdns` command to list mDNS-discovered devices on the LAN,
with addresses and product model
- Add SSH client commands to the CLI:
- `ssh [user <name>] [port <num>] <host>` — connect to a remote device
- `set ssh known-hosts <host> <keytype> <pubkey>` — pre-enroll a host key
received out-of-band, e.g. after a factory reset changes the device host key
- `no ssh known-hosts <host>` — remove a stale or changed host key entry
### Fixes
- Fix #1387: `infix.local` now resolves to exactly one device per LAN. Previously
all Infix devices claimed both `hostname.local` and `infix.local`, causing avahi
to append `#2`, `#3` suffixes to the shared alias on busy networks. Assignment
is now first-come-first-served using standard mDNS conflict resolution
- Fix #1416: `show firewall` command show an error when the firewall is disabled
- Fix regression in MVEBU SafeXcel Crypto Engine for Marvell Armada SOCs (37xx,
7k, 8k, and CN913x series). Firmware package lost in v26.01.0
[v26.02.0][] - 2026-03-01
-------------------------
@@ -1932,6 +1964,7 @@ Supported YANG models in addition to those used by sysrepo and netopeer:
[buildroot]: https://buildroot.org/
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v26.02.0...HEAD
[v26.03.0]: https://github.com/kernelkit/infix/compare/v26.02.0...v26.03.0
[v26.02.0]: https://github.com/kernelkit/infix/compare/v26.01.0...v26.02.0
[v26.01.0]: https://github.com/kernelkit/infix/compare/v25.11.0...v26.01.0
[v25.11.0]: https://github.com/kernelkit/infix/compare/v25.10.0...v25.11.0
+21
View File
@@ -27,6 +27,16 @@ The standard IETF model for static routes reside under the `static`
control plane protocol. For our examples we use the instance name
`default`, you can use any name.
The most common case when using a static IP setup is adding a default
route (i.e., the default gateway):
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit routing control-plane-protocol static name default ipv4</b>
admin@example:/config/routing/…/ipv4/> <b>set route 0.0.0.0/0 next-hop next-hop-address 192.168.1.1</b>
admin@example:/config/routing/…/ipv4/> <b>leave</b>
admin@example:/>
</code></pre>
For a route with destination 192.168.200.0/24 via 192.168.1.1:
<pre class="cli"><code>admin@example:/> <b>configure</b>
@@ -53,6 +63,17 @@ admin@example:/>
## IPv6 Static routes
Default route via an IPv6 gateway:
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit routing control-plane-protocol static name default ipv6</b>
admin@example:/config/routing/…/ipv6/> <b>set route ::/0 next-hop next-hop-address 2001:db8:3c4d:1::1</b>
admin@example:/config/routing/…/ipv6/> <b>leave</b>
admin@example:/>
</code></pre>
For a route with destination 2001:db8:3c4d:200::/64 via 2001:db8:3c4d:1::1:
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit routing control-plane-protocol static name default ipv6</b>
admin@example:/config/routing/…/ipv6/> <b>set route 2001:db8:3c4d:200::/64 next-hop next-hop-address 2001:db8:3c4d:1::1</b>
+1
View File
@@ -1,5 +1,6 @@
config BR2_PACKAGE_MDNS_ALIAS
bool mdns-alias
select BR2_PACKAGE_AVAHI
select BR2_PACKAGE_LIBUEV
help
Advertises the initial $(HOSTNAME).local and network.local with Avahi.
+1 -1
View File
@@ -1,5 +1,5 @@
# From GitHub release
sha256 fd7272e4e520418a4a1352b69df852d963adfa928afcfda8e82ce7953626efdd mdns-alias-1.0.tar.gz
sha256 cf32b3c224325b3b660669a82e6dc30ad8438016d7492433cea591fa3a8a1dd9 mdns-alias-1.1.tar.gz
# Locally generated
sha256 3d6f910b5e198f3daab48047b8ee6949040f7abee3927daf2e231f265faf7d91 LICENSE
+2 -2
View File
@@ -4,11 +4,11 @@
#
################################################################################
MDNS_ALIAS_VERSION = 1.0
MDNS_ALIAS_VERSION = 1.1
MDNS_ALIAS_SITE = https://github.com/troglobit/mdns-alias/releases/download/v$(MDNS_ALIAS_VERSION)
MDNS_ALIAS_LICENSE = ISC
MDNS_ALIAS_LICENSE_FILES = LICENSE
MDNS_ALIAS_DEPENDENCIES = host-pkgconf avahi
MDNS_ALIAS_DEPENDENCIES = host-pkgconf avahi libuev
#MDNS_ALIAS_AUTORECONF = YES
#MDNS_ALIAS_DEPENDENCIES += host-automake host-autoconf
+5 -2
View File
@@ -1,2 +1,5 @@
service <!service/mdns/running> env:-/etc/default/mdns-alias log:prio:daemon.debug,tag:mdns \
[2345] mdns-alias $MDNS_ALIAS_ARGS -- mDNS alias advertiser
# Avahi advertises the system default hostname, this service advertises
# /etc/hostname (-H) and, optionally, network.local as CNAMEs. Changes
# to /etc/default/mdns-alias will cause Finit to restart not reload.
service <service/mdns/running> env:-/etc/default/mdns-alias \
[2345] mdns-alias -H $MDNS_ALIAS_ARGS --
+2 -3
View File
@@ -1,6 +1,5 @@
config BR2_PACKAGE_NETBROWSE
bool netbrowse
select BR2_PACKAGE_PYTHON_FLASK
depends on BR2_PACKAGE_HOST_GO_TARGET_ARCH_SUPPORTS
help
Browse mDNS hosts on your network from http://network.local
Browse mDNS hosts on your network from https://network.local
+4 -6
View File
@@ -4,10 +4,10 @@
#
################################################################################
NETBROWSE_VERSION = 1.0
NETBROWSE_VERSION = 2.0
NETBROWSE_SITE_METHOD = local
NETBROWSE_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/netbrowse
NETBROWSE_SETUP_TYPE = setuptools
NETBROWSE_GOMOD = github.com/kernelkit/infix/src/netbrowse
NETBROWSE_LICENSE = MIT
NETBROWSE_LICENSE_FILES = LICENSE
NETBROWSE_REDISTRIBUTE = NO
@@ -15,10 +15,8 @@ NETBROWSE_REDISTRIBUTE = NO
define NETBROWSE_INSTALL_EXTRA
$(INSTALL) -D -m 0644 $(NETBROWSE_PKGDIR)/netbrowse.svc \
$(FINIT_D)/available/netbrowse.conf
$(INSTALL) -d -m 755 $(TARGET_DIR)/etc/default
echo "NETBROWSE_ARGS=\"network.local $(BR2_TARGET_GENERIC_HOSTNAME).local\"" \
> $(TARGET_DIR)/etc/default/netbrowse
ln -sf ../available/netbrowse.conf $(FINIT_D)/enabled/netbrowse.conf
endef
NETBROWSE_POST_INSTALL_TARGET_HOOKS += NETBROWSE_INSTALL_EXTRA
$(eval $(python-package))
$(eval $(golang-package))
+2 -2
View File
@@ -1,2 +1,2 @@
service <!> name:netbrowse @www-data:www-data log:prio:daemon.debug,tag:mdns \
[2345] gunicorn -w 1 -b 127.0.0.1:8000 netbrowse:app -- Network browser
service <!> name:netbrowse log:prio:daemon.debug,tag:netbrowse \
[2345] netbrowse -l 127.0.0.1:8000 -- Network browser
@@ -1,2 +1,2 @@
# --log-level debug
ZEBRA_ARGS="-A 127.0.0.1 -u frr -g frr --log syslog --log-level err"
ZEBRA_ARGS="-A 127.0.0.1 -a -s 90000000 -u frr -g frr --log syslog --log-level err"
@@ -1,2 +1,3 @@
service <!> pid:!/run/frr/mgmtd.pid env:-/etc/default/mgmtd \
[2345] mgmtd $MGMTD_ARGS -- FRR MGMT daemon
[2345] mgmtd $MGMTD_ARGS \
-- FRR MGMT daemon
@@ -1,2 +1,3 @@
service <!pid/zebra> env:-/etc/default/ospfd \
[2345] ospfd $OSPFD_ARGS -- OSPF daemon
service <!pid/zebra> pid:!/run/frr/ospfd.pid env:-/etc/default/ospfd \
[2345] ospfd $OSPFD_ARGS \
-- OSPF daemon
@@ -1,3 +1,3 @@
service <pid/zebra> env:-/etc/default/ripd \
[2345] ripd $RIPD_ARGS
service <pid/zebra> pid:!/run/frr/ripd.pid env:-/etc/default/ripd \
[2345] ripd $RIPD_ARGS \
-- RIP daemon
@@ -1,3 +1,3 @@
service <!pid/mgmtd> pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
[2345] zebra $ZEBRA_ARGS
service <!pid/mgmtd> pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
[2345] zebra $ZEBRA_ARGS \
-- Zebra routing daemon
@@ -1,5 +1,4 @@
# Default FRR daemons file for Infix - confd overwrites on routing changes.
# watchfrr, zebra, mgmtd, and staticd are always started by frrinit.sh.
# Default FRR daemons file used with watchfrr, started by frrinit.sh.
ospfd=no
ripd=no
bfdd=no
@@ -25,4 +24,3 @@ staticd_options="-A 127.0.0.1"
bfdd_options=" -A 127.0.0.1"
frr_profile="traditional"
@@ -0,0 +1 @@
! Empty stub — mgmtd reads its own config at startup; file must exist to avoid log noise.
@@ -0,0 +1 @@
! Empty stub — mgmtd reads per-daemon configs at startup; file must exist to avoid log noise.
@@ -0,0 +1 @@
! Empty stub — mgmtd reads per-daemon configs at startup; file must exist to avoid log noise.
@@ -0,0 +1 @@
! Empty stub — mgmtd reads per-daemon configs at startup; file must exist to avoid log noise.
@@ -0,0 +1 @@
! Empty stub — mgmtd reads per-daemon configs at startup; file must exist to avoid log noise.
+1 -1
View File
@@ -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
@@ -1,7 +1,7 @@
From 03f273fc540082d1eaa23bd9b5847e695afd8283 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Thu, 25 Sep 2025 15:00:54 +0200
Subject: [PATCH] Silence warnings about old backends
Subject: [PATCH 1/2] Silence warnings about old backends
Organization: Wires
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
@@ -0,0 +1,95 @@
From 6ab218fe7f2c7027cc5347e3b082285870c502e6 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Fri, 6 Mar 2026 07:44:38 +0100
Subject: [PATCH 2/2] fix(functions): lift iptables name length limit when
using nftables
Organization: Wires
The max_zone_name_len() and max_policy_name_len() functions return 17
and 18 respectively, derived from iptables' 28-char netfilter chain name
limit. These limits are applied unconditionally in Zone.check_name()
and Policy.check_name() regardless of the active backend.
When FirewallBackend=nftables nftables imposes no such restriction, so
user-defined zone and policy names (e.g. "appletv-to-lan-guest", 20
chars) that exceed the iptables-derived limit are incorrectly rejected.
Add _nftables_backend() which reads firewalld.conf directly so the check
can be skipped without threading backend context through to check_name()
call sites, which have no access to all_io_objects. When nftables is
active, both functions return sys.maxsize, effectively disabling the
length check.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/firewall/functions.py | 31 ++++++++++++++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/src/firewall/functions.py b/src/firewall/functions.py
index 27c862fd..1b8a32ce 100644
--- a/src/firewall/functions.py
+++ b/src/firewall/functions.py
@@ -10,9 +10,10 @@ import os
import os.path
import shlex
import string
+import sys
import tempfile
from firewall.core.logger import log
-from firewall.config import FIREWALLD_TEMPDIR, FIREWALLD_PIDFILE
+from firewall.config import FIREWALLD_CONF, FIREWALLD_TEMPDIR, FIREWALLD_PIDFILE
NOPRINT_TRANS_TABLE = {
# Limit to C0 and C1 code points. Building entries for all unicode code
@@ -576,12 +577,35 @@ def ppid_of_pid(pid):
return pid
+def _nftables_backend():
+ """Return True if FirewallBackend=nftables is configured in firewalld.conf.
+
+ When using nftables the iptables-derived 28-char chain name limit does not
+ apply. Reading the config file directly avoids threading backend context
+ through check_name() call sites, which have no access to all_io_objects.
+ """
+ try:
+ with open(FIREWALLD_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith("FirewallBackend="):
+ return line.split("=", 1)[1].strip() == "nftables"
+ except OSError:
+ pass
+ return False
+
+
def max_policy_name_len():
"""
iptables limits length of chain to (currently) 28 chars.
The longest chain we create is POST_<policy>_allow,
which leaves 28 - 11 = 17 chars for <policy>.
+
+ When using the nftables backend, nftables imposes no practical name length
+ restriction, so we return sys.maxsize to lift the check entirely.
"""
+ if _nftables_backend():
+ return sys.maxsize
from firewall.core.ipXtables import POLICY_CHAIN_PREFIX
from firewall.core.base import SHORTCUTS
@@ -594,7 +618,12 @@ def max_zone_name_len():
Netfilter limits length of chain to (currently) 28 chars.
The longest chain we create is POST_<zone>_allow,
which leaves 28 - 11 = 17 chars for <zone>.
+
+ When using the nftables backend, nftables imposes no practical name length
+ restriction, so we return sys.maxsize to lift the check entirely.
"""
+ if _nftables_backend():
+ return sys.maxsize
from firewall.core.base import SHORTCUTS
longest_shortcut = max(map(len, SHORTCUTS.values()))
--
2.43.0
+26
View File
@@ -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,
+1 -1
View File
@@ -1,4 +1,4 @@
pkglibexec_SCRIPTS = bootstrap error load gen-service gen-hostname \
pkglibexec_SCRIPTS = bootstrap error load gen-hostname \
gen-interfaces gen-motd gen-hardware gen-version \
mstpd-wait-online wait-interface
sbin_SCRIPTS = dagger migrate firewall
-47
View File
@@ -1,47 +0,0 @@
#!/bin/sh
# Very basic avahi .service generator, works for tcp (http) services
. /etc/os-release
cmd=$1
host=$2
name=$3
type=$4
port=$5
desc=$6
shift 6
file="/etc/avahi/services/$name.service"
case $cmd in
delete)
rm -f "$file"
exit 0
;;
update)
if [ ! -f "$file" ]; then
exit 0
fi
;;
*)
;;
esac
cat <<EOF >"$file"
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">$desc</name>
<service>
<type>$type</type>
<port>$port</port>
<host-name>$host.local</host-name>
<txt-record>vv=1</txt-record>
<txt-record>vendor=$(jq -r .vendor /run/system.json)</txt-record>
<txt-record>product=$(jq -r '."product-name"' /run/system.json)</txt-record>
<txt-record>serial=$(jq -r '."serial-number"' /run/system.json)</txt-record>
<txt-record>deviceid=$(jq -r '."mac-address"' /run/system.json)</txt-record>
<txt-record>vn=$VENDOR_NAME</txt-record>
<txt-record>on=$NAME</txt-record>
<txt-record>ov=$VERSION_ID</txt-record>$(for txt in "$@"; do printf "\n <txt-record>%s</txt-record>" "$txt"; done)
</service>
</service-group>
EOF
+174 -34
View File
@@ -19,9 +19,13 @@
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
#define AVAHI_SVC_PATH "/etc/avahi/services"
#define LLDP_CONFIG "/etc/lldpd.d/confd.conf"
#define LLDP_CONFIG_NEXT LLDP_CONFIG"+"
enum mdns_cmd { MDNS_ADD, MDNS_DELETE, MDNS_UPDATE };
#define FOREACH_SVC(SVC) \
SVC(none) \
SVC(ssh) \
@@ -59,15 +63,48 @@ struct mdns_svc {
char *desc;
char *text;
} services[] = {
{ web, "https", "_https._tcp", 443, "Web Management Interface", "adminurl=https://%s.local" },
{ ttyd, "ttyd", "_https._tcp", 443, "Web Console Interface", "adminurl=https://%s.local:7681" },
{ web, "http", "_http._tcp", 80, "Web Management Interface", "adminurl=http://%s.local" },
{ netconf, "netconf", "_netconf-ssh._tcp", 830, "NETCONF (XML/SSH)", NULL },
{ restconf, "restconf", "_restconf-tls._tcp", 443, "RESTCONF (JSON/HTTP)", NULL },
{ ssh, "sftp-ssh", "_sftp-ssh._tcp", 22, "Secure file transfer (FTP/SSH)", NULL },
{ ssh, "ssh", "_ssh._tcp", 22, "Secure shell command line interface (CLI)", NULL },
{ web, "https", "_https._tcp", 443, "%h Web", "adminurl=https://%s.local" },
{ ttyd, "ttyd", "_https._tcp", 443, "%h Console", "adminurl=https://%s.local:7681" },
{ web, "http", "_http._tcp", 80, "%h Web", "adminurl=http://%s.local" },
{ netconf, "netconf", "_netconf-ssh._tcp", 830, "%h", NULL },
{ restconf, "restconf", "_restconf-tls._tcp", 443, "%h", NULL },
{ ssh, "sftp-ssh", "_sftp-ssh._tcp", 22, "%h", NULL },
{ ssh, "ssh", "_ssh._tcp", 22, "%h", NULL },
};
static const char *jgets(json_t *obj, const char *key)
{
json_t *val = json_object_get(obj, key);
return (val && !json_is_null(val)) ? json_string_value(val) : NULL;
}
/* Write str to fp with XML special characters escaped. */
static void xml_escape(FILE *fp, const char *str)
{
for (; *str; str++) {
switch (*str) {
case '&': fputs("&amp;", fp); break;
case '<': fputs("&lt;", fp); break;
case '>': fputs("&gt;", fp); break;
case '"': fputs("&quot;", fp); break;
case ';': break; /* avahi txt-record separator */
default: fputc(*str, fp); break;
}
}
}
static void write_txt(FILE *fp, const char *key, const char *val)
{
if (!val)
return;
fputs(" <txt-record>", fp);
xml_escape(fp, key);
fputc('=', fp);
xml_escape(fp, val);
fputs("</txt-record>\n", fp);
}
/*
* On hostname changes we need to update the mDNS records, in particular
* the ones advertising an adminurl (standarized by Apple), because they
@@ -77,27 +114,126 @@ struct mdns_svc {
* adminurl to include 'admin@%s.local' to pre-populate the default
* username in the login dialog.
*/
static int mdns_records(const char *cmd, svc type)
static int mdns_records(int cmd, svc type)
{
char hostname[MAXHOSTNAMELEN + 1];
const char *vendor, *product, *serial, *mac;
const char *vn, *on, *ov;
if (gethostname(hostname, sizeof(hostname))) {
ERRNO("failed getting system hostname");
return SR_ERR_SYS;
}
vendor = jgets(confd.root, "vendor");
product = jgets(confd.root, "product-name");
serial = jgets(confd.root, "serial-number");
mac = jgets(confd.root, "mac-address");
vn = fgetkey("/etc/os-release", "VENDOR_NAME");
on = fgetkey("/etc/os-release", "NAME");
ov = fgetkey("/etc/os-release", "VERSION_ID");
for (size_t i = 0; i < NELEMS(services); i++) {
struct mdns_svc *srv = &services[i];
char buf[256] = "";
FILE *fp;
if (type != all && srv->svc != type)
continue;
if (srv->text)
snprintf(buf, sizeof(buf), srv->text, hostname);
if (cmd == MDNS_DELETE) {
erasef(AVAHI_SVC_PATH "/%s.service", srv->name);
continue;
}
systemf("/usr/libexec/confd/gen-service %s %s %s %s %d \"%s\" %s", cmd,
hostname, srv->name, srv->type, srv->port, srv->desc, buf);
if (cmd == MDNS_UPDATE && !fexistf(AVAHI_SVC_PATH "/%s.service", srv->name))
continue;
fp = fopenf("w", AVAHI_SVC_PATH "/%s.service", srv->name);
if (!fp) {
ERRNO("failed creating %s.service", srv->name);
continue;
}
fprintf(fp,
"<?xml version=\"1.0\" standalone='no'?>\n"
"<!DOCTYPE service-group SYSTEM \"avahi-service.dtd\">\n"
"<service-group>\n"
" <name replace-wildcards=\"yes\">%s</name>\n"
" <service>\n"
" <type>%s</type>\n"
" <port>%d</port>\n"
" <txt-record>vv=1</txt-record>\n",
srv->desc, srv->type, srv->port);
write_txt(fp, "vendor", vendor);
write_txt(fp, "product", product);
write_txt(fp, "serial", serial);
write_txt(fp, "deviceid", mac);
write_txt(fp, "vn", vn);
write_txt(fp, "on", on);
write_txt(fp, "ov", ov);
if (srv->text) {
char txt[256];
snprintf(txt, sizeof(txt), srv->text, hostname);
fputs(" <txt-record>", fp);
xml_escape(fp, txt);
fputs("</txt-record>\n", fp);
}
fprintf(fp,
" </service>\n"
"</service-group>\n");
fclose(fp);
}
/* Always-on records tied to mDNS being active, not a specific service */
if (type == all) {
if (cmd == MDNS_DELETE) {
erasef(AVAHI_SVC_PATH "/workstation.service");
erasef(AVAHI_SVC_PATH "/device-info.service");
} else {
FILE *fp;
fp = fopenf("w", AVAHI_SVC_PATH "/workstation.service");
if (fp) {
fprintf(fp,
"<?xml version=\"1.0\" standalone='no'?>\n"
"<!DOCTYPE service-group SYSTEM \"avahi-service.dtd\">\n"
"<service-group>\n"
" <name replace-wildcards=\"yes\">%%h [%s]</name>\n"
" <service>\n"
" <type>_workstation._tcp</type>\n"
" <port>9</port>\n"
" </service>\n"
"</service-group>\n",
mac ?: "");
fclose(fp);
} else {
ERRNO("failed creating workstation.service");
}
/* TODO: Use device-info YANG model for Apple-compatible model string */
fp = fopenf("w", AVAHI_SVC_PATH "/device-info.service");
if (fp) {
fprintf(fp,
"<?xml version=\"1.0\" standalone='no'?>\n"
"<!DOCTYPE service-group SYSTEM \"avahi-service.dtd\">\n"
"<service-group>\n"
" <name replace-wildcards=\"yes\">%%h</name>\n"
" <service>\n"
" <type>_device-info._tcp</type>\n"
" <port>0</port>\n");
write_txt(fp, "model", product);
fprintf(fp,
" </service>\n"
"</service-group>\n");
fclose(fp);
} else {
ERRNO("failed creating device-info.service");
}
}
}
return SR_ERR_OK;
@@ -182,7 +318,7 @@ static void svc_enadis(int ena, svc type, const char *svc)
}
if (type != none)
mdns_records(ena ? "add" : "delete", type);
mdns_records(ena ? MDNS_ADD : MDNS_DELETE, type);
systemf("initctl -nbq touch avahi");
systemf("initctl -nbq touch nginx");
@@ -204,11 +340,20 @@ static void fput_list(FILE *fp, struct lyd_node *cfg, const char *list, const ch
#define AVAHI_CONF "/etc/avahi/avahi-daemon.conf"
static void mdns_conf(struct lyd_node *cfg)
static void mdns_conf(struct confd *confd, struct lyd_node *cfg)
{
char hname[HOST_NAME_MAX + 1];
const char *hostname;
const char *fmt;
struct lyd_node *ctx;
FILE *fp;
fmt = lydx_get_cattr(cfg, "hostname"); /* "%h" when unset (YANG default) */
if (!hostnamefmt(confd, fmt, hname, sizeof(hname), NULL, 0))
hostname = hname;
else
hostname = fgetkey("/etc/os-release", "DEFAULT_HOSTNAME");
fp = fopen(AVAHI_CONF, "w");
if (!fp) {
ERRNO("failed creating %s", AVAHI_CONF);
@@ -217,9 +362,10 @@ static void mdns_conf(struct lyd_node *cfg)
fprintf(fp, "# Generated by Infix confd\n"
"[server]\n"
"host-name=%s\n"
"domain-name=%s\n"
"use-ipv4=yes\n"
"use-ipv6=yes\n", lydx_get_cattr(cfg, "domain"));
"use-ipv6=yes\n", hostname, lydx_get_cattr(cfg, "domain"));
ctx = lydx_get_descendant(lyd_child(cfg), "interfaces", NULL);
if (ctx) {
@@ -254,23 +400,17 @@ static void mdns_cname(sr_session_ctx_t *session)
if (ena) {
int www = srx_enabled(session, "/infix-services:web/netbrowse/enabled");
const char *hostname = fgetkey("/etc/os-release", "DEFAULT_HOSTNAME");
FILE *fp;
if (hostname || www) {
FILE *fp;
fp = fopen("/etc/default/mdns-alias", "w");
if (fp) {
fprintf(fp, "MDNS_ALIAS_ARGS=\"%s%s %s\"\n",
hostname ?: "", hostname ? ".local" : "",
www ? "network.local" : "");
fclose(fp);
} else {
ERRNO("failed updating mDNS aliases");
ena = 0;
}
} else
ena = 0; /* nothing to advertise */
fp = fopen("/etc/default/mdns-alias", "w");
if (fp) {
fprintf(fp, "MDNS_ALIAS_ARGS=\"%s\"\n",
www ? "network.local" : "");
fclose(fp);
} else {
ERRNO("failed updating mDNS aliases");
ena = 0;
}
}
svc_enadis(ena, none, "mdns-alias");
@@ -292,10 +432,10 @@ static int mdns_change(sr_session_ctx_t *session, struct lyd_node *config, struc
ena = lydx_is_enabled(srv, "enabled");
if (ena) {
/* Generate/update avahi-daemon.conf */
mdns_conf(srv);
mdns_conf(confd, srv);
/* Generate/update basic mDNS service records */
mdns_records("update", all);
mdns_records(MDNS_UPDATE, all);
}
svc_enadis(ena, none, "avahi");
+12 -6
View File
@@ -1540,27 +1540,33 @@ static char *get_mac(struct confd *confd, char *mac, size_t len)
int hostnamefmt(struct confd *confd, const char *fmt, char *hostnm, size_t hostlen,
char *domain, size_t domlen)
{
char buf[HOST_NAME_MAX + 1];
char mac[10];
char *ptr;
char *f, *ptr;
size_t i;
if (!fmt || !*fmt)
return -1;
strlcpy(buf, fmt, sizeof(buf));
f = buf;
memset(hostnm, 0, hostlen);
if (domain)
memset(domain, 0, domlen);
ptr = strchr(fmt, '.');
ptr = strchr(f, '.');
if (ptr) {
*ptr++ = 0;
if (domain)
strlcpy(domain, ptr, domlen);
}
for (i = 0; i < strlen(fmt); i++) {
if (fmt[i] == '%') {
switch (fmt[++i]) {
for (i = 0; i < strlen(f); i++) {
if (f[i] == '%') {
if (f[++i] == '\0')
break;
switch (f[i]) {
case 'i':
strlcat(hostnm, id, hostlen);
break;
@@ -1577,7 +1583,7 @@ int hostnamefmt(struct confd *confd, const char *fmt, char *hostnm, size_t hostl
break; /* Unknown, skip */
}
} else {
char ch[2] = { fmt[i], 0 };
char ch[2] = { f[i], 0 };
strlcat(hostnm, ch, hostlen);
}
}
+1 -1
View File
@@ -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@2025-12-10.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"
+78
View File
@@ -16,6 +16,12 @@ 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;
}
import ietf-keystore {
prefix ks;
@@ -25,6 +31,10 @@ module infix-services {
contact "kernelkit@googlegroups.com";
description "Infix services, generic.";
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";
reference "internal";
@@ -80,6 +90,18 @@ module infix-services {
type inet:domain-name;
}
leaf hostname {
description "Hostname for mDNS A/AAAA records (avahi host-name setting).
When not set the default is the build-time branding hostname.
Supports the same format specifiers as /system/hostname:
%h Default hostname (DEFAULT_HOSTNAME from os-release)
%i OS ID from os-release
%m Last three octets of base MAC (e.g. c0-ff-ee)";
type infix-sys:hostname;
default "%h";
}
container interfaces {
description "Filter interfaces to act on.";
@@ -127,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";
+245 -3
View File
@@ -1,9 +1,12 @@
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
@@ -79,6 +82,53 @@ static int run(char *const argv[])
return -1;
}
/*
* Like run(), but drops privileges to the given user before exec.
* Use for commands that must run as the CLI user, not root (klishd).
*/
static int run_as_user(const char *user, char *const argv[])
{
struct passwd *pw;
pid_t pid;
int rc;
pw = getpwnam(user);
if (!pw) {
fprintf(stderr, ERRMSG "unknown user: %s\n", user);
return -1;
}
pid = fork();
if (pid == -1)
return -1;
if (!pid) {
if (initgroups(user, pw->pw_gid) || setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
fprintf(stderr, "Aborting, failed dropping privileges to "
"(UID:%d GID:%d): %s\n",
pw->pw_uid, pw->pw_gid, strerror(errno));
_exit(1);
}
execvp(argv[0], argv);
_exit(127);
}
while (waitpid(pid, &rc, 0) < 0) {
if (errno != EINTR)
return -1;
}
if (WIFEXITED(rc))
return WEXITSTATUS(rc);
if (WIFSIGNALED(rc)) {
errno = EINTR;
return -1;
}
return -1;
}
/*
* Shell command execution - only use with hardcoded commands or when
* shell features (pipes, redirects) are needed. Never use with user input.
@@ -304,7 +354,7 @@ int infix_shell(kcontext_t *ctx)
};
pw = getpwnam(user);
if (setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
if (initgroups(user, pw->pw_gid) || setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
fprintf(stderr, "Aborting, failed dropping privileges to (UID:%d GID:%d): %s\n",
pw->pw_uid, pw->pw_gid, strerror(errno));
_exit(1);
@@ -313,8 +363,10 @@ int infix_shell(kcontext_t *ctx)
_exit(execvp(args[0], args));
}
while (waitpid(pid, &rc, 0) != pid)
;
while (waitpid(pid, &rc, 0) < 0) {
if (errno != EINTR)
return -1;
}
if (WIFEXITED(rc))
rc = WEXITSTATUS(rc);
@@ -427,6 +479,192 @@ int infix_asym_keys(kcontext_t *ctx)
"| jq -r '.\"ietf-keystore:keystore\".\"asymmetric-keys\".\"asymmetric-key\"[].name'");
}
/*
* Create ~/.ssh/known_hosts with correct ownership if it doesn't exist.
* Must be called as root before dropping privileges, since ~/.ssh/ is
* owned by root on Infix (the system manages authorized_keys via YANG).
* Once the file exists with the user's ownership, ssh(1) can update it.
*/
static void ensure_known_hosts(const struct passwd *pw)
{
char path[512];
int fd;
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0600);
if (fd < 0)
return; /* Already exists, or unrecoverable error */
fchown(fd, pw->pw_uid, pw->pw_gid);
close(fd);
}
int infix_ssh_connect(kcontext_t *ctx)
{
kpargv_t *pargv = kcontext_pargv(ctx);
const char *host, *ruser, *port, *user;
struct passwd *pw;
kparg_t *parg;
char *argv[8];
int i = 0;
host = kparg_value(kpargv_find(pargv, "host"));
parg = kpargv_find(pargv, "user");
ruser = parg ? kparg_value(parg) : NULL;
parg = kpargv_find(pargv, "port");
port = parg ? kparg_value(parg) : NULL;
if (!host) {
fprintf(stderr, ERRMSG "missing host argument.\n");
return -1;
}
user = cd_home(ctx);
pw = getpwnam(user);
if (pw)
ensure_known_hosts(pw);
argv[i++] = "ssh";
if (ruser) { argv[i++] = "-l"; argv[i++] = (char *)ruser; }
if (port) { argv[i++] = "-p"; argv[i++] = (char *)port; }
argv[i++] = (char *)host;
argv[i] = NULL;
return run_as_user(user, argv);
}
/*
* Completion: list hostnames from the current user's ~/.ssh/known_hosts.
*/
int infix_ssh_known_hosts(kcontext_t *ctx)
{
char path[512], line[4096];
const char *user;
struct passwd *pw;
FILE *f;
user = cd_home(ctx);
pw = getpwnam(user);
if (!pw)
return 0;
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
f = fopen(path, "r");
if (!f)
return 0;
while (fgets(line, sizeof(line), f)) {
char *sp;
/* Skip comments, blank lines, and hashed entries */
if (line[0] == '#' || line[0] == '\n' || line[0] == '|')
continue;
sp = strchr(line, ' ');
if (!sp)
continue;
*sp = '\0';
/* Entries may be comma-separated: "host,ip algo key" */
for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ","))
puts(tok);
}
fclose(f);
return 0;
}
/*
* Pre-enroll a host public key received out-of-band into ~/.ssh/known_hosts.
* Runs as the CLI user to ensure correct file ownership.
*/
int infix_ssh_add_known_host(kcontext_t *ctx)
{
kpargv_t *pargv = kcontext_pargv(ctx);
const char *host, *keytype, *pubkey, *user;
char path[512];
struct passwd *pw;
pid_t pid;
int rc;
host = kparg_value(kpargv_find(pargv, "host"));
keytype = kparg_value(kpargv_find(pargv, "keytype"));
pubkey = kparg_value(kpargv_find(pargv, "pubkey"));
if (!host || !keytype || !pubkey) {
fprintf(stderr, ERRMSG "missing arguments.\n");
return -1;
}
user = cd_home(ctx);
pw = getpwnam(user);
if (!pw) {
fprintf(stderr, ERRMSG "unknown user: %s\n", user);
return -1;
}
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
ensure_known_hosts(pw);
pid = fork();
if (pid == -1)
return -1;
if (!pid) {
FILE *f;
if (setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
fprintf(stderr, "Aborting, failed dropping privileges: %s\n",
strerror(errno));
_exit(1);
}
f = fopen(path, "a");
if (!f) {
fprintf(stderr, ERRMSG "cannot open %s: %s\n", path, strerror(errno));
_exit(1);
}
fprintf(f, "%s %s %s\n", host, keytype, pubkey);
fclose(f);
printf("Host %s added to %s\n", host, path);
_exit(0);
}
while (waitpid(pid, &rc, 0) != pid)
;
if (WIFEXITED(rc))
return WEXITSTATUS(rc);
if (WIFSIGNALED(rc)) {
errno = EINTR;
return -1;
}
return -1;
}
int infix_ssh_remove_known_host(kcontext_t *ctx)
{
kpargv_t *pargv = kcontext_pargv(ctx);
const char *host;
char *argv[4];
host = kparg_value(kpargv_find(pargv, "host"));
if (!host) {
fprintf(stderr, ERRMSG "missing host argument.\n");
return -1;
}
argv[0] = "ssh-keygen";
argv[1] = "-R";
argv[2] = (char *)host;
argv[3] = NULL;
return run_as_user(cd_home(ctx), argv);
}
int kplugin_infix_fini(kcontext_t *ctx)
{
(void)ctx;
@@ -453,6 +691,10 @@ int kplugin_infix_init(kcontext_t *ctx)
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
kplugin_add_syms(plugin, ksym_new("set_boot_order", infix_set_boot_order));
kplugin_add_syms(plugin, ksym_new("shell", infix_shell));
kplugin_add_syms(plugin, ksym_new("ssh_connect", infix_ssh_connect));
kplugin_add_syms(plugin, ksym_new("ssh_known_hosts", infix_ssh_known_hosts));
kplugin_add_syms(plugin, ksym_new("ssh_add_known_host", infix_ssh_add_known_host));
kplugin_add_syms(plugin, ksym_new("ssh_remove_known_host", infix_ssh_remove_known_host));
return 0;
}
+53
View File
@@ -169,6 +169,24 @@
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="SSH_KNOWN_HOSTS">
<COMPL>
<ACTION sym="ssh_known_hosts@infix"/>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="SSH_KEY_TYPE">
<COMPL>
<ACTION sym="printl">ecdsa-sha2-nistp256</ACTION>
<ACTION sym="printl">ecdsa-sha2-nistp384</ACTION>
<ACTION sym="printl">ecdsa-sha2-nistp521</ACTION>
<ACTION sym="printl">ssh-ed25519</ACTION>
<ACTION sym="printl">ssh-rsa</ACTION>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<VIEW name="main">
<HOTKEY key="^D" cmd="exit"/>
@@ -300,6 +318,15 @@ echo "Public: $pub"
<PARAM name="third" ptype="/BOOT_TARGET" help="Third boot target" min="0"/>
<ACTION sym="set_boot_order@infix"/>
</COMMAND>
<COMMAND name="ssh" help="SSH client settings" mode="switch">
<COMMAND name="known-hosts" help="Pre-enroll a host public key received out-of-band">
<PARAM name="host" ptype="/STRING" help="Hostname or IP address"/>
<PARAM name="keytype" ptype="/SSH_KEY_TYPE" help="Public key algorithm"/>
<PARAM name="pubkey" ptype="/STRING" help="Base64-encoded public key"/>
<ACTION sym="ssh_add_known_host@infix"/>
</COMMAND>
</COMMAND>
</COMMAND>
<COMMAND name="dhcp-server" help="DHCP server tools" mode="switch">
@@ -529,6 +556,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
@@ -871,6 +902,28 @@ echo "Public: $pub"
</ACTION>
</COMMAND>
<COMMAND name="ssh" help="Connect to a remote device over SSH">
<SWITCH name="optional" min="0" max="2">
<COMMAND name="user" help="Login with a different username">
<PARAM name="user" ptype="/STRING" help="Remote username"/>
</COMMAND>
<COMMAND name="port" help="Connect to a non-standard port">
<PARAM name="port" ptype="/UINT" help="TCP port number"/>
</COMMAND>
</SWITCH>
<PARAM name="host" ptype="/STRING" help="Hostname or IP address"/>
<ACTION sym="ssh_connect@infix" in="tty" out="tty" interrupt="true"/>
</COMMAND>
<COMMAND name="no" help="Remove a setting or entry" mode="switch">
<COMMAND name="ssh" help="SSH client settings" mode="switch">
<COMMAND name="known-hosts" help="Remove a host from the SSH known_hosts file">
<PARAM name="host" ptype="/SSH_KNOWN_HOSTS" help="Hostname or IP to remove"/>
<ACTION sym="ssh_remove_known_host@infix"/>
</COMMAND>
</COMMAND>
</COMMAND>
<COMMAND name="upgrade" help="Install a software update bundle from remote or local file">
<PARAM name="URI" ptype="/STRING" help="[(ftp|tftp|http|https|sftp)://(dns.name | ip.address)/path/to/]upgrade-bundle.pkg"/>
<SWITCH name="optional" min="0" max="2">
+1 -1
View File
@@ -1 +1 @@
__pycache__/
netbrowse
-4
View File
@@ -1,4 +0,0 @@
include LICENSE
include README.md
recursive-include netbrowse/templates *
recursive-include netbrowse/static *
-34
View File
@@ -1,34 +0,0 @@
mDNS Network Browser
====================
This program is a Python Flask app that provides an mDNS browser for,
e.g., Nginx. It is intended to answer calls to https://network.local
A UNIX socket, for fastcgi, is created in `/tmp/netbrowse.sock` with
permissions 0660 as the user and group the program is started as.
When using Finit this can be achieved with
service @www-data:www-data netbrowse network.local
In your Nginx server configuration, add:
location /browse {
include fastcgi_params;
fastcgi_pass unix:/tmp/netbrowse.sock;
}
For more a elaborate setup, you can have another server block:
```
server {
listen 80;
listen [::]:80;
server_name network.local;
location / {
include fastcgi_params;
fastcgi_pass unix:/tmp/netbrowse.sock;
}
}
```
+247
View File
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: MIT
package main
import (
"log"
"os/exec"
"sort"
"strings"
)
// Service represents a single mDNS service advertised by a host.
type Service struct {
Type string `json:"type"`
Name string `json:"name"`
URL string `json:"url"`
}
// Host groups all services for one mDNS host with its metadata.
type Host struct {
Addr string `json:"addr"`
Product string `json:"product,omitempty"`
Version string `json:"version,omitempty"`
Other bool `json:"other"`
Svcs []Service `json:"svcs"`
}
type serviceInfo struct {
displayName string
urlTemplate string // empty means no URL
}
var knownServices = map[string]serviceInfo{
"_http._tcp": {"HTTP", "http://{address}:{port}{path}"},
"_https._tcp": {"HTTPS", "https://{address}:{port}{path}"},
"_netconf-ssh._tcp": {"NETCONF", ""},
"_restconf-tls._tcp": {"RESTCONF", ""},
"_ssh._tcp": {"SSH", ""},
"_sftp-ssh._tcp": {"SFTP", ""},
}
var typeOrder = map[string]int{
"HTTPS": 1, "HTTP": 2, "SSH": 3, "SFTP": 4,
}
// hasK checks whether avahi-browse supports the -k flag.
func hasK() bool {
out, err := exec.Command("avahi-browse", "--help").CombinedOutput()
if err != nil {
return false
}
return strings.Contains(string(out), "-k")
}
// scan runs avahi-browse, parses the output, and returns discovered
// hosts with their services and metadata.
func scan() map[string]Host {
args := "-tarp"
if hasK() {
args += "k"
}
out, err := exec.Command("avahi-browse", args).Output()
if err != nil {
log.Printf("avahi-browse: %v", err)
return nil
}
svcsMap := make(map[string][]Service)
addrMap := make(map[string]string) // preferred IP per host (IPv4 wins)
productMap := make(map[string]string)
versionMap := make(map[string]string)
vvHosts := make(map[string]bool) // has vv=1 TXT record
legHosts := make(map[string]bool) // has on=Infix TXT record (legacy)
mgmtHosts := make(map[string]bool) // has at least one management service type
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line == "" {
continue
}
parts := strings.Split(line, ";")
if len(parts) <= 9 || parts[0] != "=" {
continue
}
family := parts[2]
serviceName := parts[3]
serviceType := parts[4]
link := parts[6]
address := parts[7]
port := parts[8]
txt := parts[9]
if family != "IPv4" && family != "IPv6" {
continue
}
info, known := knownServices[serviceType]
displayName := info.displayName
urlTemplate := info.urlTemplate
if !known {
displayName = serviceType
}
// vv=1 is the platform marker set by confd/services.c, survives OS
// rebranding. We require it together with a management service type
// (ssh, web, netconf, restconf) to avoid false positives from Apple
// devices, which also use vv=1 in their AirPlay/RAOP TXT records.
// on=Infix is kept as a fallback for older firmware predating vv=1.
if known {
mgmtHosts[link] = true
}
// Prefer real IPv4; skip loopback and link-local.
// Loopback (127.x / ::1) appears when avahi resolves local-machine
// services from the same host — the address is useless for display.
isLoopback := address == "127.0.0.1" || address == "::1" || strings.HasPrefix(address, "127.")
isLinkLocal := strings.HasPrefix(address, "fe80:")
if !isLoopback && !isLinkLocal {
if family == "IPv4" {
addrMap[link] = address // IPv4 always wins
} else if addrMap[link] == "" {
addrMap[link] = address // IPv6 fallback
}
}
// Parse TXT records.
// avahi-browse -p quotes records containing spaces, e.g.
// "ty=Brother DCP-L3550CDW series" "adminurl=http://..."
// Split on the between-record boundary `" "` (close-quote space
// open-quote) to keep each record intact, then trim outer quotes.
var path, adminurl, product, version string
for _, record := range strings.Split(txt, "\" \"") {
stripped := strings.Trim(record, "\"")
switch {
case stripped == "vv=1":
vvHosts[link] = true
case stripped == "on=Infix":
legHosts[link] = true
case path == "" && strings.HasPrefix(stripped, "path="):
path = stripped[5:]
case adminurl == "" && strings.HasPrefix(stripped, "adminurl="):
adminurl = stripped[9:]
case product == "" && strings.HasPrefix(stripped, "product="):
product = stripped[8:]
case version == "" && strings.HasPrefix(stripped, "ov="):
version = stripped[3:]
}
}
// IPP/Bonjour printers encode product as "(Name)" — strip the parens.
product = strings.TrimPrefix(strings.TrimSuffix(product, ")"), "(")
if product != "" && productMap[link] == "" {
productMap[link] = product
}
if version != "" && versionMap[link] == "" {
versionMap[link] = version
}
var url string
if adminurl != "" {
url = adminurl
} else if urlTemplate != "" {
url = strings.NewReplacer(
"{address}", address,
"{port}", port,
"{path}", path,
).Replace(urlTemplate)
}
svc := Service{
Type: displayName,
Name: decode(serviceName),
URL: url,
}
// Deduplicate
dup := false
for _, existing := range svcsMap[link] {
if existing.Type == svc.Type && existing.Name == svc.Name && existing.URL == svc.URL {
dup = true
break
}
}
if !dup {
svcsMap[link] = append(svcsMap[link], svc)
}
}
// Sort services per host
for link := range svcsMap {
sort.SliceStable(svcsMap[link], func(i, j int) bool {
oi := typeOrder[svcsMap[link][i].Type]
oj := typeOrder[svcsMap[link][j].Type]
if oi == 0 {
oi = 999
}
if oj == 0 {
oj = 999
}
return oi < oj
})
}
// Build final host map. Default view shows only Infix devices: a host
// qualifies if it has vv=1 on a management service (to exclude Apple
// AirPlay collisions), or on=Infix for older firmware predating vv=1.
hosts := make(map[string]Host)
for link, svcs := range svcsMap {
isInfix := (vvHosts[link] && mgmtHosts[link]) || legHosts[link]
hosts[link] = Host{
Addr: addrMap[link],
Product: productMap[link],
Version: versionMap[link],
Other: !isInfix,
Svcs: svcs,
}
}
return hosts
}
// decode handles avahi's DNS-SD escape sequences in service names:
// - \DDD decimal value 0-255, e.g. \058 → ':'
// - \X literal character X, e.g. \. → '.'
func decode(name string) string {
var b strings.Builder
for i := 0; i < len(name); i++ {
if name[i] != '\\' || i+1 >= len(name) {
b.WriteByte(name[i])
continue
}
if i+3 < len(name) &&
name[i+1] >= '0' && name[i+1] <= '9' &&
name[i+2] >= '0' && name[i+2] <= '9' &&
name[i+3] >= '0' && name[i+3] <= '9' {
val := int(name[i+1]-'0')*100 + int(name[i+2]-'0')*10 + int(name[i+3]-'0')
if val <= 255 {
b.WriteByte(byte(val))
i += 3
continue
}
}
// \X where X is not a 3-digit decimal: output X literally
b.WriteByte(name[i+1])
i++
}
return b.String()
}
+541
View File
@@ -0,0 +1,541 @@
<!DOCTYPE html>
<html data-theme="system">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>mDNS Browser</title>
<style>
/* IBM Plex Mono — latin subset, weights 400/500/600
* Copyright © 2017 IBM Corp. (Reserved Font Name "Plex")
* Licensed under the SIL Open Font License 1.1
* Source: https://github.com/IBM/plex v20 via fonts.gstatic.com */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal; font-weight: 400; font-display: swap;
src: url('/static/fonts/IBMPlexMono-Regular.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal; font-weight: 500; font-display: swap;
src: url('/static/fonts/IBMPlexMono-Medium.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal; font-weight: 600; font-display: swap;
src: url('/static/fonts/IBMPlexMono-SemiBold.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
</style>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* Light theme (default) */
--bg: #f2f0ec;
--surface: #ffffff;
--border: #e2ddd6;
--text: #1c1917;
--text-dim: #78716c;
--accent: #1d4ed8;
--accent-bg: #eff6ff;
--hdr: rgba(242,240,236,.92);
--shadow: 0 1px 2px rgba(0,0,0,.06), 0 3px 8px rgba(0,0,0,.04);
--shadow-h: 0 2px 6px rgba(0,0,0,.10), 0 8px 20px rgba(0,0,0,.06);
/* Badges: light */
--c-https: #15803d; --bg-https: #dcfce7;
--c-http: #0e7490; --bg-http: #cffafe;
--c-ssh: #1e40af; --bg-ssh: #dbeafe;
--c-sftp: #6d28d9; --bg-sftp: #ede9fe;
--c-netconf: #b45309; --bg-netconf: #fef3c7;
--c-restconf: #c2410c; --bg-restconf: #ffedd5;
--c-other: #57534e; --bg-other: #f5f5f4;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #09111f;
--surface: #0f1a2e;
--border: #1a2840;
--text: #c2cede;
--text-dim: #4e6070;
--accent: #60a5fa;
--accent-bg: #0c1f3a;
--hdr: rgba(9,17,31,.92);
--shadow: 0 1px 3px rgba(0,0,0,.40), 0 4px 12px rgba(0,0,0,.25);
--shadow-h: 0 2px 8px rgba(0,0,0,.50), 0 8px 24px rgba(0,0,0,.35);
/* Badges: dark */
--c-https: #4ade80; --bg-https: rgba(74,222,128,.10);
--c-http: #22d3ee; --bg-http: rgba(34,211,238,.10);
--c-ssh: #93c5fd; --bg-ssh: rgba(147,197,253,.10);
--c-sftp: #c4b5fd; --bg-sftp: rgba(196,181,253,.10);
--c-netconf: #fcd34d; --bg-netconf: rgba(252,211,77,.10);
--c-restconf: #fb923c; --bg-restconf: rgba(251,146,60,.10);
--c-other: #a8a29e; --bg-other: rgba(168,162,158,.08);
}
}
html[data-theme="dark"] {
--bg: #09111f; --surface: #0f1a2e; --border: #1a2840;
--text: #c2cede; --text-dim: #4e6070; --accent: #60a5fa;
--accent-bg: #0c1f3a; --hdr: rgba(9,17,31,.92);
--shadow: 0 1px 3px rgba(0,0,0,.40), 0 4px 12px rgba(0,0,0,.25);
--shadow-h: 0 2px 8px rgba(0,0,0,.50), 0 8px 24px rgba(0,0,0,.35);
--c-https: #4ade80; --bg-https: rgba(74,222,128,.10);
--c-http: #22d3ee; --bg-http: rgba(34,211,238,.10);
--c-ssh: #93c5fd; --bg-ssh: rgba(147,197,253,.10);
--c-sftp: #c4b5fd; --bg-sftp: rgba(196,181,253,.10);
--c-netconf: #fcd34d; --bg-netconf: rgba(252,211,77,.10);
--c-restconf: #fb923c; --bg-restconf: rgba(251,146,60,.10);
--c-other: #a8a29e; --bg-other: rgba(168,162,158,.08);
}
html[data-theme="light"] {
--bg: #f2f0ec; --surface: #ffffff; --border: #e2ddd6;
--text: #1c1917; --text-dim: #78716c; --accent: #1d4ed8;
--accent-bg: #eff6ff; --hdr: rgba(242,240,236,.92);
--shadow: 0 1px 2px rgba(0,0,0,.06), 0 3px 8px rgba(0,0,0,.04);
--shadow-h: 0 2px 6px rgba(0,0,0,.10), 0 8px 20px rgba(0,0,0,.06);
--c-https: #15803d; --bg-https: #dcfce7;
--c-http: #0e7490; --bg-http: #cffafe;
--c-ssh: #1e40af; --bg-ssh: #dbeafe;
--c-sftp: #6d28d9; --bg-sftp: #ede9fe;
--c-netconf: #b45309; --bg-netconf: #fef3c7;
--c-restconf: #c2410c; --bg-restconf: #ffedd5;
--c-other: #57534e; --bg-other: #f5f5f4;
}
html, body {
min-height: 100%;
font-family: 'IBM Plex Mono', 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
background: var(--bg);
color: var(--text);
transition: background .2s, color .2s;
}
/* ── Header ─────────────────────────────────── */
header {
position: sticky; top: 0; z-index: 100;
background: var(--hdr);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
}
.hdr-inner {
max-width: 1400px; margin: 0 auto;
padding: 10px 20px;
display: flex; align-items: center; gap: 12px;
}
.brand {
font-size: 14px; font-weight: 600; letter-spacing: .03em;
color: var(--text); white-space: nowrap; flex-shrink: 0;
user-select: none;
}
.brand em { color: var(--accent); font-style: normal; }
.search-wrap { flex: 1; min-width: 120px; }
#search {
width: 100%; padding: 6px 10px;
font-family: inherit; font-size: 12px;
background: var(--surface); color: var(--text);
border: 1px solid var(--border); border-radius: 5px;
outline: none;
transition: border-color .15s, box-shadow .15s;
appearance: none; -webkit-appearance: none;
}
#search::placeholder { color: var(--text-dim); }
#search:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-bg);
}
#search::-webkit-search-cancel-button,
#search::-webkit-search-decoration { display: none; }
.controls { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.btn {
display: inline-flex; align-items: center; gap: 4px;
padding: 5px 10px;
font-family: inherit; font-size: 11px; font-weight: 500;
background: var(--surface); color: var(--text-dim);
border: 1px solid var(--border); border-radius: 5px;
cursor: pointer; white-space: nowrap;
transition: color .15s, background .15s, border-color .15s;
user-select: none; line-height: 1.4;
}
.btn:hover { color: var(--text); border-color: var(--text-dim); }
.btn[aria-pressed="true"] {
background: var(--accent-bg); color: var(--accent);
border-color: var(--accent);
}
.btn.icon { padding: 5px 8px; }
#cdspan { font-size: 10px; opacity: .75; min-width: 2.4em; }
/* ── Grid ───────────────────────────────────── */
main {
max-width: 1400px; margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
gap: 14px; align-items: start;
}
/* ── Card ───────────────────────────────────── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
overflow: hidden;
transition: transform .15s ease, box-shadow .15s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-h);
}
.card-host {
padding: 12px 14px 10px;
font-size: 12px; font-weight: 600;
color: var(--text);
border-bottom: 1px solid var(--border);
word-break: break-all;
}
.card-addr {
font-weight: 400;
color: var(--text-dim);
font-size: 11px;
}
.card-meta {
padding: 5px 14px 6px;
font-size: 11px; color: var(--text-dim);
border-bottom: 1px solid var(--border);
}
.card-svcs { padding: 6px 0; }
.svc {
display: flex; align-items: center; gap: 8px;
padding: 5px 14px;
}
.svc a {
display: flex; align-items: center; gap: 8px;
text-decoration: none; color: inherit; width: 100%;
}
.svc a:hover .svc-name { color: var(--accent); }
.svc a:hover .arrow { opacity: 1; }
.badge {
flex-shrink: 0;
display: inline-block;
padding: 2px 6px; border-radius: 3px;
font-size: 9px; font-weight: 600; letter-spacing: .05em;
text-transform: uppercase;
min-width: 64px; text-align: center;
}
.b-https { color: var(--c-https); background: var(--bg-https); }
.b-http { color: var(--c-http); background: var(--bg-http); }
.b-ssh { color: var(--c-ssh); background: var(--bg-ssh); }
.b-sftp { color: var(--c-sftp); background: var(--bg-sftp); }
.b-netconf { color: var(--c-netconf); background: var(--bg-netconf); }
.b-restconf { color: var(--c-restconf); background: var(--bg-restconf); }
.b-other { color: var(--c-other); background: var(--bg-other); }
.svc-name {
flex: 1; min-width: 0;
font-size: 11px; color: var(--text-dim);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
transition: color .12s;
}
.arrow {
flex-shrink: 0; font-size: 10px; color: var(--accent);
opacity: 0; transition: opacity .12s;
}
/* ── Empty / error states ───────────────────── */
.state {
grid-column: 1 / -1;
padding: 60px 20px; text-align: center;
color: var(--text-dim); line-height: 1.8;
}
.state.err { color: #ef4444; }
.state p + p { margin-top: 4px; }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 22px; height: 22px; margin: 0 auto 16px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin .65s linear infinite;
}
/* ── Footer ─────────────────────────────────── */
footer {
max-width: 1400px; margin: 0 auto;
padding: 12px 20px 28px;
font-size: 11px; color: var(--text-dim); text-align: center;
}
@media (max-width: 560px) {
.hdr-inner { flex-wrap: wrap; gap: 8px; }
.brand { order: 0; }
.search-wrap { order: 2; flex-basis: 100%; }
.controls { order: 1; margin-left: auto; }
main { padding: 12px; gap: 10px; grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header>
<div class="hdr-inner">
<div class="brand">mDNS<em>·</em>browser</div>
<div class="search-wrap">
<input type="search" id="search" placeholder="Search… (press /)" autocomplete="off" spellcheck="false">
</div>
<div class="controls">
<button class="btn icon" id="btn-refresh" title="Refresh now"></button>
<button class="btn" id="btn-all" aria-pressed="false" title="Show all mDNS devices, not just Infix">All</button>
<button class="btn" id="btn-auto" aria-pressed="false" title="Toggle auto-refresh every 30 s">Auto<span id="cdspan"></span></button>
<button class="btn icon" id="btn-theme" title="Cycle theme: system → dark → light"></button>
</div>
</div>
</header>
<main id="grid">
<div class="state"><div class="spinner"></div><p>Scanning…</p></div>
</main>
<footer id="foot"></footer>
<script>(function () {
// ── State ──────────────────────────────────────────────────────────────────
var showAll = false;
var autoOn = false;
var cdCount = 30;
var cdTimer = null;
var query = '';
var themeMode = localStorage.getItem('theme') || 'system';
var lastData = null;
var lastTs = null;
// ── DOM refs ───────────────────────────────────────────────────────────────
var grid = document.getElementById('grid');
var foot = document.getElementById('foot');
var searchEl = document.getElementById('search');
var btnAll = document.getElementById('btn-all');
var btnAuto = document.getElementById('btn-auto');
var cdspan = document.getElementById('cdspan');
var btnTheme = document.getElementById('btn-theme');
var btnRefresh = document.getElementById('btn-refresh');
// ── Theme ──────────────────────────────────────────────────────────────────
var THEME_ICONS = { system: '◐', dark: '☽', light: '☀' };
var THEME_ORDER = ['system', 'dark', 'light'];
function applyTheme() {
document.documentElement.setAttribute('data-theme', themeMode);
btnTheme.textContent = THEME_ICONS[themeMode];
localStorage.setItem('theme', themeMode);
}
btnTheme.addEventListener('click', function () {
var i = THEME_ORDER.indexOf(themeMode);
themeMode = THEME_ORDER[(i + 1) % THEME_ORDER.length];
applyTheme();
});
// ── Data ───────────────────────────────────────────────────────────────────
function fetchData() {
fetch('/data')
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
lastData = data || {};
lastTs = new Date();
render();
})
.catch(function (e) {
grid.innerHTML = '<div class="state err"><p>Error: ' + esc(e.message) + '</p></div>';
foot.textContent = '';
});
}
// ── Render ─────────────────────────────────────────────────────────────────
var BADGE_CLASS = {
'HTTPS': 'b-https',
'HTTP': 'b-http',
'SSH': 'b-ssh',
'SFTP': 'b-sftp',
'NETCONF': 'b-netconf',
'RESTCONF': 'b-restconf',
};
function render() {
var q = query.toLowerCase();
var entries = Object.entries(lastData);
// Devices to show based on the All toggle
var inScope = entries.filter(function (e) {
return showAll || !e[1].other;
});
// Further filtered by the search query
var visible = inScope.filter(function (e) {
if (!q) return true;
var h = e[1];
var text = e[0] + ' ' + (h.addr || '') + ' ' + (h.product || '') + ' ' + (h.version || '');
h.svcs.forEach(function (s) { text += ' ' + s.type + ' ' + s.name; });
return text.toLowerCase().indexOf(q) !== -1;
});
if (visible.length === 0) {
var msg = (inScope.length === 0)
? '<p>No devices found.</p><p>Make sure avahi-browse is installed and running.</p>'
: '<p>No results for <strong>' + esc(query) + '</strong>.</p>';
grid.innerHTML = '<div class="state">' + msg + '</div>';
setFooter(0, inScope.length, entries.length);
return;
}
visible.sort(function (a, b) { return a[0].localeCompare(b[0]); });
grid.innerHTML = visible.map(function (e) {
return makeCard(e[0], e[1]);
}).join('');
setFooter(visible.length, inScope.length, entries.length);
}
function makeCard(host, h) {
var rows = h.svcs.map(function (s) {
var cls = BADGE_CLASS[s.type] || 'b-other';
var badge = '<span class="badge ' + cls + '">' + esc(s.type) + '</span>';
var name = '<span class="svc-name">' + esc(s.name) + '</span>';
if (s.url) {
return '<div class="svc"><a href="' + esc(s.url) + '" target="_blank" rel="noopener noreferrer">'
+ badge + name + '<span class="arrow">↗</span></a></div>';
}
return '<div class="svc">' + badge + name + '</div>';
}).join('');
var hdrText = esc(host);
if (h.addr) hdrText += ' <span class="card-addr">(' + esc(h.addr) + ')</span>';
var meta = '';
if (h.product || h.version) {
var parts = [];
if (h.product) parts.push(esc(h.product));
if (h.version) parts.push(esc(h.version));
meta = '<div class="card-meta">' + parts.join(' ') + '</div>';
}
return '<div class="card"><div class="card-host">' + hdrText + '</div>'
+ meta
+ '<div class="card-svcs">' + rows + '</div></div>';
}
function setFooter(visible, inScope, total) {
var label = inScope + ' device' + (inScope !== 1 ? 's' : '');
if (visible < inScope) {
label = visible + ' of ' + label;
}
if (!showAll && total > inScope) {
label += ' (+' + (total - inScope) + ' hidden)';
}
var parts = [label];
if (lastTs) {
parts.push('updated ' + lastTs.toLocaleTimeString());
}
foot.textContent = parts.join(' · ');
}
function esc(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ── Search ─────────────────────────────────────────────────────────────────
searchEl.addEventListener('input', function () {
query = searchEl.value;
if (lastData) render();
});
// ── Show all ───────────────────────────────────────────────────────────────
btnAll.addEventListener('click', function () {
showAll = !showAll;
btnAll.setAttribute('aria-pressed', showAll);
if (lastData) render();
});
// ── Auto-refresh ───────────────────────────────────────────────────────────
function startCd() {
cdCount = 30;
cdspan.textContent = ' ' + cdCount + 's';
cdTimer = setInterval(function () {
cdCount--;
if (cdCount <= 0) {
cdCount = 30;
fetchData();
}
cdspan.textContent = ' ' + cdCount + 's';
}, 1000);
}
function stopCd() {
clearInterval(cdTimer);
cdspan.textContent = '';
}
btnAuto.addEventListener('click', function () {
autoOn = !autoOn;
btnAuto.setAttribute('aria-pressed', autoOn);
autoOn ? startCd() : stopCd();
});
// ── Manual refresh ─────────────────────────────────────────────────────────
btnRefresh.addEventListener('click', function () {
if (autoOn) {
cdCount = 30;
cdspan.textContent = ' ' + cdCount + 's';
}
fetchData();
});
// ── Keyboard shortcuts ─────────────────────────────────────────────────────
document.addEventListener('keydown', function (e) {
if (e.target === searchEl) {
if (e.key === 'Escape') searchEl.blur();
return;
}
if (e.key === '/') {
e.preventDefault();
searchEl.focus();
searchEl.select();
}
});
// ── Init ───────────────────────────────────────────────────────────────────
applyTheme();
fetchData();
}());</script>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
module github.com/kernelkit/infix/src/netbrowse
go 1.21
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
package main
import (
"context"
"embed"
"encoding/json"
"flag"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
//go:embed browse.html
var browseHTML []byte
//go:embed static
var staticFS embed.FS
func browseHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(browseHTML)
}
func dataHandler(w http.ResponseWriter, r *http.Request) {
hosts := scan()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
if err := json.NewEncoder(w).Encode(hosts); err != nil {
log.Printf("json: %v", err)
}
}
func main() {
listen := flag.String("l", "127.0.0.1:8000", "listen address:port")
flag.Parse()
mux := http.NewServeMux()
mux.HandleFunc("/", browseHandler)
mux.HandleFunc("/netbrowse", browseHandler)
mux.HandleFunc("/data", dataHandler)
mux.HandleFunc("/netbrowse/data", dataHandler)
staticSub, err := fs.Sub(staticFS, "static")
if err != nil {
log.Fatal(err)
}
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
srv := &http.Server{Addr: *listen, Handler: mux}
go func() {
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, syscall.SIGTERM, syscall.SIGINT)
<-sigch
log.Println("shutting down")
srv.Shutdown(context.Background())
}()
log.Printf("listening on %s", *listen)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env python3
"""
Very basic mDNS scanner with HTML table renderer
"""
from flask import Flask, render_template
from .mdns_hosts import MdnsHosts
__all__ = ['MdnsHosts']
app = Flask(__name__)
@app.route('/')
@app.route('/netbrowse')
def index():
"""The /browse or network.local application"""
mdns_hosts = MdnsHosts()
hosts_services = mdns_hosts.scan()
order = {'HTTPS': 1, 'HTTP': 2, 'SSH': 3, 'SFTP': 4}
for _, details in hosts_services.items():
details['services'].sort(key=lambda x: order.get(x['type'], 999))
return render_template('browse.html', hosts_services=hosts_services)
def main():
"""Stand-alone running with Flask development server."""
try:
app.run(debug=True)
except KeyboardInterrupt:
print("Exiting")
if __name__ == '__main__':
main()
-97
View File
@@ -1,97 +0,0 @@
"""
Very basic mDNS scanner with HTML table renderer
"""
import subprocess
class MdnsHosts:
"""mDNS scanner class using avahi-browse"""
def hask(self):
"""Check if avahi-browse has -k option"""
try:
result = subprocess.run(['avahi-browse', '--help'], check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return '-k' in result.stdout
except subprocess.CalledProcessError:
return False
def scan(self):
"""Perform mDNS scan and return list of hosts."""
services = {
'_http._tcp': ('HTTP', 'http://{address}:{port}{path}'),
'_https._tcp': ('HTTPS', 'https://{address}:{port}{path}'),
'_netconf-ssh._tcp': ('NETCONF', None),
'_restconf-tls._tcp': ('RESTCONF', None),
'_ssh._tcp': ('SSH', None),
'_sftp-ssh._tcp': ('SFTP', None),
}
result = subprocess.run(['avahi-browse', '-tarpk' if self.hask() else '-tarp'],
stdout=subprocess.PIPE, text=True)
lines = result.stdout.strip().split('\n')
hosts_services = {}
for line in lines:
# print(f"{line}")
parts = line.split(';')
if len(parts) <= 8 or parts[0] != '=':
continue
family = parts[2]
service_name = parts[3]
service_type = parts[4]
link = parts[6]
address = parts[7]
port = parts[8]
txt = parts[9]
adminurl = None
if family not in ('IPv4', 'IPv6'):
continue
if service_type in services:
identifier, url_template = services[service_type]
other = False
else:
identifier = service_type
url_template = None
other = True
path = ""
records = txt.split(' ')
for record in records:
stripped = record.strip("\"")
if "path=" in stripped:
path = stripped.split('path=')[-1]
break
if "adminurl=" in stripped:
adminurl = stripped.split('adminurl=')[-1]
break
if adminurl:
url = adminurl
elif url_template:
url = url_template.format(address=address, port=port, path=path)
else:
url = None
service_details = {
'type': identifier,
'name': self.decode(service_name),
'url': url,
'other': other
}
if link not in hosts_services:
hosts_services[link] = {'services': [service_details]}
elif service_details not in hosts_services[link]['services']:
hosts_services[link]['services'].append(service_details)
return hosts_services
def decode(self, name):
"""Decode escape sequences like \032 and \040 in service names"""
name = name.replace('\\032', ' ')
name = name.replace('\\040', '(')
name = name.replace('\\041', ')')
return bytes(name, "utf-8").decode("unicode_escape")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

@@ -1,196 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>mDNS Hosts - Services</title>
<style>
.container {
max-width: 1024px;
margin: auto;
padding: 0 0px;
overflow: auto;
}
.container h1 {
text-align: center;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
/* margin-top: 50px; */
/* margin-bottom: 20px; */
font-size: 32px;
color: #333;
padding: 20px; /* Add some padding around the text */
}
/* Container for the checkbox for better alignment */
.checkbox-container {
margin-bottom: 20px; /* Spacing between the checkbox and the table */
display: flex;
align-items: center; /* Vertically center the checkbox and label */
}
/* Styles for the label text for better readability */
.checkbox-container label {
margin-left: 5px;
font-size: 16px;
}
/* Custom checkbox styles if needed */
input[type="checkbox"] {
/* Custom styles here */
}
.container table {
background-color: rgba(255, 255, 255, 0.75);
/* background-color: white; */
}
.network-image {
display: block;
margin-left: auto;
margin-right: auto;
text-align: center;
width: 50%;
}
.detail-row { display: none; }
.other { display: none; }
.toggle-button {
cursor: pointer;
/* font-size: 20px; */
color: #333;
line-height: 1;
user-select: none;
transition: transform 0.3s ease;
}
html, body {
height: 100%;
font-family: Arial, sans-serif;
margin: 0; /* 20px */
padding: 0;
/* By kjpargeter https://www.freepik.com/free-vector/network-connections-background_2533758.htm */
background-image: url('{{ url_for("static", filename="images/2533758_8245.svg") }}');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
table {
border-collapse: collapse;
width: 100%;
margin: 20px auto;
border: 2px solid rgba(0, 0, 0, 0.1); /* Slight, semi-transparent border */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
table tr th:first-child, table tr td:first-child {
width: 1%; /* Minimize the width */
white-space: nowrap; /* Prevents content from wrapping */
padding: 10px; /* Adjust padding as needed */
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid #ddd;
}
th {
background-color: #e2e2e2;
}
tr:hover {
background-color: #d8d8d8;
}
</style>
</head>
<body>
<div class="container">
<!-- <input type="text" id="search" onkeyup="filter()" placeholder="Filter column ..."> -->
<div class="network-image">
<!-- Icon created by Ranah Pixel Studio -- https://www.flaticon.com/free-icons/computer -->
<!-- <img src="{{ url_for('static', filename='images/switch.png') }}" width="160px"> -->
</div>
<h1>mDNS Hosts and Services</h1>
<div class="checkbox-container">
<label for="showAll">
<input type="checkbox" id="showAll" name="showAll" onchange="toggleAll(this)">
Show all
</label>
</div>
<table id="main">
<thead>
<tr><th></th><th>Link</th><th>Name</th><th>Type</th></tr>
</thead>
<tbody>
{% for link, details in hosts_services.items() %}
{% for service in details.services %}
{% if loop.first %}
<tr class="device-row {{ 'other' if service.other else '' }}">
<td><span class="toggle-button" onclick="toggleFoldout('{{ link }}', event)">&#x25B6;</span></td>
{% else %}
<tr class="device-row detail-row" data-name="{{ link }}">
<td></td>
{% endif %}
{% if service.url %}
<td><a href="{{ service.url }}" target="_blank" rel="noopener noreferrer">{{ link }}</a></td>
{% else %}
<td>{{ link }}</td>
{% endif %}
<td>{{ service.name }}</td>
<td>{{ service.type }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('showAll').checked = false;
});
function filter() {
var input, filter, table, tr, tdaddr, tdserv, i, addr, serv;
input = document.getElementById("search");
filter = input.value.toUpperCase();
table = document.getElementById("main");
tr = table.getElementsByTagName("tr");
for (i = 1; i < tr.length; i++) {
tdaddr = tr[i].getElementsByTagName("td")[0];
tdserv = tr[i].getElementsByTagName("td")[1];
tdlink = tr[i].getElementsByTagName("td")[2];
tdtype = tr[i].getElementsByTagName("td")[3];
addr = tdaddr ? tdaddr.textContent || tdaddr.innerText : "";
serv = tdserv ? tdserv.textContent || tdserv.innerText : "";
link = tdlink ? tdlink.textContent || tdlink.innerText : "";
type = tdtype ? tdtype.textContent || tdtype.innerText : "";
if (addr.toUpperCase().indexOf(filter) > -1 ||
serv.toUpperCase().indexOf(filter) > -1 ||
link.toUpperCase().indexOf(filter) > -1 ||
type.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
function toggleFoldout(name, event) {
var isHidden = true;
var detailRows;
detailRows = document.querySelectorAll(`tr[data-name='${name}']`);
detailRows.forEach(row => {
if (row.style.display === 'none' || row.style.display === '') {
row.style.display = 'table-row';
isHidden = false;
} else {
row.style.display = 'none';
isHidden = true;
}
});
event.target.innerHTML = isHidden ? '&#x25B6;' : '&#x25BC;';
}
function toggleAll(checkbox) {
const allRows = document.querySelectorAll('.device-row');
const otherRows = document.querySelectorAll('.other');
allRows.forEach(row => {
if (row.classList.contains('other'))
row.style.display = checkbox.checked ? 'table-row' : 'none';
});
}
</script>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
# build-backend = "setuptools.build_meta"
[project]
name = "netbrowse"
version = "1.0"
description = "mDNS Service Browser"
authors = [{name = "Joachim Wiberg", email = "troglobit@gmail.com"}]
license = {file = "LICENSE"}
readme = "README.md"
dependencies = [
"flask",
]
[project.optional-dependencies]
dev = [
"pytest>=6.0",
"flake8>=3.8",
]
# [tool.setuptools.packages.find]
# where = ["netbrowse"]
# [tool.setuptools.package-data]
# netbrowse = ["templates/*.html", "static/*"]
[project.scripts]
netbrowse = "netbrowse:main"

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

-539
View File
@@ -1,539 +0,0 @@
mDNS Browsing Inventory
=======================
Results of investigation into mDNS TXT records being used by different vendors.
For printers Apple published the Bonjour Printing Specification:
<https://developer.apple.com/bonjour/printing-specification/bonjourprinting-1.2.1.pdf>
See the Legend below for an overview of collected (known) TXT records of relevance.
Other Links
-----------
- <https://grouper.ieee.org/groups/1722/contributions/2009/Bonjour%20Device%20Discovery.pdf>
- <https://wiki.debian.org/CUPSAirPrint>
Available Service Types
-----------------------
<http://www.dns-sd.org/ServiceTypes.html>
Proposed TXT Records
--------------------
**Alt. 1**
"vv=1" "product=Qemu" "ty=VM" "on=Infix" "ov=v24.03.0" "vn=KernelKit"
**Alt. 2**
"vv=1" "vendor=Qemu" "product=VM" "ty=x86-64" "vn=KernelKit" "on=Infix" "ov=v24.03.0"
Legend
------
Notice the difference between software and hardware/product related records.
am : Apple Model
mn : Model Name or role
ty : Type
on : OS Name
ov : OS Version
vn : OS Vendor Name
vs : (Software or) firmware version
vv : Vendor format of mDNS TXT records
pk : Public Key, digest or unique identifier
sf : Status Flags, detailing settings or states of the device
tv : Version of (software or) protocol
adminurl: Administration interface, e.g. adminurl=http://printer.local/#configPage
apiurl : Endpoint for REST API
vendor : Product Vendor name
product : Product name
model : Product model
path : Path to service, e.g. path=/printer
deviceid: MAC Address
btaddr : Bluetooth address
Home LAN
--------
```
$ avahi-browse -tarpk
+;eth2;IPv4;GIMLI;_smb._tcp;local
+;qtap9;IPv6;GIMLI;_smb._tcp;local
+;qtap8;IPv6;GIMLI;_smb._tcp;local
+;qtap7;IPv6;GIMLI;_smb._tcp;local
+;qtap6;IPv6;GIMLI;_smb._tcp;local
+;qtap5;IPv6;GIMLI;_smb._tcp;local
+;qtap4;IPv6;GIMLI;_smb._tcp;local
+;qtap3;IPv6;GIMLI;_smb._tcp;local
+;qtap2;IPv6;GIMLI;_smb._tcp;local
+;qtap1;IPv6;GIMLI;_smb._tcp;local
+;qtap0;IPv6;GIMLI;_smb._tcp;local
+;br0;IPv6;GIMLI;_smb._tcp;local
+;lxcbr0;IPv4;GIMLI;_smb._tcp;local
+;virbr0;IPv6;GIMLI;_smb._tcp;local
+;virbr0;IPv4;GIMLI;_smb._tcp;local
+;wlan0;IPv6;LUTHIEN;_smb._tcp;local
+;wlan0;IPv6;GIMLI;_smb._tcp;local
+;wlan0;IPv4;readynas\032\040CIFS\041;_smb._tcp;local
+;wlan0;IPv4;LUTHIEN;_smb._tcp;local
+;wlan0;IPv4;GIMLI;_smb._tcp;local
+;wlan0;IPv4;LIBREELEC;_smb._tcp;local
+;eth0;IPv6;LUTHIEN;_smb._tcp;local
+;eth0;IPv6;GIMLI;_smb._tcp;local
+;eth0;IPv4;readynas\032\040CIFS\041;_smb._tcp;local
+;eth0;IPv4;LUTHIEN;_smb._tcp;local
+;eth0;IPv4;LIBREELEC;_smb._tcp;local
+;eth0;IPv4;GIMLI;_smb._tcp;local
+;lo;IPv4;GIMLI;_smb._tcp;local
+;eth2;IPv4;GIMLI;_device-info._tcp;local
+;qtap9;IPv6;GIMLI;_device-info._tcp;local
+;qtap8;IPv6;GIMLI;_device-info._tcp;local
+;qtap7;IPv6;GIMLI;_device-info._tcp;local
+;qtap6;IPv6;GIMLI;_device-info._tcp;local
+;qtap5;IPv6;GIMLI;_device-info._tcp;local
+;qtap4;IPv6;GIMLI;_device-info._tcp;local
+;qtap3;IPv6;GIMLI;_device-info._tcp;local
+;qtap2;IPv6;GIMLI;_device-info._tcp;local
+;qtap1;IPv6;GIMLI;_device-info._tcp;local
+;qtap0;IPv6;GIMLI;_device-info._tcp;local
+;br0;IPv6;GIMLI;_device-info._tcp;local
+;lxcbr0;IPv4;GIMLI;_device-info._tcp;local
+;virbr0;IPv6;GIMLI;_device-info._tcp;local
+;virbr0;IPv4;GIMLI;_device-info._tcp;local
+;wlan0;IPv6;LUTHIEN;_device-info._tcp;local
+;wlan0;IPv6;GIMLI;_device-info._tcp;local
+;wlan0;IPv4;LIBREELEC;_device-info._tcp;local
+;wlan0;IPv4;LUTHIEN;_device-info._tcp;local
+;wlan0;IPv4;GIMLI;_device-info._tcp;local
+;eth0;IPv6;LUTHIEN;_device-info._tcp;local
+;eth0;IPv6;GIMLI;_device-info._tcp;local
+;eth0;IPv4;LIBREELEC;_device-info._tcp;local
+;eth0;IPv4;LUTHIEN;_device-info._tcp;local
+;eth0;IPv4;GIMLI;_device-info._tcp;local
+;lo;IPv4;GIMLI;_device-info._tcp;local
+;qtap9;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap8;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap7;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap6;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap5;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap4;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap3;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap2;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap1;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap0;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local
+;qtap9;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap8;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap7;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap6;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap5;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap4;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap3;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap2;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap1;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;qtap0;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local
+;wlan0;IPv4;LibreELEC;_sftp-ssh._tcp;local
+;eth0;IPv4;LibreELEC;_sftp-ssh._tcp;local
+;qtap9;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap8;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap7;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap6;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap5;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap4;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap3;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap2;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap1;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap0;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local
+;qtap9;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap9;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap8;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap8;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap7;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap7;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap6;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap6;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap5;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap5;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap4;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap4;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap3;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap3;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap2;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap2;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap1;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap1;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap0;IPv6;Web\032Management\032Interface;_https._tcp;local
+;qtap0;IPv6;Web\032Console\032Interface;_https._tcp;local
+;qtap9;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap8;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap7;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap6;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap5;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap4;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap3;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap2;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap1;IPv6;Web\032Management\032Interface;_http._tcp;local
+;qtap0;IPv6;Web\032Management\032Interface;_http._tcp;local
+;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local
+;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local
+;wlan0;IPv4;Itunes\032Server\032on\032readynas;_http._tcp;local
+;wlan0;IPv4;FrontView\032on\032readynas;_http._tcp;local
+;wlan0;IPv4;Kodi\032\040LibreELEC\041;_http._tcp;local
+;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local
+;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local
+;eth0;IPv4;Itunes\032Server\032on\032readynas;_http._tcp;local
+;eth0;IPv4;FrontView\032on\032readynas;_http._tcp;local
+;eth0;IPv4;Kodi\032\040LibreELEC\041;_http._tcp;local
+;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local
+;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local
+;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local
+;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local
+;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local
+;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local
+;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local
+;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local
+;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local
+;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local
+;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local
+;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local
+;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local
+;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local
+;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local
+;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local
+;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local
+;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local
+;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local
+;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local
+;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local
+;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local
+;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local
+;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local
+;wlan0;IPv6;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local
+;wlan0;IPv4;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local
+;eth0;IPv6;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local
+;eth0;IPv4;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local
+;wlan0;IPv6;3e88005fdb7bd9d8;_trel._udp;local
+;wlan0;IPv4;3e88005fdb7bd9d8;_trel._udp;local
+;eth0;IPv6;3e88005fdb7bd9d8;_trel._udp;local
+;eth0;IPv4;3e88005fdb7bd9d8;_trel._udp;local
+;wlan0;IPv6;Living\032Room;_srpl-tls._tcp;local
+;wlan0;IPv4;Living\032Room;_srpl-tls._tcp;local
+;eth0;IPv6;Living\032Room;_srpl-tls._tcp;local
+;eth0;IPv4;Living\032Room;_srpl-tls._tcp;local
+;wlan0;IPv6;1CB3C90F49FD\064Living\032Room;_raop._tcp;local
+;wlan0;IPv4;1CB3C90F49FD\064Living\032Room;_raop._tcp;local
+;wlan0;IPv4;B827EBAE1BA7\064Kodi\032\040LibreELEC\041;_raop._tcp;local
+;eth0;IPv6;1CB3C90F49FD\064Living\032Room;_raop._tcp;local
+;eth0;IPv4;1CB3C90F49FD\064Living\032Room;_raop._tcp;local
+;eth0;IPv4;B827EBAE1BA7\064Kodi\032\040LibreELEC\041;_raop._tcp;local
+;wlan0;IPv6;Living\032Room;_airplay._tcp;local
+;wlan0;IPv4;Living\032Room;_airplay._tcp;local
+;eth0;IPv6;Living\032Room;_airplay._tcp;local
+;eth0;IPv4;Living\032Room;_airplay._tcp;local
+;wlan0;IPv6;Living\032Room;_companion-link._tcp;local
+;wlan0;IPv4;Living\032Room;_companion-link._tcp;local
+;eth0;IPv6;Living\032Room;_companion-link._tcp;local
+;eth0;IPv4;Living\032Room;_companion-link._tcp;local
+;wlan0;IPv6;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local
+;wlan0;IPv4;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local
+;eth0;IPv6;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local
+;eth0;IPv4;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local
+;wlan0;IPv4;ReadyNAS\032Discovery\032\091readynas\093;_readynas._tcp;local
+;eth0;IPv4;ReadyNAS\032Discovery\032\091readynas\093;_readynas._tcp;local
+;wlan0;IPv4;Itunes\032Server\032on\032readynas;_rsp._tcp;local
+;eth0;IPv4;Itunes\032Server\032on\032readynas;_rsp._tcp;local
+;wlan0;IPv4;Itunes\032Server\032on\032readynas;_daap._tcp;local
+;eth0;IPv4;Itunes\032Server\032on\032readynas;_daap._tcp;local
+;wlan0;IPv4;firefly\032\09174\058da\05838\0586e\0585e\0582a\093;_workstation._tcp;local
+;wlan0;IPv4;readynas\032\091a0\05821\058b7\058c1\0588c\0583a\093;_workstation._tcp;local
+;eth0;IPv4;firefly\032\09174\058da\05838\0586e\0585e\0582a\093;_workstation._tcp;local
+;eth0;IPv4;readynas\032\091a0\05821\058b7\058c1\0588c\0583a\093;_workstation._tcp;local
+;wlan0;IPv4;root\064LibreELEC;_pulse-server._tcp;local
+;eth0;IPv4;root\064LibreELEC;_pulse-server._tcp;local
+;wlan0;IPv4;root\064LibreELEC\058\032Dummy\032Output;_pulse-sink._tcp;local
+;eth0;IPv4;root\064LibreELEC\058\032Dummy\032Output;_pulse-sink._tcp;local
+;wlan0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc-h._tcp;local
+;eth0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc-h._tcp;local
+;wlan0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc._tcp;local
+;eth0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc._tcp;local
+;wlan0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-events._udp;local
+;eth0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-events._udp;local
=;eth0;IPv4;readynas\032\040CIFS\041;_smb._tcp;local;readynas.local;192.168.1.173;445;
=;eth0;IPv4;LIBREELEC;_smb._tcp;local;LibreELEC.local;192.168.1.227;445;
=;qtap9;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::d45b:7aff:febb:d70b;445;
=;qtap8;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::60c4:7cff:fe35:7db1;445;
=;qtap7;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::4808:95ff:fee8:18df;445;
=;qtap6;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::58b2:55ff:fef6:afe7;445;
=;qtap5;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::707d:c6ff:fed8:9764;445;
=;qtap4;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::e861:b6ff:fe68:660;445;
=;qtap3;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::6c2c:b6ff:febf:fbc5;445;
=;qtap2;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::75:b3ff:fe11:4083;445;
=;qtap1;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::a0be:ccff:fec2:fb34;445;
=;qtap0;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::b4ca:54ff:fe5e:e0b4;445;
=;br0;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::e018:99ff:fe36:51c7;445;
=;virbr0;IPv6;GIMLI;_smb._tcp;local;gimli.local;2001:db8::1;445;
=;wlan0;IPv6;GIMLI;_smb._tcp;local;gimli.local;2001:9b0:214:3500::522;445;
=;eth0;IPv6;GIMLI;_smb._tcp;local;gimli.local;2001:9b0:214:3500::522;445;
=;eth0;IPv6;LUTHIEN;_smb._tcp;local;luthien.local;2001:9b0:214:3500::c2e;445;
=;wlan0;IPv6;LUTHIEN;_smb._tcp;local;luthien.local;2001:9b0:214:3500::c2e;445;
=;qtap8;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:8;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap7;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:7;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap6;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:6;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap5;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:5;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap4;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:4;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap3;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:3;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;eth0;IPv6;LUTHIEN;_device-info._tcp;local;luthien.local;2001:9b0:214:3500::650;0;"model=MacSamba"
=;wlan0;IPv6;LUTHIEN;_device-info._tcp;local;luthien.local;2001:9b0:214:3500::650;0;"model=MacSamba"
=;qtap9;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::d45b:7aff:febb:d70b;0;"model=MacSamba"
=;qtap8;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::60c4:7cff:fe35:7db1;0;"model=MacSamba"
=;qtap7;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::4808:95ff:fee8:18df;0;"model=MacSamba"
=;qtap6;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::58b2:55ff:fef6:afe7;0;"model=MacSamba"
=;qtap5;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::707d:c6ff:fed8:9764;0;"model=MacSamba"
=;qtap4;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::e861:b6ff:fe68:660;0;"model=MacSamba"
=;qtap3;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::6c2c:b6ff:febf:fbc5;0;"model=MacSamba"
=;qtap2;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::75:b3ff:fe11:4083;0;"model=MacSamba"
=;qtap1;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::a0be:ccff:fec2:fb34;0;"model=MacSamba"
=;qtap0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::b4ca:54ff:fe5e:e0b4;0;"model=MacSamba"
=;br0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::e018:99ff:fe36:51c7;0;"model=MacSamba"
=;virbr0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;2001:db8::1;0;"model=MacSamba"
=;wlan0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;2001:9b0:214:3500::e19;0;"model=MacSamba"
=;eth0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;2001:9b0:214:3500::e19;0;"model=MacSamba"
=;eth0;IPv4;LUTHIEN;_smb._tcp;local;luthien.local;192.168.1.234;445;
=;eth2;IPv4;GIMLI;_smb._tcp;local;gimli.local;192.168.2.1;445;
=;lxcbr0;IPv4;GIMLI;_smb._tcp;local;gimli.local;10.0.3.1;445;
=;virbr0;IPv4;GIMLI;_smb._tcp;local;gimli.local;192.168.122.1;445;
=;wlan0;IPv4;GIMLI;_smb._tcp;local;gimli.local;192.168.1.236;445;
=;wlan0;IPv4;readynas\032\040CIFS\041;_smb._tcp;local;readynas.local;192.168.1.173;445;
=;wlan0;IPv4;LIBREELEC;_smb._tcp;local;LibreELEC.local;192.168.1.227;445;
=;wlan0;IPv4;LUTHIEN;_smb._tcp;local;luthien.local;192.168.1.234;445;
=;eth0;IPv4;LUTHIEN;_device-info._tcp;local;luthien.local;192.168.1.132;0;"model=MacSamba"
=;eth0;IPv4;LIBREELEC;_device-info._tcp;local;LibreELEC.local;192.168.1.227;0;"model=Xserve"
=;wlan0;IPv4;LUTHIEN;_device-info._tcp;local;luthien.local;192.168.1.132;0;"model=MacSamba"
=;qtap9;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:9;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;wlan0;IPv4;LIBREELEC;_device-info._tcp;local;LibreELEC.local;192.168.1.227;0;"model=Xserve"
=;eth0;IPv4;GIMLI;_smb._tcp;local;gimli.local;192.168.1.131;445;
=;eth0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;192.168.1.131;0;"model=MacSamba"
=;lo;IPv4;GIMLI;_device-info._tcp;local;gimli.local;127.0.0.1;0;"model=MacSamba"
=;lo;IPv4;GIMLI;_smb._tcp;local;gimli.local;127.0.0.1;445;
=;eth2;IPv4;GIMLI;_device-info._tcp;local;gimli.local;192.168.2.1;0;"model=MacSamba"
=;lxcbr0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;10.0.3.1;0;"model=MacSamba"
=;virbr0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;192.168.122.1;0;"model=MacSamba"
=;wlan0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;192.168.1.236;0;"model=MacSamba"
=;qtap2;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:2;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap2;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:2;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap2;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:2;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap1;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:1;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap1;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:1;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap1;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:1;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap0;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:0;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap0;IPv6;Secure\032shell\032command\032line\032interface\032\040CLI\041;_ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:0;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap0;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:0;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap9;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:9;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap9;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:9;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap9;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:9;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap9;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:9;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap8;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:8;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap8;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:8;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap8;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:8;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap8;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:8;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap7;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:7;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap7;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:7;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap7;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:7;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap7;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:7;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap6;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:6;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap6;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:6;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap6;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:6;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap6;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:6;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap5;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:5;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap5;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:5;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap5;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:5;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap5;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:5;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap4;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:4;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap4;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:4;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap4;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:4;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap4;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:4;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap3;IPv6;NETCONF\032\040XML\047SSH\041;_netconf-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:3;830;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap3;IPv6;Secure\032file\032transfer\032\040FTP\047SSH\041;_sftp-ssh._tcp;local;infix-00-00-00.local;fe80::ff:fe00:3;22;"product=Infix v24.02.0-96-gc1770c40-dirty"
=;eth0;IPv4;LibreELEC;_sftp-ssh._tcp;local;LibreELEC.local;192.168.1.227;22;
=;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;80;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local;NPIC9167E.local;192.168.1.172;80;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;qtap2;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:2;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap2;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:2;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap2;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:2;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap1;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:1;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap1;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:1;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap1;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:1;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap0;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:0;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap0;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:0;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap0;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:0;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap3;IPv6;Web\032Management\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:3;443;"adminurl=https://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap3;IPv6;Web\032Console\032Interface;_https._tcp;local;infix-00-00-00.local;fe80::ff:fe00:3;443;"adminurl=https//infix-00-00-00.local/console" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap3;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:3;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap9;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:9;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap8;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:8;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap7;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:7;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap6;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:6;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap5;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:5;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;qtap4;IPv6;Web\032Management\032Interface;_http._tcp;local;infix-00-00-00.local;fe80::ff:fe00:4;80;"adminurl=http://infix-00-00-00.local" "product=Infix v24.02.0-96-gc1770c40-dirty"
=;wlan0;IPv4;LibreELEC;_sftp-ssh._tcp;local;LibreELEC.local;192.168.1.227;22;
=;wlan0;IPv4;Itunes\032Server\032on\032readynas;_http._tcp;local;readynas.local;192.168.1.173;3689;"ffid=7e725542" "Password=false" "Version=196610" "iTSh Version=131073" "mtd-version=svn-1676" "Machine Name=Itunes Server" "Machine ID=6C37BA14" "Database ID=6C37BA14" "txtvers=1"
=;wlan0;IPv4;FrontView\032on\032readynas;_http._tcp;local;readynas.local;192.168.1.173;80;"path=/admin/"
=;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;80;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http._tcp;local;NPIC9167E.local;192.168.1.172;80;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv4;Itunes\032Server\032on\032readynas;_http._tcp;local;readynas.local;192.168.1.173;3689;"ffid=7e725542" "Password=false" "Version=196610" "iTSh Version=131073" "mtd-version=svn-1676" "Machine Name=Itunes Server" "Machine ID=6C37BA14" "Database ID=6C37BA14" "txtvers=1"
=;eth0;IPv4;FrontView\032on\032readynas;_http._tcp;local;readynas.local;192.168.1.173;80;"path=/admin/"
=;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;515;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=50" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "rp=RAW" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local;NPIC9167E.local;192.168.1.172;515;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=50" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "rp=RAW" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;515;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=50" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "rp=RAW" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_printer._tcp;local;NPIC9167E.local;192.168.1.172;515;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=50" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "rp=RAW" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;9100;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=40" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local;NPIC9167E.local;192.168.1.172;9100;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=40" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;9100;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=40" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_pdl-datastream._tcp;local;NPIC9167E.local;192.168.1.172;9100;"TBCP=T" "Binary=T" "Transparent=T" "note=" "adminurl=http://NPIC9167E.local." "priority=40" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "qtotal=1" "txtvers=1" "UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;443;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=https://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local;NPIC9167E.local;192.168.1.172;443;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=https://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;443;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=https://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipps._tcp;local;NPIC9167E.local;192.168.1.172;443;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=https://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;631;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=http://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local;NPIC9167E.local;192.168.1.172;631;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=http://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;631;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=http://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_ipp._tcp;local;NPIC9167E.local;192.168.1.172;631;"mopria_certified=1.2" "mac=48:0f:cf:c9:16:7e" "usb_MDL=HP Color LaserJet Pro M252dw" "usb_MFG=Hewlett-Packard" "TLS=1.2" "PaperMax=legal-A4" "kind=document,envelope,photo" "UUID=564e4333-4e30-3439-3539-480fcfc9167e" "Fax=F" "Scan=F" "Duplex=T" "Color=T" "note=" "adminurl=http://NPIC9167E.local./hp/device/info_config_AirPrint.html?tab=Networking&menu=AirPrintStatus" "priority=10" "product=(HP Color LaserJet Pro M252dw)" "ty=HP Color LaserJet Pro M252dw" "URF=V1.4,CP99,W8,OB10,PQ3-4-5,ADOBERGB24,DEVRGB24,DEVW8,SRGB24,DM1,IS1,MT1-2-3-5-12,RS600" "rp=ipp/print" "pdl=image/urf,application/pdf,application/postscript,application/vnd.hp-PCL,application/vnd.hp-PCLXL,application/PCLm,application/octet-stream,image/jpeg" "qtotal=1" "txtvers=1"
=;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local;NPIC9167E.local;192.168.1.172;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_print-caps._tcp;local;NPIC9167E.local;192.168.1.172;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local;NPIC9167E.local;192.168.1.172;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv6;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local;NPIC9167E.local;fdd5:659a:93ce::341;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;eth0;IPv4;HP\032Color\032LaserJet\032Pro\032M252dw\032\040C9167E\041;_http-alt._tcp;local;NPIC9167E.local;192.168.1.172;8080;"UUID=564e4333-4e30-3439-3539-480fcfc9167e"
=;wlan0;IPv6;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local;Living-Room.local;192.168.1.197;49154;"dn=DefaultDomain" "bb=\240\191" "sq=T" "pt=;7\180\009" "at=\000\000e[\145\147\000\000" "sb=\000\000\001\177" "dd=>\136\000_\219{\217\216" "xa=>\136\000_\219{\217\216" "tv=1.3.0" "xp=\194\229\194\014\243\175Jl" "nn=MyHome1433094499" "mn=BorderRouter" "vn=Apple Inc." "rv=1"
=;eth0;IPv4;Kodi\032\040LibreELEC\041;_http._tcp;local;LibreELEC.local;192.168.1.227;8080;"uuid=58c530ac-0bb0-4f99-bbc6-c9d81639c72d" "txtvers=1"
=;wlan0;IPv4;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local;Living-Room.local;192.168.1.197;49154;"dn=DefaultDomain" "bb=\240\191" "sq=T" "pt=;7\180\009" "at=\000\000e[\145\147\000\000" "sb=\000\000\001\177" "dd=>\136\000_\219{\217\216" "xa=>\136\000_\219{\217\216" "tv=1.3.0" "xp=\194\229\194\014\243\175Jl" "nn=MyHome1433094499" "mn=BorderRouter" "vn=Apple Inc." "rv=1"
=;eth0;IPv6;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local;Living-Room.local;192.168.1.197;49154;"dn=DefaultDomain" "bb=\240\191" "sq=T" "pt=;7\180\009" "at=\000\000e[\145\147\000\000" "sb=\000\000\001\177" "dd=>\136\000_\219{\217\216" "xa=>\136\000_\219{\217\216" "tv=1.3.0" "xp=\194\229\194\014\243\175Jl" "nn=MyHome1433094499" "mn=BorderRouter" "vn=Apple Inc." "rv=1"
=;eth0;IPv4;Apple\032BorderRouter\032\035D9D8;_meshcop._udp;local;Living-Room.local;192.168.1.197;49154;"dn=DefaultDomain" "bb=\240\191" "sq=T" "pt=;7\180\009" "at=\000\000e[\145\147\000\000" "sb=\000\000\001\177" "dd=>\136\000_\219{\217\216" "xa=>\136\000_\219{\217\216" "tv=1.3.0" "xp=\194\229\194\014\243\175Jl" "nn=MyHome1433094499" "mn=BorderRouter" "vn=Apple Inc." "rv=1"
=;wlan0;IPv6;3e88005fdb7bd9d8;_trel._udp;local;Living-Room.local;192.168.1.197;51426;"xp=\194\229\194\014\243\175Jl" "xa=>\136\000_\219{\217\216"
=;wlan0;IPv4;3e88005fdb7bd9d8;_trel._udp;local;Living-Room.local;192.168.1.197;51426;"xp=\194\229\194\014\243\175Jl" "xa=>\136\000_\219{\217\216"
=;eth0;IPv6;3e88005fdb7bd9d8;_trel._udp;local;Living-Room.local;192.168.1.197;51426;"xp=\194\229\194\014\243\175Jl" "xa=>\136\000_\219{\217\216"
=;eth0;IPv4;3e88005fdb7bd9d8;_trel._udp;local;Living-Room.local;192.168.1.197;51426;"xp=\194\229\194\014\243\175Jl" "xa=>\136\000_\219{\217\216"
=;wlan0;IPv6;Living\032Room;_srpl-tls._tcp;local;Living-Room.local;192.168.1.197;853;"xpanid=c2e5c20ef3af4a6c" "did=5c2101b3d88c4acf" "pid=a3c93bc433606826" "dn=openthread.thread.home.arpa."
=;wlan0;IPv4;Living\032Room;_srpl-tls._tcp;local;Living-Room.local;192.168.1.197;853;"xpanid=c2e5c20ef3af4a6c" "did=5c2101b3d88c4acf" "pid=a3c93bc433606826" "dn=openthread.thread.home.arpa."
=;eth0;IPv6;Living\032Room;_srpl-tls._tcp;local;Living-Room.local;192.168.1.197;853;"xpanid=c2e5c20ef3af4a6c" "did=5c2101b3d88c4acf" "pid=a3c93bc433606826" "dn=openthread.thread.home.arpa."
=;eth0;IPv4;Living\032Room;_srpl-tls._tcp;local;Living-Room.local;192.168.1.197;853;"xpanid=c2e5c20ef3af4a6c" "did=5c2101b3d88c4acf" "pid=a3c93bc433606826" "dn=openthread.thread.home.arpa."
=;wlan0;IPv6;1CB3C90F49FD\064Living\032Room;_raop._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "ov=17.4" "vs=760.20.1" "vn=65537" "tp=UDP" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "am=AppleTV11,1" "md=0,1,2" "sf=0x644" "ft=0x4A7FDFD5,0xBC177FDE" "et=0,3,5" "da=true" "cn=0,1,2,3"
=;wlan0;IPv4;1CB3C90F49FD\064Living\032Room;_raop._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "ov=17.4" "vs=760.20.1" "vn=65537" "tp=UDP" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "am=AppleTV11,1" "md=0,1,2" "sf=0x644" "ft=0x4A7FDFD5,0xBC177FDE" "et=0,3,5" "da=true" "cn=0,1,2,3"
=;eth0;IPv6;1CB3C90F49FD\064Living\032Room;_raop._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "ov=17.4" "vs=760.20.1" "vn=65537" "tp=UDP" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "am=AppleTV11,1" "md=0,1,2" "sf=0x644" "ft=0x4A7FDFD5,0xBC177FDE" "et=0,3,5" "da=true" "cn=0,1,2,3"
=;eth0;IPv4;1CB3C90F49FD\064Living\032Room;_raop._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "ov=17.4" "vs=760.20.1" "vn=65537" "tp=UDP" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "am=AppleTV11,1" "md=0,1,2" "sf=0x644" "ft=0x4A7FDFD5,0xBC177FDE" "et=0,3,5" "da=true" "cn=0,1,2,3"
=;wlan0;IPv6;Living\032Room;_airplay._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "osvers=17.4" "srcvers=760.20.1" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "psi=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "pi=ff37ee9c-0677-4ee4-a2c3-7cc3071c42c7" "protovers=1.1" "model=AppleTV11,1" "gcgl=1" "igl=1" "gid=8AB8C139-61B6-404C-BBE5-151E2601E356" "flags=0x644" "features=0x4A7FDFD5,0xBC177FDE" "fex=1d9/St5/F7w4oQY" "deviceid=1C:B3:C9:0F:49:FD" "btaddr=00:00:00:00:00:00" "acl=0"
=;wlan0;IPv4;Living\032Room;_airplay._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "osvers=17.4" "srcvers=760.20.1" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "psi=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "pi=ff37ee9c-0677-4ee4-a2c3-7cc3071c42c7" "protovers=1.1" "model=AppleTV11,1" "gcgl=1" "igl=1" "gid=8AB8C139-61B6-404C-BBE5-151E2601E356" "flags=0x644" "features=0x4A7FDFD5,0xBC177FDE" "fex=1d9/St5/F7w4oQY" "deviceid=1C:B3:C9:0F:49:FD" "btaddr=00:00:00:00:00:00" "acl=0"
=;eth0;IPv6;Living\032Room;_airplay._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "osvers=17.4" "srcvers=760.20.1" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "psi=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "pi=ff37ee9c-0677-4ee4-a2c3-7cc3071c42c7" "protovers=1.1" "model=AppleTV11,1" "gcgl=1" "igl=1" "gid=8AB8C139-61B6-404C-BBE5-151E2601E356" "flags=0x644" "features=0x4A7FDFD5,0xBC177FDE" "fex=1d9/St5/F7w4oQY" "deviceid=1C:B3:C9:0F:49:FD" "btaddr=00:00:00:00:00:00" "acl=0"
=;eth0;IPv4;Living\032Room;_airplay._tcp;local;Living-Room.local;192.168.1.197;7000;"vv=1" "osvers=17.4" "srcvers=760.20.1" "pk=dcd3a159312bf6ba5369680a5642e919ef6a4b1549584e6e6f2d234fa4d6890d" "psi=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "pi=ff37ee9c-0677-4ee4-a2c3-7cc3071c42c7" "protovers=1.1" "model=AppleTV11,1" "gcgl=1" "igl=1" "gid=8AB8C139-61B6-404C-BBE5-151E2601E356" "flags=0x644" "features=0x4A7FDFD5,0xBC177FDE" "fex=1d9/St5/F7w4oQY" "deviceid=1C:B3:C9:0F:49:FD" "btaddr=00:00:00:00:00:00" "acl=0"
=;wlan0;IPv6;Living\032Room;_companion-link._tcp;local;Living-Room.local;192.168.1.197;49153;"rpMRtID=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "rpBA=57:46:6D:57:B4:50" "rpHI=d3660be33890" "rpAD=404149af4259" "rpVr=543.1" "rpMd=AppleTV11,1" "rpHA=1b4700ce1a10" "rpFl=0xB67A2" "rpHN=5c9257b8eae5" "rpMac=0"
=;wlan0;IPv4;Living\032Room;_companion-link._tcp;local;Living-Room.local;192.168.1.197;49153;"rpMRtID=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "rpBA=57:46:6D:57:B4:50" "rpHI=d3660be33890" "rpAD=404149af4259" "rpVr=543.1" "rpMd=AppleTV11,1" "rpHA=1b4700ce1a10" "rpFl=0xB67A2" "rpHN=5c9257b8eae5" "rpMac=0"
=;wlan0;IPv4;Kodi\032\040LibreELEC\041;_http._tcp;local;LibreELEC.local;192.168.1.227;8080;"uuid=58c530ac-0bb0-4f99-bbc6-c9d81639c72d" "txtvers=1"
=;eth0;IPv6;Living\032Room;_companion-link._tcp;local;Living-Room.local;192.168.1.197;49153;"rpMRtID=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "rpBA=57:46:6D:57:B4:50" "rpHI=d3660be33890" "rpAD=404149af4259" "rpVr=543.1" "rpMd=AppleTV11,1" "rpHA=1b4700ce1a10" "rpFl=0xB67A2" "rpHN=5c9257b8eae5" "rpMac=0"
=;eth0;IPv4;Living\032Room;_companion-link._tcp;local;Living-Room.local;192.168.1.197;49153;"rpMRtID=74EBA0AF-DB0F-483F-A9E6-0B4EB6C82857" "rpBA=57:46:6D:57:B4:50" "rpHI=d3660be33890" "rpAD=404149af4259" "rpVr=543.1" "rpMd=AppleTV11,1" "rpHA=1b4700ce1a10" "rpFl=0xB67A2" "rpHN=5c9257b8eae5" "rpMac=0"
=;wlan0;IPv6;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local;Living-Room.local;192.168.1.197;50103;
=;wlan0;IPv4;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local;Living-Room.local;192.168.1.197;50103;
=;eth0;IPv6;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local;Living-Room.local;192.168.1.197;50103;
=;eth0;IPv4;70-35-60-63\.1\032Living\032Room;_sleep-proxy._udp;local;Living-Room.local;192.168.1.197;50103;
=;eth0;IPv4;ReadyNAS\032Discovery\032\091readynas\093;_readynas._tcp;local;readynas.local;192.168.1.173;9;"raid=X-RAID2" "channels=4" "serial=2DK61B0G00662" "model=ReadyNAS Ultra 4" "vendor=NETGEAR"
=;eth0;IPv4;B827EBAE1BA7\064Kodi\032\040LibreELEC\041;_raop._tcp;local;LibreELEC.local;192.168.1.227;36666;"vs=130.14" "am=Kodi,1" "md=0,1,2" "da=true" "vn=3" "pw=false" "sr=44100" "ss=16" "sm=false" "tp=UDP" "sv=false" "et=0,1" "ek=1" "ch=2" "cn=0,1" "txtvers=1"
=;wlan0;IPv4;ReadyNAS\032Discovery\032\091readynas\093;_readynas._tcp;local;readynas.local;192.168.1.173;9;"raid=X-RAID2" "channels=4" "serial=2DK61B0G00662" "model=ReadyNAS Ultra 4" "vendor=NETGEAR"
=;wlan0;IPv4;B827EBAE1BA7\064Kodi\032\040LibreELEC\041;_raop._tcp;local;LibreELEC.local;192.168.1.227;36666;"vs=130.14" "am=Kodi,1" "md=0,1,2" "da=true" "vn=3" "pw=false" "sr=44100" "ss=16" "sm=false" "tp=UDP" "sv=false" "et=0,1" "ek=1" "ch=2" "cn=0,1" "txtvers=1"
=;eth0;IPv4;Itunes\032Server\032on\032readynas;_daap._tcp;local;readynas.local;192.168.1.173;3689;"ffid=7e725542" "Password=false" "Version=196610" "iTSh Version=131073" "mtd-version=svn-1676" "Machine Name=Itunes Server" "Machine ID=6C37BA14" "Database ID=6C37BA14" "txtvers=1"
=;eth0;IPv4;Itunes\032Server\032on\032readynas;_rsp._tcp;local;readynas.local;192.168.1.173;3689;"ffid=7e725542" "Password=false" "Version=196610" "iTSh Version=131073" "mtd-version=svn-1676" "Machine Name=Itunes Server" "Machine ID=6C37BA14" "Database ID=6C37BA14" "txtvers=1"
=;eth0;IPv4;readynas\032\091a0\05821\058b7\058c1\0588c\0583a\093;_workstation._tcp;local;readynas.local;192.168.1.173;9;
=;eth0;IPv4;root\064LibreELEC;_pulse-server._tcp;local;LibreELEC.local;192.168.1.227;4713;"cookie=0xcaa87808" "fqdn=LibreELEC" "uname=Linux armv7l 4.19.127 #1 SMP Tue Jul 6 19:08:28 CEST 2021" "machine-id=0591ddc66796df8c2bb9bd455b2cd975" "user-name=root" "server-version=pulseaudio 12.2"
=;eth0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-events._udp;local;LibreELEC.local;192.168.1.227;9777;
=;eth0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc._tcp;local;LibreELEC.local;192.168.1.227;9090;"uuid=58c530ac-0bb0-4f99-bbc6-c9d81639c72d" "txtvers=1"
=;eth0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc-h._tcp;local;LibreELEC.local;192.168.1.227;8080;"uuid=58c530ac-0bb0-4f99-bbc6-c9d81639c72d" "txtvers=1"
=;eth0;IPv4;root\064LibreELEC\058\032Dummy\032Output;_pulse-sink._tcp;local;LibreELEC.local;192.168.1.227;4713;"icon-name=computer" "class=abstract" "description=Dummy Output" "subtype=virtual" "channel_map=front-left,front-right" "format=s16le" "channels=2" "rate=44100" "device=auto_null" "cookie=0xcaa87808" "fqdn=LibreELEC" "uname=Linux armv7l 4.19.127 #1 SMP Tue Jul 6 19:08:28 CEST 2021" "machine-id=0591ddc66796df8c2bb9bd455b2cd975" "user-name=root" "server-version=pulseaudio 12.2"
=;wlan0;IPv4;Itunes\032Server\032on\032readynas;_daap._tcp;local;readynas.local;192.168.1.173;3689;"ffid=7e725542" "Password=false" "Version=196610" "iTSh Version=131073" "mtd-version=svn-1676" "Machine Name=Itunes Server" "Machine ID=6C37BA14" "Database ID=6C37BA14" "txtvers=1"
=;wlan0;IPv4;Itunes\032Server\032on\032readynas;_rsp._tcp;local;readynas.local;192.168.1.173;3689;"ffid=7e725542" "Password=false" "Version=196610" "iTSh Version=131073" "mtd-version=svn-1676" "Machine Name=Itunes Server" "Machine ID=6C37BA14" "Database ID=6C37BA14" "txtvers=1"
=;wlan0;IPv4;readynas\032\091a0\05821\058b7\058c1\0588c\0583a\093;_workstation._tcp;local;readynas.local;192.168.1.173;9;
=;wlan0;IPv4;root\064LibreELEC;_pulse-server._tcp;local;LibreELEC.local;192.168.1.227;4713;"cookie=0xcaa87808" "fqdn=LibreELEC" "uname=Linux armv7l 4.19.127 #1 SMP Tue Jul 6 19:08:28 CEST 2021" "machine-id=0591ddc66796df8c2bb9bd455b2cd975" "user-name=root" "server-version=pulseaudio 12.2"
=;wlan0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-events._udp;local;LibreELEC.local;192.168.1.227;9777;
=;wlan0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc._tcp;local;LibreELEC.local;192.168.1.227;9090;"uuid=58c530ac-0bb0-4f99-bbc6-c9d81639c72d" "txtvers=1"
=;wlan0;IPv4;Kodi\032\040LibreELEC\041;_xbmc-jsonrpc-h._tcp;local;LibreELEC.local;192.168.1.227;8080;"uuid=58c530ac-0bb0-4f99-bbc6-c9d81639c72d" "txtvers=1"
=;wlan0;IPv4;root\064LibreELEC\058\032Dummy\032Output;_pulse-sink._tcp;local;LibreELEC.local;192.168.1.227;4713;"icon-name=computer" "class=abstract" "description=Dummy Output" "subtype=virtual" "channel_map=front-left,front-right" "format=s16le" "channels=2" "rate=44100" "device=auto_null" "cookie=0xcaa87808" "fqdn=LibreELEC" "uname=Linux armv7l 4.19.127 #1 SMP Tue Jul 6 19:08:28 CEST 2021" "machine-id=0591ddc66796df8c2bb9bd455b2cd975" "user-name=root" "server-version=pulseaudio 12.2"
Failed to resolve service 'firefly [74:da:38:6e:5e:2a]' of type '_workstation._tcp' in domain 'local': Timeout reached
Failed to resolve service 'firefly [74:da:38:6e:5e:2a]' of type '_workstation._tcp' in domain 'local': Timeout reached
```
Office LAN
----------
```
$ avahi-browse -tarpk
+;eth2;IPv4;GIMLI;_device-info._tcp;local
+;br0;IPv6;GIMLI;_device-info._tcp;local
+;lxcbr0;IPv4;GIMLI;_device-info._tcp;local
+;virbr0;IPv6;GIMLI;_device-info._tcp;local
+;virbr0;IPv4;GIMLI;_device-info._tcp;local
+;wlan0;IPv6;GIMLI;_device-info._tcp;local
+;wlan0;IPv4;GIMLI;_device-info._tcp;local
+;eth0;IPv6;GIMLI;_device-info._tcp;local
+;eth0;IPv4;GIMLI;_device-info._tcp;local
+;lo;IPv4;GIMLI;_device-info._tcp;local
+;eth2;IPv4;GIMLI;_smb._tcp;local
+;br0;IPv6;GIMLI;_smb._tcp;local
+;lxcbr0;IPv4;GIMLI;_smb._tcp;local
+;virbr0;IPv6;GIMLI;_smb._tcp;local
+;virbr0;IPv4;GIMLI;_smb._tcp;local
+;wlan0;IPv6;GIMLI;_smb._tcp;local
+;wlan0;IPv4;GIMLI;_smb._tcp;local
+;eth0;IPv6;GIMLI;_smb._tcp;local
+;eth0;IPv4;GIMLI;_smb._tcp;local
+;lo;IPv4;GIMLI;_smb._tcp;local
+;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_uscan._tcp;local
+;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_uscan._tcp;local
+;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_http._tcp;local
+;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_http._tcp;local
+;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_scanner._tcp;local
+;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_scanner._tcp;local
+;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_ipp._tcp;local
+;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_ipp._tcp;local
+;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_printer._tcp;local
+;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_printer._tcp;local
+;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_pdl-datastream._tcp;local
+;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_pdl-datastream._tcp;local
=;br0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::e018:99ff:fe36:51c7;0;"model=MacSamba"
=;br0;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::e018:99ff:fe36:51c7;445;
=;virbr0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;2001:db8::1;0;"model=MacSamba"
=;virbr0;IPv6;GIMLI;_smb._tcp;local;gimli.local;2001:db8::1;445;
=;wlan0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::fc94:34d5:b4d3:69e5;0;"model=MacSamba"
=;wlan0;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::fc94:34d5:b4d3:69e5;445;
=;eth0;IPv6;GIMLI;_device-info._tcp;local;gimli.local;fe80::fb59:6e2f:da3:975c;0;"model=MacSamba"
=;eth0;IPv6;GIMLI;_smb._tcp;local;gimli.local;fe80::fb59:6e2f:da3:975c;445;
=;eth2;IPv4;GIMLI;_device-info._tcp;local;gimli.local;192.168.2.1;0;"model=MacSamba"
=;eth2;IPv4;GIMLI;_smb._tcp;local;gimli.local;192.168.2.1;445;
=;lxcbr0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;10.0.3.1;0;"model=MacSamba"
=;lxcbr0;IPv4;GIMLI;_smb._tcp;local;gimli.local;10.0.3.1;445;
=;virbr0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;192.168.122.1;0;"model=MacSamba"
=;virbr0;IPv4;GIMLI;_smb._tcp;local;gimli.local;192.168.122.1;445;
=;wlan0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;172.31.31.245;0;"model=MacSamba"
=;wlan0;IPv4;GIMLI;_smb._tcp;local;gimli.local;172.31.31.245;445;
=;eth0;IPv4;GIMLI;_device-info._tcp;local;gimli.local;172.31.21.164;0;"model=MacSamba"
=;eth0;IPv4;GIMLI;_smb._tcp;local;gimli.local;172.31.21.164;445;
=;lo;IPv4;GIMLI;_device-info._tcp;local;gimli.local;127.0.0.1;0;"model=MacSamba"
=;lo;IPv4;GIMLI;_smb._tcp;local;gimli.local;127.0.0.1;445;
=;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_pdl-datastream._tcp;local;BRNB42200415BAA.local;172.31.21.11;9100;"UUID=e3248000-80ce-11db-8000-b42200415baa" "TBCP=T" "Transparent=F" "Binary=T" "PaperCustom=T" "Scan=T" "Fax=F" "Duplex=T" "Copies=T" "Color=T" "usb_CMD=PJL,PCL,PCLXL,URF" "usb_MDL=DCP-L3550CDW series" "usb_MFG=Brother" "priority=75" "adminurl=http://BRNB42200415BAA.local./" "product=(Brother DCP-L3550CDW series)" "ty=Brother DCP-L3550CDW series" "note=" "pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster" "qtotal=1" "txtvers=1"
=;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_printer._tcp;local;BRNB42200415BAA.local;172.31.21.11;515;"UUID=e3248000-80ce-11db-8000-b42200415baa" "TBCP=F" "Transparent=T" "Binary=T" "PaperCustom=T" "Scan=T" "Fax=F" "Duplex=T" "Copies=T" "Color=T" "usb_CMD=PJL,PCL,PCLXL,URF" "usb_MDL=DCP-L3550CDW series" "usb_MFG=Brother" "priority=50" "adminurl=http://BRNB42200415BAA.local./" "product=(Brother DCP-L3550CDW series)" "ty=Brother DCP-L3550CDW series" "note=" "rp=duerqxesz5090" "pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster" "qtotal=1" "txtvers=1"
=;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_ipp._tcp;local;BRNB42200415BAA.local;172.31.21.11;631;"mopria-certified=1.3" "print_wfds=T" "UUID=e3248000-80ce-11db-8000-b42200415baa" "PaperMax=legal-A4" "kind=document,envelope,label,postcard" "URF=SRGB24,W8,CP1,IS4-1,MT1-3-4-5-8-11,OB10,PQ4,RS600,V1.4,DM1" "TBCP=F" "Transparent=T" "Binary=T" "PaperCustom=T" "Scan=T" "Fax=F" "Duplex=T" "Copies=T" "Color=T" "usb_CMD=PJL,PCL,PCLXL,URF" "usb_MDL=DCP-L3550CDW series" "usb_MFG=Brother" "priority=25" "adminurl=http://BRNB42200415BAA.local./net/net/airprint.html" "product=(Brother DCP-L3550CDW series)" "ty=Brother DCP-L3550CDW series" "note=" "rp=ipp/print" "pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster" "qtotal=1" "txtvers=1"
=;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_scanner._tcp;local;BRNB42200415BAA.local;172.31.21.11;54921;"flatbed=T" "feeder=T" "button=T" "mdl=DCP-L3550CDW series" "mfg=Brother" "ty=Brother DCP-L3550CDW series" "adminurl=http://BRNB42200415BAA.local./" "note=" "txtvers=1"
=;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_http._tcp;local;BRNB42200415BAA.local;172.31.21.11;80;
=;eth0;IPv6;Brother\032DCP-L3550CDW\032series;_uscan._tcp;local;BRNB42200415BAA.local;172.31.21.11;80;"duplex=F" "is=adf,platen" "cs=binary,grayscale,color" "UUID=e3248000-80ce-11db-8000-b42200415baa" "pdl=application/pdf,image/jpeg" "note=" "ty=Brother DCP-L3550CDW series" "rs=eSCL" "representation=http://BRNB42200415BAA.local./icons/device-icons-128.png" "adminurl=http://BRNB42200415BAA.local./net/net/airprint.html" "vers=2.63" "txtvers=1"
=;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_pdl-datastream._tcp;local;BRNB42200415BAA.local;172.31.21.11;9100;"UUID=e3248000-80ce-11db-8000-b42200415baa" "TBCP=T" "Transparent=F" "Binary=T" "PaperCustom=T" "Scan=T" "Fax=F" "Duplex=T" "Copies=T" "Color=T" "usb_CMD=PJL,PCL,PCLXL,URF" "usb_MDL=DCP-L3550CDW series" "usb_MFG=Brother" "priority=75" "adminurl=http://BRNB42200415BAA.local./" "product=(Brother DCP-L3550CDW series)" "ty=Brother DCP-L3550CDW series" "note=" "pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster" "qtotal=1" "txtvers=1"
=;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_printer._tcp;local;BRNB42200415BAA.local;172.31.21.11;515;"UUID=e3248000-80ce-11db-8000-b42200415baa" "TBCP=F" "Transparent=T" "Binary=T" "PaperCustom=T" "Scan=T" "Fax=F" "Duplex=T" "Copies=T" "Color=T" "usb_CMD=PJL,PCL,PCLXL,URF" "usb_MDL=DCP-L3550CDW series" "usb_MFG=Brother" "priority=50" "adminurl=http://BRNB42200415BAA.local./" "product=(Brother DCP-L3550CDW series)" "ty=Brother DCP-L3550CDW series" "note=" "rp=duerqxesz5090" "pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster" "qtotal=1" "txtvers=1"
=;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_ipp._tcp;local;BRNB42200415BAA.local;172.31.21.11;631;"mopria-certified=1.3" "print_wfds=T" "UUID=e3248000-80ce-11db-8000-b42200415baa" "PaperMax=legal-A4" "kind=document,envelope,label,postcard" "URF=SRGB24,W8,CP1,IS4-1,MT1-3-4-5-8-11,OB10,PQ4,RS600,V1.4,DM1" "TBCP=F" "Transparent=T" "Binary=T" "PaperCustom=T" "Scan=T" "Fax=F" "Duplex=T" "Copies=T" "Color=T" "usb_CMD=PJL,PCL,PCLXL,URF" "usb_MDL=DCP-L3550CDW series" "usb_MFG=Brother" "priority=25" "adminurl=http://BRNB42200415BAA.local./net/net/airprint.html" "product=(Brother DCP-L3550CDW series)" "ty=Brother DCP-L3550CDW series" "note=" "rp=ipp/print" "pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster" "qtotal=1" "txtvers=1"
=;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_scanner._tcp;local;BRNB42200415BAA.local;172.31.21.11;54921;"flatbed=T" "feeder=T" "button=T" "mdl=DCP-L3550CDW series" "mfg=Brother" "ty=Brother DCP-L3550CDW series" "adminurl=http://BRNB42200415BAA.local./" "note=" "txtvers=1"
=;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_http._tcp;local;BRNB42200415BAA.local;172.31.21.11;80;
=;eth0;IPv4;Brother\032DCP-L3550CDW\032series;_uscan._tcp;local;BRNB42200415BAA.local;172.31.21.11;80;"duplex=F" "is=adf,platen" "cs=binary,grayscale,color" "UUID=e3248000-80ce-11db-8000-b42200415baa" "pdl=application/pdf,image/jpeg" "note=" "ty=Brother DCP-L3550CDW series" "rs=eSCL" "representation=http://BRNB42200415BAA.local./icons/device-icons-128.png" "adminurl=http://BRNB42200415BAA.local./net/net/airprint.html" "vers=2.63" "txtvers=1"
```
+3 -1
View File
@@ -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
+817
View File
@@ -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");
}
+68
View File
@@ -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
+6 -5
View File
@@ -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"
+91 -1
View File
@@ -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"""
@@ -5533,7 +5618,8 @@ def main():
global UNIT_TEST
try:
json_data = json.load(sys.stdin)
raw = sys.stdin.read()
json_data = json.loads(raw) if raw.strip() else {}
except json.JSONDecodeError:
print("Error, invalid JSON input")
sys.exit(1)
@@ -5566,6 +5652,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 +5724,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":
+6
View File
@@ -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);
+1 -1
View File
@@ -8,7 +8,7 @@ Firewall configuration suitable for end devices on untrusted networks.
image::basic.svg[align=center, scaledwidth=50%]
- Single zone configuration, "public", with action=drop
- Single zone configuration, "public-untrusted-net", with `action=drop`
- Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222)
- All other ports (HTTP, HTTPS, Telnet, etc.) blocked
- Check that unused interfaces are automatically assigned to default zone
+12 -10
View File
@@ -5,7 +5,7 @@ Firewall configuration suitable for end devices on untrusted networks.
image::basic.svg[align=center, scaledwidth=50%]
- Single zone configuration, "public", with action=drop
- Single zone configuration, "public-untrusted-net", with `action=drop`
- Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222)
- All other ports (HTTP, HTTPS, Telnet, etc.) blocked
- Check that unused interfaces are automatically assigned to default zone
@@ -47,7 +47,7 @@ with infamy.Test() as test:
target.put_config_dict("infix-firewall", {
"firewall": {
"default": "public",
"default": "public-untrusted-net",
"logging": "all",
"service": [{
"name": "mySSH",
@@ -69,7 +69,9 @@ with infamy.Test() as test:
"interface": [mgmt_if],
"service": ["ssh", "netconf", "restconf"]
}, {
"name": "public",
# 20-char name, exceeds old iptables-derived 17-char limit
# Verifies we allow long names with nftables, issue #1389
"name": "public-untrusted-net",
"description": "Public untrusted network",
"action": "drop",
"interface": [data_if],
@@ -80,7 +82,7 @@ with infamy.Test() as test:
# Wait for configuration to be activated
infamy.Firewall.wait_for_operational(target, {
"public": {"action": "drop"},
"public-untrusted-net": {"action": "drop"},
"mgmt": {"action": "accept"}
})
@@ -88,7 +90,7 @@ with infamy.Test() as test:
data = target.get_data("/infix-firewall:firewall")
fw = data["firewall"]
assert fw["default"] == "public"
assert fw["default"] == "public-untrusted-net"
services = {svc["name"]: svc for svc in fw.get("service", [])}
assert "mySSH" in services, "Custom service mySSH not found"
@@ -106,8 +108,8 @@ with infamy.Test() as test:
assert int(port_entry["lower"]) == 8080
zones = {zone["name"]: zone for zone in fw["zone"]}
assert "public" in zones, "Public zone not found in configuration"
public_zone = zones["public"]
assert "public-untrusted-net" in zones, "public-untrusted-net zone not found in configuration"
public_zone = zones["public-untrusted-net"]
assert public_zone["action"] == "drop"
assert data_if in public_zone["interface"]
assert "ssh" in public_zone["service"]
@@ -119,13 +121,13 @@ with infamy.Test() as test:
data = target.get_data("/infix-firewall:firewall")
fw = data["firewall"]
assert fw["default"] == "public", "Default zone should be 'public'"
assert fw["default"] == "public-untrusted-net", "Default zone should be 'public-untrusted-net'"
zones = {zone["name"]: zone for zone in fw["zone"]}
public_zone = zones["public"]
public_zone = zones["public-untrusted-net"]
assert unused_if in public_zone["interface"], \
f"Unused interface {unused_if} should be in default zone 'public', got interfaces: {public_zone['interface']}"
f"Unused interface {unused_if} should be in default zone 'public-untrusted-net', got interfaces: {public_zone['interface']}"
with infamy.IsolatedMacVlan(host_data) as ns:
ns.addip(HOST_IP)
+13 -11
View File
@@ -4,11 +4,11 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/hardware/gps_simple]
==== Description
Verify that a simulated GPS receiver is detected and reports a valid
fix via the ietf-hardware operational datastore.
Verify that two simulated GPS receivers are detected and report valid
fixes via the ietf-hardware operational datastore.
The test injects NMEA sentences through a QEMU pipe chardev FIFO,
which appears as a virtio serial port inside the guest.
The test injects NMEA sentences through QEMU pipe chardev FIFOs,
which appear as virtio serial ports inside the guest.
==== Topology
@@ -17,13 +17,15 @@ image::topology.svg[GPS receiver basic test topology, align=center, scaledwidth=
==== Sequence
. Set up topology and attach to target DUT
. Configure GPS hardware component
. Verify GPS is activated
. Verify GPS has a fix
. Verify the position is near the coordinates you test with
. Configure GPS hardware components
. Verify both GPS receivers are activated
. Verify both GPS receivers have a fix
. Verify gps0 position is near the coordinates
. Verify gps1 position is near the coordinates
. Save the configuration to startup configuration and reboot
. Verify GPS is activated
. Verify GPS has a fix
. Verify the position is near the coordinates you test with
. Verify both GPS receivers are activated
. Verify both GPS receivers have a fix
. Verify gps0 position is near the coordinates
. Verify gps1 position is near the coordinates
+32 -1
View File
@@ -1,3 +1,32 @@
"""TAP (Test Anything Protocol) output helpers for Infamy tests.
TAP is a simple line-based protocol for communicating test results between a
test producer and a consumer (harness). The canonical reference is:
https://testanything.org/tap-specification.html
The key elements are:
Plan line: ``1..N`` — total number of tests, written once
Result lines: ``ok N - desc`` — test N passed
``not ok N - desc``— test N failed
Directives: ``ok N # SKIP`` ``not ok N # TODO``
Diagnostics: Any line beginning with ``#`` is a comment/diagnostic.
The plan line may appear either at the beginning (before any results) or at
the end (after all results). Infamy writes it at the END so that the total
step count is known; a harness that requires an upfront plan will report
"no plan" on failure — see CommentWriter below for a known limitation.
``CommentWriter`` redirects Python's ``sys.stdout`` so that ordinary
``print()`` calls are prefixed with ``# `` and appear as diagnostics, while
TAP result lines (written via ``self.out``, the *original* stdout) are emitted
verbatim. The plan line is also written via ``self.out``.
Note: because ``sys.stdout`` is replaced *after* module import the default
argument ``output=sys.stdout`` captures the original stream at import time,
so nested or re-entrant ``Test()`` instances will share the same ``self.out``.
"""
import contextlib
import datetime
import subprocess
@@ -41,7 +70,7 @@ class Test:
self._not_ok("Missing explicit test result\n")
else:
if t in (TestPass, TestSkip):
self.out.write(f"{self.steps}..{self.steps}\n")
self.out.write(f"1..{self.steps}\n")
self.out.flush()
raise SystemExit(0)
if t is AssertionError:
@@ -56,6 +85,8 @@ class Test:
elif len(e.args) and type(e.args[0]) is subprocess.CompletedProcess:
print("Failing subprocess stdout:\n", e.args[0].stdout)
self.out.write(f"1..{self.steps}\n")
self.out.flush()
raise SystemExit(1)
@contextlib.contextmanager
+44 -5
View File
@@ -3,6 +3,7 @@
set -e
. $(dirname $(readlink -f "$0"))/libll.sh
. $(dirname $(readlink -f "$0"))/libfleet.sh
usage()
{
@@ -13,6 +14,7 @@ usage: $me [-A] <command> [<args>]
Wrap various existing network facilities such that an interface name
may be supplied in places where a hostname is otherwise expected.
Also provides fleet management and upgrade of enrolled Infix devices.
Options:
@@ -20,7 +22,7 @@ may be supplied in places where a hostname is otherwise expected.
For commands requiring authentication, use password
authentication with username "admin" and password "admin".
Commands:
Link-local commands:
scan [-a] [<iface>]
Discover Infix devices on the LAN using mDNS-SD (avahi-browse).
@@ -44,17 +46,51 @@ may be supplied in places where a hostname is otherwise expected.
<src> or <dst> is assumed to be an interface name, which is
expanded to a neighboring host on that interface.
${0} help
Fleet commands (RESTCONF, config in ~/.config/infix/config.json):
fleet enroll [-p <profile>] [-u <user>] [-w <password>] <name> <address>
Enroll a device. Profile groups devices for bulk operations.
Password may be omitted if -A is always used.
fleet enroll -d <name>
Remove an enrolled device.
fleet list
List all enrolled devices.
fleet upgrade <name|profile> <url>
Install a software bundle from <url> on one device or all devices
in a profile, in parallel, with a live per-device progress display.
fleet backup [-o <dir>] <name|profile>
Save the startup-config from one device or all devices in a profile.
Single device: saved to the current directory.
Profile: saved to ~/.config/infix/<profile>/<name>/.
fleet reboot <name|profile>
Reboot one device or all devices in a profile.
help
Display this message.
Examples:
Start interactive ssh(1) session to the neighbor to eth0
Start interactive ssh(1) session to the neighbor on eth0
$me ssh eth0
Copy /etc/hostname from the neighbor to eth0, using admin/admin
Copy /etc/hostname from the neighbor on eth0, using admin/admin
$me -A scp eth0:/etc/hostname /tmp/hostname
Enroll two devices into the "aarch64" fleet
$me fleet enroll -p aarch64 -w secret gw-1 192.168.1.10
$me fleet enroll -p aarch64 -w secret gw-2 192.168.1.11
Upgrade the entire aarch64 fleet
$me fleet upgrade aarch64 ftp://build-server/infix-aarch64-26.02.pkg
Backup a single device's startup-config to the current directory
$me -A fleet backup gw-1
EOF
}
@@ -96,8 +132,11 @@ case "$cmd" in
ssh)
llssh "$@"
;;
fleet)
fleet_main "$@"
;;
*)
echo "Unknown command \"$cmd\"" >2
echo "Unknown command \"$cmd\"" >&2
exit 1
;;
esac
+14 -6
View File
@@ -249,9 +249,8 @@ update_changelog() {
# Check if the latest release is UNRELEASED (first release header in file)
FIRST_RELEASE=$(grep -m1 "^\[v" doc/ChangeLog.md)
if ! echo "$FIRST_RELEASE" | grep -q "\[UNRELEASED\]"; then
log_error "First release section in ChangeLog.md is not UNRELEASED"
log_error "Please create an UNRELEASED section first before running this script"
exit 1
log_warn "No UNRELEASED section found in ChangeLog.md, skipping changelog update"
return 0
fi
# Extract just the UNRELEASED section (from start until next version header)
@@ -264,13 +263,22 @@ update_changelog() {
sed -i '0,/^- Upgrade \(\*\*\)\?Linux kernel to .*/s//- Upgrade Linux kernel to '"$NEW_VERSION"' (LTS)/' doc/ChangeLog.md
log_info "Updated existing kernel version entry to $NEW_VERSION"
else
# Add new kernel upgrade entry after first "### Changes"
# Add new kernel upgrade entry after the first "### Changes" in the UNRELEASED section.
# Use getline to peek at the line following "### Changes" so we can preserve it:
# - blank line: print it, then insert kernel entry (normal case)
# - non-blank line: insert kernel entry first, then print the consumed line
# (guards against silently dropping existing entries, e.g. a Buildroot upgrade)
awk -v new_line="- Upgrade Linux kernel to $NEW_VERSION (LTS)" '
/^### Changes/ && !done {
print
getline
if (NF == 0) print
print new_line
if (NF == 0) {
print
print new_line
} else {
print new_line
print
}
done=1
next
}
+530
View File
@@ -0,0 +1,530 @@
# Fleet management via RESTCONF for Infix devices.
#
# Requires: curl, jq
FLEET_CONFIG_DIR="${HOME}/.config/infix"
FLEET_CONFIG="${FLEET_CONFIG_DIR}/config.json"
FLEET_BAR_WIDTH=20
FLEET_TIMEOUT=600
FLEET_POLL=1
# Ensure the config directory and file exist, with safe permissions.
fleet_config_init()
{
if [ ! -d "$FLEET_CONFIG_DIR" ]; then
mkdir -p "$FLEET_CONFIG_DIR"
fi
if [ ! -f "$FLEET_CONFIG" ]; then
printf '{"devices":{}}\n' > "$FLEET_CONFIG"
chmod 600 "$FLEET_CONFIG"
fi
}
# Usage: fleet_auth <name>
#
# Print "user:password" for a device. Falls back to $LLSSH_USER:$LLSSH_PASS
# (set by the -A flag) if no password is stored for the device.
fleet_auth()
{
local name="$1"
local user pass
user=$(jq -r --arg n "$name" '.devices[$n].user // "admin"' "$FLEET_CONFIG")
pass=$(jq -r --arg n "$name" '.devices[$n].password // ""' "$FLEET_CONFIG")
if [ -z "$pass" ]; then
if [ -n "$LLSSH_PASS" ]; then
user="${LLSSH_USER:-admin}"
pass="$LLSSH_PASS"
else
printf "Error: no password for '%s' (use -A or enroll with -w)\n" \
"$name" >&2
return 1
fi
fi
printf "%s:%s" "$user" "$pass"
}
# Usage: fleet_resolve <target>
#
# Print the device name(s) matching <target>, one per line. <target> may be
# a device name or a profile name. Exits non-zero with a message if neither.
fleet_resolve()
{
local target="$1"
local names
fleet_config_init
if jq -e --arg n "$target" '.devices[$n]' "$FLEET_CONFIG" >/dev/null 2>&1; then
printf "%s\n" "$target"
return 0
fi
names=$(jq -r --arg p "$target" \
'.devices | to_entries[]
| select(.value.profile == $p) | .key' \
"$FLEET_CONFIG")
if [ -n "$names" ]; then
printf "%s\n" "$names"
return 0
fi
printf "Error: '%s' is not a known device name or profile\n" "$target" >&2
return 1
}
# Usage: fleet_rc_get <address> <auth> <path>
#
# RESTCONF GET, returns response body.
fleet_rc_get()
{
local addr="$1" auth="$2" path="$3"
curl -ks \
-u "$auth" \
-H "Accept: application/yang-data+json" \
"https://${addr}${path}"
}
# Usage: fleet_rc_post <address> <auth> <path> [<json-body>]
#
# RESTCONF POST, returns response body.
fleet_rc_post()
{
local addr="$1" auth="$2" path="$3" data="${4:-}"
if [ -n "$data" ]; then
curl -ks -X POST \
-u "$auth" \
-H "Content-Type: application/yang-data+json" \
-H "Accept: application/yang-data+json" \
-d "$data" \
"https://${addr}${path}"
else
curl -ks -X POST \
-u "$auth" \
-H "Content-Type: application/yang-data+json" \
-H "Accept: application/yang-data+json" \
"https://${addr}${path}"
fi
}
# Usage: fleet_rc_error <response>
#
# If <response> contains a RESTCONF error, print the message and return 0.
# Returns 1 (no error) otherwise.
fleet_rc_error()
{
local resp="$1"
local msg
msg=$(printf "%s" "$resp" | \
jq -r 'if ."ietf-restconf:errors" then
."ietf-restconf:errors".error[0]["error-message"]
// "unknown error"
else empty end' 2>/dev/null)
if [ -n "$msg" ]; then
printf "%s" "$msg"
return 0
fi
return 1
}
# Usage: fleet_draw_bar <name> <pct> <msg>
#
# Print one progress bar line, clearing to end of line first.
fleet_draw_bar()
{
local name="$1" pct="$2" msg="$3"
local filled empty bar i
filled=$(( pct * FLEET_BAR_WIDTH / 100 ))
empty=$(( FLEET_BAR_WIDTH - filled ))
bar=""
i=0
while [ "$i" -lt "$filled" ]; do
bar="${bar}="
i=$(( i + 1 ))
done
if [ "$filled" -lt "$FLEET_BAR_WIDTH" ]; then
bar="${bar}>"
empty=$(( empty - 1 ))
fi
i=0
while [ "$i" -lt "$empty" ]; do
bar="${bar} "
i=$(( i + 1 ))
done
msg=$(printf "%.40s" "$msg")
printf "\033[K%-16s [%s] %3d%% %s\n" "$name" "$bar" "$pct" "$msg"
}
# Usage: _fleet_upgrade_one <name> <addr> <auth> <url> <tmpdir>
#
# Background worker: POST install-bundle then poll installer state, writing
# results to <tmpdir>/<name>.{pct,msg,err}.
_fleet_upgrade_one()
{
local name="$1" addr="$2" auth="$3" url="$4" tmpdir="$5"
local resp err state pct msg elapsed
set +e # handle errors explicitly within this background worker
resp=$(fleet_rc_post "$addr" "$auth" \
"/restconf/operations/infix-system:install-bundle" \
"{\"infix-system:input\":{\"url\":\"$url\"}}")
if err=$(fleet_rc_error "$resp"); then
printf "%s" "$err" > "${tmpdir}/${name}.err"
return 1
fi
elapsed=0
while [ "$elapsed" -lt "$FLEET_TIMEOUT" ]; do
state=$(fleet_rc_get "$addr" "$auth" \
"/restconf/data/ietf-system:system-state/infix-system:software/installer")
pct=$(printf "%s" "$state" | \
jq -r '.["infix-system:installer"].progress.percentage // 0')
msg=$(printf "%s" "$state" | \
jq -r '.["infix-system:installer"].progress.message // ""')
err=$(printf "%s" "$state" | \
jq -r '.["infix-system:installer"]["last-error"] // ""')
[ "$pct" = "null" ] && pct=0
[ "$msg" = "null" ] && msg=""
[ "$err" = "null" ] && err=""
if [ -n "$err" ]; then
printf "%s" "$err" > "${tmpdir}/${name}.err"
return 1
fi
# Atomic update of status files
printf "%s" "$pct" > "${tmpdir}/${name}.pct.tmp" \
&& mv "${tmpdir}/${name}.pct.tmp" "${tmpdir}/${name}.pct"
printf "%s" "$msg" > "${tmpdir}/${name}.msg.tmp" \
&& mv "${tmpdir}/${name}.msg.tmp" "${tmpdir}/${name}.msg"
[ "$pct" -ge 100 ] && return 0
sleep "$FLEET_POLL"
elapsed=$(( elapsed + FLEET_POLL ))
done
printf "Timeout after %d seconds" "$elapsed" > "${tmpdir}/${name}.err"
return 1
}
# Usage: fleet_upgrade <name|profile> <url>
#
# Upgrade one device or all devices in a profile, in parallel, showing a
# live per-device progress bar for each.
fleet_upgrade()
{
local target="$1" url="$2"
if [ -z "$target" ] || [ -z "$url" ]; then
printf "Usage: ixll fleet upgrade <name|profile> <url>\n" >&2
return 1
fi
fleet_config_init
local devices name addr auth tmpdir ndevices first pct msg err all_done
if ! devices=$(fleet_resolve "$target"); then
return 1
fi
tmpdir=$(mktemp -d)
active="" # space-separated list of devices actually started
# Initialise status files and launch one background poller per device.
for name in $devices; do
addr=$(jq -r --arg n "$name" '.devices[$n].address' "$FLEET_CONFIG")
if ! auth=$(fleet_auth "$name" 2>/dev/null); then
printf "Skipping %s: no credentials\n" "$name" >&2
continue
fi
printf "0" > "${tmpdir}/${name}.pct"
printf "Starting." > "${tmpdir}/${name}.msg"
printf "" > "${tmpdir}/${name}.err"
active="${active} ${name}"
_fleet_upgrade_one "$name" "$addr" "$auth" "$url" "$tmpdir" &
done
active="${active# }" # strip leading space
ndevices=$(printf "%s\n" "$active" | wc -w)
if [ "$ndevices" -eq 0 ]; then
printf "No devices to upgrade.\n" >&2
rm -rf "$tmpdir"
return 1
fi
# Display loop: redraw all bars in place until every device is done.
# Uses $active (not $devices) so skipped devices don't affect line count.
first=1
while true; do
[ "$first" -eq 0 ] && printf "\033[%dA" "$ndevices"
first=0
all_done=1
for name in $active; do
pct=$(cat "${tmpdir}/${name}.pct" 2>/dev/null || printf "0")
msg=$(cat "${tmpdir}/${name}.msg" 2>/dev/null || printf "Starting.")
err=$(cat "${tmpdir}/${name}.err" 2>/dev/null || printf "")
if [ -n "$err" ]; then
printf "\033[K%-16s \033[31m[FAILED]\033[0m %s\n" "$name" "$err"
elif [ "$pct" -ge 100 ] 2>/dev/null; then
printf "\033[K%-16s \033[32m[done]\033[0m\n" "$name"
else
fleet_draw_bar "$name" "$pct" "$msg"
all_done=0
fi
done
[ "$all_done" -eq 1 ] && break
sleep 1
done
wait || true
rm -rf "$tmpdir"
}
# Usage: fleet_backup [-o <dir>] <name|profile>
#
# Save the startup-config from one device or all devices in a profile.
# Single-device target: saved to current directory.
# Profile target: saved to ~/.config/infix/<profile>/<name>/.
fleet_backup()
{
local outdir=
OPTIND=1
while getopts "o:" opt; do
case $opt in
o) outdir="$OPTARG" ;;
*) ;;
esac
done
shift $(( OPTIND - 1 ))
local target="$1"
if [ -z "$target" ]; then
printf "Usage: ixll fleet backup [-o <dir>] <name|profile>\n" >&2
return 1
fi
fleet_config_init
# Determine if target is a single enrolled device or a profile.
local single_device=0
if jq -e --arg n "$target" '.devices[$n]' "$FLEET_CONFIG" \
>/dev/null 2>&1; then
single_device=1
fi
local devices name addr auth profile iso_date outfile resp errmsg
if ! devices=$(fleet_resolve "$target"); then
return 1
fi
for name in $devices; do
addr=$(jq -r --arg n "$name" '.devices[$n].address' "$FLEET_CONFIG")
if ! auth=$(fleet_auth "$name" 2>/dev/null); then
printf "Skipping %s: no credentials\n" "$name" >&2
continue
fi
profile=$(jq -r --arg n "$name" '.devices[$n].profile // ""' \
"$FLEET_CONFIG")
iso_date=$(date +%Y-%m-%dT%H:%M:%S)
if [ -n "$outdir" ]; then
outfile="${outdir}/startup-config-${iso_date}.cfg"
elif [ "$single_device" -eq 1 ]; then
outfile="startup-config-${iso_date}.cfg"
else
outfile="${FLEET_CONFIG_DIR}/${profile}/${name}/startup-config-${iso_date}.cfg"
mkdir -p "$(dirname "$outfile")"
fi
printf "Backing up %-16s ... " "$name"
resp=$(fleet_rc_get "$addr" "$auth" "/restconf/ds/ietf-datastores:startup")
if errmsg=$(fleet_rc_error "$resp"); then
printf "\033[31mFAILED\033[0m: %s\n" "$errmsg"
continue
fi
printf "%s" "$resp" | jq . > "$outfile"
printf "saved to %s\n" "$outfile"
done
}
# Usage: fleet_reboot <name|profile>
#
# Reboot one device or all devices in a profile (fire-and-forget).
fleet_reboot()
{
local target="$1"
if [ -z "$target" ]; then
printf "Usage: ixll fleet reboot <name|profile>\n" >&2
return 1
fi
fleet_config_init
local devices name addr auth
if ! devices=$(fleet_resolve "$target"); then
return 1
fi
for name in $devices; do
addr=$(jq -r --arg n "$name" '.devices[$n].address' "$FLEET_CONFIG")
if ! auth=$(fleet_auth "$name" 2>/dev/null); then
printf "Skipping %s: no credentials\n" "$name" >&2
continue
fi
printf "Rebooting %-16s ... " "$name"
fleet_rc_post "$addr" "$auth" \
"/restconf/operations/ietf-system:system-restart" \
>/dev/null 2>&1
printf "done\n"
done
}
# Usage: fleet_enroll [-d] [-p <profile>] [-u <user>] [-w <password>]
# <name> [<address>]
#
# Enroll a device, or delete it with -d.
fleet_enroll()
{
local delete=0 profile= user=admin password=
OPTIND=1
while getopts "dp:u:w:" opt; do
case $opt in
d) delete=1 ;;
p) profile="$OPTARG" ;;
u) user="$OPTARG" ;;
w) password="$OPTARG" ;;
*)
printf "Usage: ixll fleet enroll [-d] [-p profile] [-u user] [-w password] <name> [<address>]\n" >&2
return 1
;;
esac
done
shift $(( OPTIND - 1 ))
local name="$1" address="$2"
if [ -z "$name" ]; then
printf "Usage: ixll fleet enroll [-d] [-p profile] [-u user] [-w password] <name> [<address>]\n" >&2
return 1
fi
fleet_config_init
if [ "$delete" -eq 1 ]; then
if ! jq -e --arg n "$name" '.devices[$n]' "$FLEET_CONFIG" \
>/dev/null 2>&1; then
printf "Error: device '%s' not found\n" "$name" >&2
return 1
fi
jq --arg n "$name" 'del(.devices[$n])' \
"$FLEET_CONFIG" > "${FLEET_CONFIG}.tmp" \
&& mv "${FLEET_CONFIG}.tmp" "$FLEET_CONFIG"
printf "Device '%s' removed.\n" "$name"
return 0
fi
if [ -z "$address" ]; then
printf "Usage: ixll fleet enroll [-p profile] [-u user] [-w password] <name> <address>\n" >&2
return 1
fi
jq --arg n "$name" \
--arg a "$address" \
--arg u "$user" \
--arg p "${profile:-}" \
--arg w "${password:-}" \
'.devices[$n] = {address: $a, user: $u}
| if $p != "" then .devices[$n].profile = $p else . end
| if $w != "" then .devices[$n].password = $w else . end' \
"$FLEET_CONFIG" > "${FLEET_CONFIG}.tmp" \
&& mv "${FLEET_CONFIG}.tmp" "$FLEET_CONFIG"
printf "Device '%s' enrolled at %s.\n" "$name" "$address"
}
# Usage: fleet_list
#
# List all enrolled devices.
fleet_list()
{
fleet_config_init
local count
count=$(jq '.devices | length' "$FLEET_CONFIG")
if [ "$count" -eq 0 ]; then
printf "No devices enrolled. Use 'ixll fleet enroll' to add one.\n"
return 0
fi
printf "\033[7m%-12s %-16s %-22s %-8s %s\033[0m\n" \
"PROFILE" "NAME" "ADDRESS" "USER" "PASS"
jq -r '.devices | to_entries[] |
[(.value.profile // "(none)"),
.key,
.value.address,
(.value.user // "admin"),
(if .value.password then "*" else "-" end)] |
join("\t")' "$FLEET_CONFIG" | \
while IFS=$(printf '\t') read -r profile name address user pass; do
printf "%-12s %-16s %-22s %-8s %s\n" \
"$profile" "$name" "$address" "$user" "$pass"
done
}
# Usage: fleet_main <subcommand> [<args>]
#
# Dispatch fleet subcommands.
fleet_main()
{
if [ $# -lt 1 ]; then
printf "Usage: ixll fleet <enroll|list|upgrade|backup|reboot>\n" >&2
return 1
fi
local subcmd="$1"
shift
case "$subcmd" in
enroll) fleet_enroll "$@" ;;
list) fleet_list "$@" ;;
upgrade) fleet_upgrade "$@" ;;
backup) fleet_backup "$@" ;;
reboot) fleet_reboot "$@" ;;
*)
printf "Unknown fleet command '%s'\n" "$subcmd" >&2
return 1
;;
esac
}
+18 -12
View File
@@ -202,32 +202,39 @@ llscan_mdns()
flags="-tarpk"
fi
avahi-browse $flags | awk -F';' -v show_all="$all" '
avahi-browse $flags 2>/dev/null | awk -F';' -v show_all="$all" '
$1 == "=" {
host = $7
host = $7
proto = $3
addr = $8
txt = $10
addr = $8
stype = $5
txt = $10
on = ""; ov = ""; product = ""; serial = ""; devid = ""
on = ""; ov = ""; product = ""; devid = ""; vv = ""
n = split(txt, parts, "\" \"")
for (i = 1; i <= n; i++) {
gsub(/"/, "", parts[i])
if (parts[i] ~ /^on=/) { split(parts[i], kv, "="); on = kv[2] }
else if (parts[i] ~ /^ov=/) { split(parts[i], kv, "="); ov = kv[2] }
else if (parts[i] ~ /^product=/) { split(parts[i], kv, "="); product = kv[2] }
else if (parts[i] ~ /^serial=/) { split(parts[i], kv, "="); serial = kv[2] }
else if (parts[i] ~ /^deviceid=/) { split(parts[i], kv, "="); devid = kv[2] }
else if (parts[i] ~ /^vv=/) { split(parts[i], kv, "="); vv = kv[2] }
}
if (!show_all && on != "Infix") next
# vv=1 is the platform marker set by confd/services.c, survives OS rebranding.
# We require it together with a management service type to avoid false
# positives from Apple devices, which also use vv=1 in AirPlay/RAOP records.
# on=Infix is kept as a fallback for older firmware that predates vv=1.
mgmt = (stype == "_ssh._tcp" || stype == "_sftp-ssh._tcp" || \
stype == "_https._tcp" || stype == "_http._tcp" || \
stype == "_netconf-ssh._tcp" || stype == "_restconf-tls._tcp")
if (!show_all && !(vv == "1" && mgmt) && on != "Infix") next
# Use deviceid (MAC) as unique key; fall back to hostname
key = devid ? devid : host
if (!product) product = on ? on : "-"
if (!ov) ov = "-"
if (!serial || serial == "null") serial = "-"
# Prefer IPv4 for display address
if (proto == "IPv4") {
@@ -241,7 +248,6 @@ llscan_mdns()
hosts[key] = host
products[key] = product
versions[key] = ov
serials[key] = serial
} else if (length(host) > length(hosts[key])) {
# Prefer the unique hostname (e.g., infix-c0-ff-ee.local)
# over the generic one (e.g., infix.local)
@@ -258,8 +264,8 @@ llscan_mdns()
exit 1
}
fmt = "%-26s %-18s %-24s %-22s %s\n"
hdr = sprintf(fmt, "HOSTNAME", "ADDRESS", "PRODUCT", "VERSION", "SERIAL")
fmt = "%-26s %-18s %-24s %-30s\n"
hdr = sprintf(fmt, "HOSTNAME", "ADDRESS", "PRODUCT", "VERSION")
sub(/\n$/, "", hdr)
printf "\033[7m%s\033[0m\n", hdr
@@ -268,7 +274,7 @@ llscan_mdns()
a = (ipv4[k] ? ipv4[k] : (ipv6[k] ? ipv6[k] : "-"))
p = products[k]; if (length(p) > 23) p = substr(p, 1, 22) "~"
v = versions[k]; if (length(v) > 21) v = substr(v, 1, 20) "~"
printf fmt, hosts[k], a, p, v, serials[k]
printf fmt, hosts[k], a, p, v
}
printf "\n%d device(s) found.\n", ndevs