mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-23 17:33:01 +02:00
@@ -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,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 --
|
||||
|
||||
@@ -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,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))
|
||||
|
||||
@@ -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
-3
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
+95
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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("&", fp); break;
|
||||
case '<': fputs("<", fp); break;
|
||||
case '>': fputs(">", fp); break;
|
||||
case '"': fputs(""", 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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
__pycache__/
|
||||
netbrowse
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
recursive-include netbrowse/templates *
|
||||
recursive-include netbrowse/static *
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/kernelkit/infix/src/netbrowse
|
||||
|
||||
go 1.21
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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)">▶</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 ? '▶' : '▼';
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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.
Binary file not shown.
@@ -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 |
@@ -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"
|
||||
```
|
||||
@@ -2,13 +2,15 @@ DISTCLEANFILES = *~ *.d
|
||||
ACLOCAL_AMFLAGS = -I m4
|
||||
|
||||
sbin_PROGRAMS = statd
|
||||
statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h
|
||||
statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h avahi.c avahi.h
|
||||
statd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
|
||||
statd_CFLAGS = -W -Wall -Wextra
|
||||
statd_CFLAGS += $(jansson_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS)
|
||||
statd_CFLAGS += $(libsrx_CFLAGS) $(libite_CFLAGS)
|
||||
statd_CFLAGS += $(avahi_client_CFLAGS)
|
||||
statd_LDADD = $(jansson_LIBS) $(libyang_LIBS) $(sysrepo_LIBS)
|
||||
statd_LDADD += $(libsrx_LIBS) $(libite_LIBS) $(EV_LIBS) -lz
|
||||
statd_LDADD += $(avahi_client_LIBS)
|
||||
|
||||
# Test stub for journal retention policy (no dependencies, standalone)
|
||||
noinst_PROGRAMS = journal_retention_stub
|
||||
|
||||
@@ -0,0 +1,817 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
/*
|
||||
* avahi.c - mDNS neighbor table for statd using libavahi-client + libev.
|
||||
*
|
||||
* Discovery flow:
|
||||
* 1. AvahiServiceTypeBrowser finds all service types on the local network.
|
||||
* 2. For each type, an AvahiServiceBrowser enumerates service instances.
|
||||
* 3. For each instance, a transient AvahiServiceResolver resolves hostname,
|
||||
* address, port and TXT records.
|
||||
* 4. Resolved data is pushed to SR_DS_OPERATIONAL under
|
||||
* /infix-services:mdns/neighbors via sr_set_item_str() + sr_apply_changes().
|
||||
* 5. On BROWSER_REMOVE, the corresponding DS subtree is deleted.
|
||||
*
|
||||
* The AvahiPoll vtable bridges avahi's event model to the main libev loop
|
||||
* (same thread — no locking required).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/queue.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <ev.h>
|
||||
#include <avahi-client/client.h>
|
||||
#include <avahi-client/lookup.h>
|
||||
#include <avahi-common/address.h>
|
||||
#include <avahi-common/error.h>
|
||||
#include <avahi-common/watch.h>
|
||||
#include <sysrepo.h>
|
||||
|
||||
#include <srx/common.h>
|
||||
|
||||
#include "avahi.h"
|
||||
|
||||
/* Complete the opaque avahi types declared in avahi-common/watch.h */
|
||||
struct AvahiWatch {
|
||||
ev_io io; /* MUST be first (cast from ev_io *) */
|
||||
AvahiWatchEvent last_event;
|
||||
AvahiWatchCallback callback;
|
||||
void *userdata;
|
||||
struct avahi_ctx *ctx;
|
||||
};
|
||||
|
||||
struct AvahiTimeout {
|
||||
ev_timer timer; /* MUST be first */
|
||||
AvahiTimeoutCallback callback;
|
||||
void *userdata;
|
||||
struct avahi_ctx *ctx;
|
||||
};
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* libev-backed AvahiPoll vtable
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
static void watch_io_cb(struct ev_loop *loop, ev_io *w, int events)
|
||||
{
|
||||
struct AvahiWatch *watch = (struct AvahiWatch *)w;
|
||||
AvahiWatchEvent av = 0;
|
||||
|
||||
(void)loop;
|
||||
if (events & EV_READ) av |= AVAHI_WATCH_IN;
|
||||
if (events & EV_WRITE) av |= AVAHI_WATCH_OUT;
|
||||
if (events & EV_ERROR) av |= AVAHI_WATCH_ERR;
|
||||
watch->last_event = av;
|
||||
watch->callback(watch, w->fd, av, watch->userdata);
|
||||
}
|
||||
|
||||
static AvahiWatch *watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent event,
|
||||
AvahiWatchCallback callback, void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = api->userdata;
|
||||
struct AvahiWatch *w;
|
||||
int ev_events = 0;
|
||||
|
||||
w = calloc(1, sizeof(*w));
|
||||
if (!w)
|
||||
return NULL;
|
||||
|
||||
w->callback = callback;
|
||||
w->userdata = userdata;
|
||||
w->ctx = ctx;
|
||||
|
||||
if (event & AVAHI_WATCH_IN) ev_events |= EV_READ;
|
||||
if (event & AVAHI_WATCH_OUT) ev_events |= EV_WRITE;
|
||||
|
||||
ev_io_init(&w->io, watch_io_cb, fd, ev_events);
|
||||
if (ev_events)
|
||||
ev_io_start(ctx->loop, &w->io);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
static void watch_update(AvahiWatch *w, AvahiWatchEvent event)
|
||||
{
|
||||
int ev_events = 0;
|
||||
|
||||
ev_io_stop(w->ctx->loop, &w->io);
|
||||
if (event & AVAHI_WATCH_IN) ev_events |= EV_READ;
|
||||
if (event & AVAHI_WATCH_OUT) ev_events |= EV_WRITE;
|
||||
ev_io_set(&w->io, w->io.fd, ev_events);
|
||||
if (ev_events)
|
||||
ev_io_start(w->ctx->loop, &w->io);
|
||||
}
|
||||
|
||||
static AvahiWatchEvent watch_get_events(AvahiWatch *w)
|
||||
{
|
||||
return w->last_event;
|
||||
}
|
||||
|
||||
static void watch_free(AvahiWatch *w)
|
||||
{
|
||||
ev_io_stop(w->ctx->loop, &w->io);
|
||||
free(w);
|
||||
}
|
||||
|
||||
static void timeout_cb(struct ev_loop *loop, ev_timer *t, int events)
|
||||
{
|
||||
struct AvahiTimeout *timeout = (struct AvahiTimeout *)t;
|
||||
|
||||
(void)loop;
|
||||
(void)events;
|
||||
timeout->callback(timeout, timeout->userdata);
|
||||
}
|
||||
|
||||
static AvahiTimeout *timeout_new(const AvahiPoll *api, const struct timeval *tv,
|
||||
AvahiTimeoutCallback callback, void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = api->userdata;
|
||||
struct AvahiTimeout *t;
|
||||
|
||||
t = calloc(1, sizeof(*t));
|
||||
if (!t)
|
||||
return NULL;
|
||||
|
||||
t->callback = callback;
|
||||
t->userdata = userdata;
|
||||
t->ctx = ctx;
|
||||
|
||||
ev_timer_init(&t->timer, timeout_cb, 0.0, 0.0);
|
||||
|
||||
if (tv) {
|
||||
struct timeval now;
|
||||
double delay;
|
||||
|
||||
gettimeofday(&now, NULL);
|
||||
delay = (double)(tv->tv_sec - now.tv_sec) +
|
||||
(double)(tv->tv_usec - now.tv_usec) / 1e6;
|
||||
if (delay < 0.0)
|
||||
delay = 0.0;
|
||||
ev_timer_set(&t->timer, delay, 0.0);
|
||||
ev_timer_start(ctx->loop, &t->timer);
|
||||
}
|
||||
/* NULL tv means disabled timer — do not start */
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
static void timeout_update(AvahiTimeout *t, const struct timeval *tv)
|
||||
{
|
||||
ev_timer_stop(t->ctx->loop, &t->timer);
|
||||
|
||||
if (tv) {
|
||||
struct timeval now;
|
||||
double delay;
|
||||
|
||||
gettimeofday(&now, NULL);
|
||||
delay = (double)(tv->tv_sec - now.tv_sec) +
|
||||
(double)(tv->tv_usec - now.tv_usec) / 1e6;
|
||||
if (delay < 0.0)
|
||||
delay = 0.0;
|
||||
ev_timer_set(&t->timer, delay, 0.0);
|
||||
ev_timer_start(t->ctx->loop, &t->timer);
|
||||
}
|
||||
}
|
||||
|
||||
static void timeout_free(AvahiTimeout *t)
|
||||
{
|
||||
ev_timer_stop(t->ctx->loop, &t->timer);
|
||||
free(t);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* In-memory state helpers
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
static struct avahi_neighbor *find_neighbor(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
struct avahi_neighbor *n;
|
||||
|
||||
LIST_FOREACH(n, &ctx->neighbors, link) {
|
||||
if (!strcmp(n->hostname, hostname))
|
||||
return n;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct avahi_neighbor *get_neighbor(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
struct avahi_neighbor *n = find_neighbor(ctx, hostname);
|
||||
|
||||
if (n)
|
||||
return n;
|
||||
|
||||
n = calloc(1, sizeof(*n));
|
||||
if (!n)
|
||||
return NULL;
|
||||
|
||||
snprintf(n->hostname, sizeof(n->hostname), "%s", hostname);
|
||||
LIST_INIT(&n->addrs);
|
||||
LIST_INSERT_HEAD(&ctx->neighbors, n, link);
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
static int has_addr(struct avahi_neighbor *n, const char *addr)
|
||||
{
|
||||
struct avahi_addr *a;
|
||||
|
||||
LIST_FOREACH(a, &n->addrs, link) {
|
||||
if (!strcmp(a->val, addr))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void add_addr(struct avahi_neighbor *n, const char *addr)
|
||||
{
|
||||
struct avahi_addr *a = calloc(1, sizeof(*a));
|
||||
|
||||
if (!a)
|
||||
return;
|
||||
snprintf(a->val, sizeof(a->val), "%s", addr);
|
||||
LIST_INSERT_HEAD(&n->addrs, a, link);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find service in flat list by 5-tuple (ifindex, proto, name, type, domain).
|
||||
*/
|
||||
static struct avahi_service *find_service(struct avahi_ctx *ctx,
|
||||
int ifindex, AvahiProtocol proto,
|
||||
const char *name, const char *type,
|
||||
const char *domain)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
|
||||
LIST_FOREACH(s, &ctx->services, link) {
|
||||
if (s->ifindex == ifindex && s->proto == proto &&
|
||||
!strcmp(s->name, name) && !strcmp(s->type, type) &&
|
||||
!strcmp(s->domain, domain))
|
||||
return s;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check whether any service in the flat list matches (hostname, name) —
|
||||
* used after removing one 5-tuple entry to decide if the DS entry should
|
||||
* be removed too (another interface may still have the same service).
|
||||
*/
|
||||
static int svc_ds_entry_exists(struct avahi_ctx *ctx, const char *hostname, const char *name)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
|
||||
LIST_FOREACH(s, &ctx->services, link) {
|
||||
if (!strcmp(s->hostname, hostname) && !strcmp(s->name, name))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int neighbor_has_services(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
|
||||
LIST_FOREACH(s, &ctx->services, link) {
|
||||
if (!strcmp(s->hostname, hostname))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void free_txts(struct avahi_service *svc)
|
||||
{
|
||||
struct avahi_txt *t;
|
||||
|
||||
while (!LIST_EMPTY(&svc->txts)) {
|
||||
t = LIST_FIRST(&svc->txts);
|
||||
LIST_REMOVE(t, link);
|
||||
free(t);
|
||||
}
|
||||
}
|
||||
|
||||
static void free_service(struct avahi_service *svc)
|
||||
{
|
||||
free_txts(svc);
|
||||
LIST_REMOVE(svc, link);
|
||||
free(svc);
|
||||
}
|
||||
|
||||
static void free_neighbor(struct avahi_neighbor *n)
|
||||
{
|
||||
struct avahi_addr *a;
|
||||
|
||||
while (!LIST_EMPTY(&n->addrs)) {
|
||||
a = LIST_FIRST(&n->addrs);
|
||||
LIST_REMOVE(a, link);
|
||||
free(a);
|
||||
}
|
||||
LIST_REMOVE(n, link);
|
||||
free(n);
|
||||
}
|
||||
|
||||
static void free_all(struct avahi_ctx *ctx)
|
||||
{
|
||||
struct avahi_service *s;
|
||||
struct avahi_neighbor *n;
|
||||
|
||||
while (!LIST_EMPTY(&ctx->services)) {
|
||||
s = LIST_FIRST(&ctx->services);
|
||||
free_service(s);
|
||||
}
|
||||
while (!LIST_EMPTY(&ctx->neighbors)) {
|
||||
n = LIST_FIRST(&ctx->neighbors);
|
||||
free_neighbor(n);
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* sysrepo push helpers
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
#define XPATH_BASE "/infix-services:mdns/neighbors"
|
||||
|
||||
static void format_timestamp(char *buf, size_t sz)
|
||||
{
|
||||
struct tm tm;
|
||||
time_t now = time(NULL);
|
||||
|
||||
gmtime_r(&now, &tm);
|
||||
strftime(buf, sz, "%Y-%m-%dT%H:%M:%S+00:00", &tm);
|
||||
}
|
||||
|
||||
static int sr_setstr(sr_session_ctx_t *ses, const char *xpath, const char *val)
|
||||
{
|
||||
int err = sr_set_item_str(ses, xpath, val, NULL, 0);
|
||||
|
||||
if (err)
|
||||
ERROR("avahi: sr_set_item_str(%s): %s", xpath, sr_strerror(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return an XPath string literal quoting val: single-quoted unless val
|
||||
* contains a single quote, in which case double quotes are used instead.
|
||||
* buf must be at least strlen(val)+3 bytes.
|
||||
*/
|
||||
static const char *xpath_str(char *buf, size_t sz, const char *val)
|
||||
{
|
||||
if (strchr(val, '\''))
|
||||
snprintf(buf, sz, "\"%s\"", val);
|
||||
else
|
||||
snprintf(buf, sz, "'%s'", val);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/*
|
||||
* Push a resolver result to the operational DS.
|
||||
* new_addr is non-NULL only when a new address was just added in memory.
|
||||
*/
|
||||
static void ds_push_resolver(struct avahi_ctx *ctx, struct avahi_service *svc,
|
||||
const char *new_addr)
|
||||
{
|
||||
char qname[258]; /* quoted svc->name for safe XPath predicates */
|
||||
char xpath[640];
|
||||
char val[64];
|
||||
struct avahi_txt *t;
|
||||
char ts[32];
|
||||
int err = 0;
|
||||
|
||||
xpath_str(qname, sizeof(qname), svc->name);
|
||||
|
||||
/* Create neighbor list instance (key embedded in predicate; sysrepo 4.x
|
||||
* rejects editing list-key leaves directly — set the list entry instead) */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']", svc->hostname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, NULL);
|
||||
|
||||
/* address (only if a new one was added) */
|
||||
if (new_addr) {
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/address", svc->hostname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, new_addr);
|
||||
}
|
||||
|
||||
/* last-seen */
|
||||
format_timestamp(ts, sizeof(ts));
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/last-seen", svc->hostname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, ts);
|
||||
|
||||
/* Delete and recreate service entry so TXT records are always fresh */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]",
|
||||
svc->hostname, qname);
|
||||
sr_delete_item(ctx->sr_ses, xpath, 0);
|
||||
|
||||
/* Create service list instance (same pattern — key in predicate) */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, NULL);
|
||||
|
||||
/* service/type */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]/type",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, svc->type);
|
||||
|
||||
/* service/port */
|
||||
snprintf(val, sizeof(val), "%u", (unsigned)svc->port);
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]/port",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, val);
|
||||
|
||||
/* service/txt (leaf-list) */
|
||||
LIST_FOREACH(t, &svc->txts, link) {
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]/txt",
|
||||
svc->hostname, qname);
|
||||
err = err ?: sr_setstr(ctx->sr_ses, xpath, t->val);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
sr_discard_changes(ctx->sr_ses);
|
||||
return;
|
||||
}
|
||||
|
||||
err = sr_apply_changes(ctx->sr_ses, 0);
|
||||
if (err)
|
||||
ERROR("avahi: sr_apply_changes: %s", sr_strerror(err));
|
||||
}
|
||||
|
||||
static void ds_delete_service(struct avahi_ctx *ctx, const char *hostname, const char *name)
|
||||
{
|
||||
char qname[258];
|
||||
char xpath[512];
|
||||
|
||||
xpath_str(qname, sizeof(qname), name);
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']/service[name=%s]",
|
||||
hostname, qname);
|
||||
sr_delete_item(ctx->sr_ses, xpath, 0);
|
||||
}
|
||||
|
||||
static void ds_delete_neighbor(struct avahi_ctx *ctx, const char *hostname)
|
||||
{
|
||||
char xpath[512];
|
||||
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
XPATH_BASE "/neighbor[hostname='%s']", hostname);
|
||||
sr_delete_item(ctx->sr_ses, xpath, 0);
|
||||
}
|
||||
|
||||
static void ds_clear_all(struct avahi_ctx *ctx)
|
||||
{
|
||||
sr_delete_item(ctx->sr_ses, XPATH_BASE, 0);
|
||||
sr_apply_changes(ctx->sr_ses, 0);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Avahi callbacks
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
static void resolver_cb(AvahiServiceResolver *r,
|
||||
AvahiIfIndex iface, AvahiProtocol proto,
|
||||
AvahiResolverEvent event,
|
||||
const char *name, const char *type, const char *domain,
|
||||
const char *hostname, const AvahiAddress *addr,
|
||||
uint16_t port, AvahiStringList *txtlist,
|
||||
AvahiLookupResultFlags flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
char addrstr[AVAHI_ADDRESS_STR_MAX] = "";
|
||||
struct avahi_neighbor *n;
|
||||
struct avahi_service *svc;
|
||||
const char *new_addr = NULL;
|
||||
AvahiStringList *s;
|
||||
struct avahi_txt *t;
|
||||
int is_loopback;
|
||||
|
||||
(void)flags;
|
||||
|
||||
if (event != AVAHI_RESOLVER_FOUND)
|
||||
goto done;
|
||||
|
||||
if (addr)
|
||||
avahi_address_snprint(addrstr, sizeof(addrstr), addr);
|
||||
|
||||
is_loopback = (!strcmp(addrstr, "127.0.0.1") ||
|
||||
!strcmp(addrstr, "::1") ||
|
||||
!strncmp(addrstr, "127.", 4));
|
||||
|
||||
/* Find or create neighbor (tracks addresses) */
|
||||
n = get_neighbor(ctx, hostname);
|
||||
if (!n) {
|
||||
ERROR("avahi: out of memory for neighbor '%s'", hostname);
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Add address only if new and not loopback */
|
||||
if (!is_loopback && addrstr[0] && !has_addr(n, addrstr)) {
|
||||
add_addr(n, addrstr);
|
||||
new_addr = addrstr;
|
||||
}
|
||||
|
||||
/* Find or create service entry in flat list */
|
||||
svc = find_service(ctx, iface, proto, name, type, domain);
|
||||
if (!svc) {
|
||||
svc = calloc(1, sizeof(*svc));
|
||||
if (!svc) {
|
||||
ERROR("avahi: out of memory for service '%s'", name);
|
||||
goto done;
|
||||
}
|
||||
svc->ifindex = iface;
|
||||
svc->proto = proto;
|
||||
snprintf(svc->name, sizeof(svc->name), "%s", name);
|
||||
snprintf(svc->type, sizeof(svc->type), "%s", type);
|
||||
snprintf(svc->domain, sizeof(svc->domain), "%s", domain);
|
||||
snprintf(svc->hostname, sizeof(svc->hostname), "%s", hostname);
|
||||
LIST_INIT(&svc->txts);
|
||||
LIST_INSERT_HEAD(&ctx->services, svc, link);
|
||||
} else {
|
||||
free_txts(svc);
|
||||
}
|
||||
|
||||
svc->port = port;
|
||||
|
||||
/* Copy TXT records verbatim */
|
||||
for (s = txtlist; s; s = avahi_string_list_get_next(s)) {
|
||||
uint8_t *data = avahi_string_list_get_text(s);
|
||||
size_t len = avahi_string_list_get_size(s);
|
||||
|
||||
t = calloc(1, sizeof(*t));
|
||||
if (!t)
|
||||
break;
|
||||
snprintf(t->val, sizeof(t->val), "%.*s", (int)len, (char *)data);
|
||||
LIST_INSERT_HEAD(&svc->txts, t, link);
|
||||
}
|
||||
|
||||
ds_push_resolver(ctx, svc, new_addr);
|
||||
|
||||
done:
|
||||
avahi_service_resolver_free(r);
|
||||
}
|
||||
|
||||
static void service_browser_cb(AvahiServiceBrowser *b,
|
||||
AvahiIfIndex iface, AvahiProtocol proto,
|
||||
AvahiBrowserEvent event,
|
||||
const char *name, const char *type, const char *domain,
|
||||
AvahiLookupResultFlags flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
|
||||
(void)b;
|
||||
(void)flags;
|
||||
|
||||
switch (event) {
|
||||
case AVAHI_BROWSER_NEW:
|
||||
if (!avahi_service_resolver_new(ctx->client, iface, proto,
|
||||
name, type, domain,
|
||||
AVAHI_PROTO_UNSPEC, 0,
|
||||
resolver_cb, ctx))
|
||||
DEBUG("avahi: resolver_new(%s) failed: %s", name,
|
||||
avahi_strerror(avahi_client_errno(ctx->client)));
|
||||
break;
|
||||
|
||||
case AVAHI_BROWSER_REMOVE: {
|
||||
struct avahi_service *svc;
|
||||
char hostname[256];
|
||||
char svc_name[256];
|
||||
|
||||
svc = find_service(ctx, iface, proto, name, type, domain);
|
||||
if (!svc)
|
||||
break;
|
||||
|
||||
snprintf(hostname, sizeof(hostname), "%s", svc->hostname);
|
||||
snprintf(svc_name, sizeof(svc_name), "%s", svc->name);
|
||||
free_service(svc);
|
||||
|
||||
/* Remove DS service entry if no other iface/proto instance remains */
|
||||
if (!svc_ds_entry_exists(ctx, hostname, svc_name)) {
|
||||
ds_delete_service(ctx, hostname, svc_name);
|
||||
|
||||
/* Remove neighbor if it has no more services */
|
||||
if (!neighbor_has_services(ctx, hostname)) {
|
||||
ds_delete_neighbor(ctx, hostname);
|
||||
struct avahi_neighbor *n = find_neighbor(ctx, hostname);
|
||||
if (n)
|
||||
free_neighbor(n);
|
||||
}
|
||||
}
|
||||
|
||||
sr_apply_changes(ctx->sr_ses, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
case AVAHI_BROWSER_ALL_FOR_NOW:
|
||||
case AVAHI_BROWSER_CACHE_EXHAUSTED:
|
||||
case AVAHI_BROWSER_FAILURE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void type_browser_cb(AvahiServiceTypeBrowser *b,
|
||||
AvahiIfIndex iface, AvahiProtocol proto,
|
||||
AvahiBrowserEvent event,
|
||||
const char *type, const char *domain,
|
||||
AvahiLookupResultFlags flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
|
||||
(void)b;
|
||||
(void)flags;
|
||||
|
||||
switch (event) {
|
||||
case AVAHI_BROWSER_NEW: {
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
/* Only create one browser per service type */
|
||||
LIST_FOREACH(te, &ctx->type_entries, link) {
|
||||
if (!strcmp(te->type, type))
|
||||
return;
|
||||
}
|
||||
|
||||
te = calloc(1, sizeof(*te));
|
||||
if (!te)
|
||||
return;
|
||||
|
||||
snprintf(te->type, sizeof(te->type), "%s", type);
|
||||
te->browser = avahi_service_browser_new(ctx->client,
|
||||
AVAHI_IF_UNSPEC,
|
||||
AVAHI_PROTO_UNSPEC,
|
||||
type, domain,
|
||||
0,
|
||||
service_browser_cb, ctx);
|
||||
if (!te->browser) {
|
||||
DEBUG("avahi: service_browser_new(%s) failed: %s", type,
|
||||
avahi_strerror(avahi_client_errno(ctx->client)));
|
||||
free(te);
|
||||
return;
|
||||
}
|
||||
|
||||
LIST_INSERT_HEAD(&ctx->type_entries, te, link);
|
||||
DEBUG("avahi: browsing service type %s", type);
|
||||
break;
|
||||
}
|
||||
|
||||
case AVAHI_BROWSER_REMOVE: {
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
LIST_FOREACH(te, &ctx->type_entries, link) {
|
||||
if (!strcmp(te->type, type)) {
|
||||
avahi_service_browser_free(te->browser);
|
||||
LIST_REMOVE(te, link);
|
||||
free(te);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AVAHI_BROWSER_ALL_FOR_NOW:
|
||||
case AVAHI_BROWSER_CACHE_EXHAUSTED:
|
||||
case AVAHI_BROWSER_FAILURE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
|
||||
{
|
||||
struct avahi_ctx *ctx = userdata;
|
||||
|
||||
ctx->client = c;
|
||||
|
||||
switch (state) {
|
||||
case AVAHI_CLIENT_S_RUNNING:
|
||||
INFO("avahi: client running");
|
||||
if (ctx->type_browser)
|
||||
break; /* Already browsing */
|
||||
|
||||
ctx->type_browser = avahi_service_type_browser_new(
|
||||
ctx->client,
|
||||
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
|
||||
NULL, /* domain = NULL → "local" */
|
||||
0,
|
||||
type_browser_cb, ctx);
|
||||
if (!ctx->type_browser)
|
||||
ERROR("avahi: service_type_browser_new failed: %s",
|
||||
avahi_strerror(avahi_client_errno(ctx->client)));
|
||||
break;
|
||||
|
||||
case AVAHI_CLIENT_FAILURE:
|
||||
ERROR("avahi: client failure: %s",
|
||||
avahi_strerror(avahi_client_errno(c)));
|
||||
|
||||
/*
|
||||
* Browsers are internally invalidated when the daemon dies.
|
||||
* Free them explicitly here so they're recreated on reconnect.
|
||||
*/
|
||||
{
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
while (!LIST_EMPTY(&ctx->type_entries)) {
|
||||
te = LIST_FIRST(&ctx->type_entries);
|
||||
avahi_service_browser_free(te->browser);
|
||||
LIST_REMOVE(te, link);
|
||||
free(te);
|
||||
}
|
||||
}
|
||||
if (ctx->type_browser) {
|
||||
avahi_service_type_browser_free(ctx->type_browser);
|
||||
ctx->type_browser = NULL;
|
||||
}
|
||||
|
||||
free_all(ctx);
|
||||
ds_clear_all(ctx);
|
||||
break;
|
||||
|
||||
case AVAHI_CLIENT_S_COLLISION:
|
||||
case AVAHI_CLIENT_S_REGISTERING:
|
||||
case AVAHI_CLIENT_CONNECTING:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Public interface
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn)
|
||||
{
|
||||
int avahi_err;
|
||||
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
ctx->loop = loop;
|
||||
LIST_INIT(&ctx->neighbors);
|
||||
LIST_INIT(&ctx->services);
|
||||
LIST_INIT(&ctx->type_entries);
|
||||
|
||||
/* Dedicated operational session for push writes (avoids sharing
|
||||
* sr_query_ses which the journal thread also uses). */
|
||||
if (sr_session_start(sr_conn, SR_DS_OPERATIONAL, &ctx->sr_ses)) {
|
||||
ERROR("avahi: failed to start sysrepo session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Wire up libev-backed AvahiPoll vtable */
|
||||
ctx->poll_api.userdata = ctx;
|
||||
ctx->poll_api.watch_new = watch_new;
|
||||
ctx->poll_api.watch_update = watch_update;
|
||||
ctx->poll_api.watch_get_events = watch_get_events;
|
||||
ctx->poll_api.watch_free = watch_free;
|
||||
ctx->poll_api.timeout_new = timeout_new;
|
||||
ctx->poll_api.timeout_update = timeout_update;
|
||||
ctx->poll_api.timeout_free = timeout_free;
|
||||
|
||||
ctx->client = avahi_client_new(&ctx->poll_api,
|
||||
AVAHI_CLIENT_NO_FAIL,
|
||||
client_cb, ctx,
|
||||
&avahi_err);
|
||||
if (!ctx->client) {
|
||||
ERROR("avahi: client_new failed: %s", avahi_strerror(avahi_err));
|
||||
sr_session_stop(ctx->sr_ses);
|
||||
ctx->sr_ses = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
INFO("avahi: mDNS neighbor monitor initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void avahi_ctx_exit(struct avahi_ctx *ctx)
|
||||
{
|
||||
struct avahi_type_entry *te;
|
||||
|
||||
/* Free browsers explicitly before freeing the client */
|
||||
while (!LIST_EMPTY(&ctx->type_entries)) {
|
||||
te = LIST_FIRST(&ctx->type_entries);
|
||||
avahi_service_browser_free(te->browser);
|
||||
LIST_REMOVE(te, link);
|
||||
free(te);
|
||||
}
|
||||
if (ctx->type_browser) {
|
||||
avahi_service_type_browser_free(ctx->type_browser);
|
||||
ctx->type_browser = NULL;
|
||||
}
|
||||
if (ctx->client) {
|
||||
avahi_client_free(ctx->client);
|
||||
ctx->client = NULL;
|
||||
}
|
||||
|
||||
if (ctx->sr_ses) {
|
||||
ds_clear_all(ctx);
|
||||
sr_session_stop(ctx->sr_ses);
|
||||
ctx->sr_ses = NULL;
|
||||
}
|
||||
|
||||
free_all(ctx);
|
||||
INFO("avahi: mDNS neighbor monitor stopped");
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#ifndef STATD_AVAHI_H_
|
||||
#define STATD_AVAHI_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sys/queue.h>
|
||||
|
||||
#include <ev.h>
|
||||
#include <sysrepo.h>
|
||||
#include <avahi-client/client.h>
|
||||
#include <avahi-client/lookup.h>
|
||||
#include <avahi-common/watch.h>
|
||||
|
||||
/*
|
||||
* In-memory state for avahi mDNS neighbor tracking.
|
||||
* Services are kept in a flat list; neighbors track addresses only.
|
||||
*/
|
||||
|
||||
struct avahi_addr {
|
||||
char val[64];
|
||||
LIST_ENTRY(avahi_addr) link;
|
||||
};
|
||||
|
||||
struct avahi_txt {
|
||||
char val[256];
|
||||
LIST_ENTRY(avahi_txt) link;
|
||||
};
|
||||
|
||||
struct avahi_service {
|
||||
int ifindex;
|
||||
AvahiProtocol proto;
|
||||
char name[256];
|
||||
char type[64];
|
||||
char domain[64];
|
||||
char hostname[256];
|
||||
uint16_t port;
|
||||
LIST_HEAD(, avahi_txt) txts;
|
||||
LIST_ENTRY(avahi_service) link;
|
||||
};
|
||||
|
||||
struct avahi_neighbor {
|
||||
char hostname[256];
|
||||
LIST_HEAD(, avahi_addr) addrs;
|
||||
LIST_ENTRY(avahi_neighbor) link;
|
||||
};
|
||||
|
||||
struct avahi_type_entry {
|
||||
AvahiServiceBrowser *browser;
|
||||
char type[64];
|
||||
LIST_ENTRY(avahi_type_entry) link;
|
||||
};
|
||||
|
||||
struct avahi_ctx {
|
||||
struct ev_loop *loop;
|
||||
sr_session_ctx_t *sr_ses; /* Dedicated operational DS write session */
|
||||
AvahiClient *client;
|
||||
AvahiServiceTypeBrowser *type_browser;
|
||||
AvahiPoll poll_api; /* libev-backed vtable */
|
||||
LIST_HEAD(, avahi_neighbor) neighbors;
|
||||
LIST_HEAD(, avahi_service) services; /* Flat list; keyed by 5-tuple */
|
||||
LIST_HEAD(, avahi_type_entry) type_entries;
|
||||
};
|
||||
|
||||
int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn);
|
||||
void avahi_ctx_exit(struct avahi_ctx *ctx);
|
||||
|
||||
#endif
|
||||
@@ -36,11 +36,12 @@ AM_CONDITIONAL(CONTAINERS, [test "x$enable_containers" != "xno"])
|
||||
# Check for pkg-config first, warn if it's not installed
|
||||
PKG_PROG_PKG_CONFIG
|
||||
|
||||
PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0])
|
||||
PKG_CHECK_MODULES([libite], [libite >= 2.6.1])
|
||||
PKG_CHECK_MODULES([libyang], [libyang >= 2.1.80])
|
||||
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36])
|
||||
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
|
||||
PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0])
|
||||
PKG_CHECK_MODULES([libite], [libite >= 2.6.1])
|
||||
PKG_CHECK_MODULES([libyang], [libyang >= 2.1.80])
|
||||
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36])
|
||||
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
|
||||
PKG_CHECK_MODULES([avahi_client], [avahi-client >= 0.7])
|
||||
|
||||
AC_CHECK_HEADER([ev.h],
|
||||
[saved_LIBS="$LIBS"
|
||||
|
||||
@@ -3771,6 +3771,91 @@ def show_lldp(json):
|
||||
entry.print()
|
||||
|
||||
|
||||
def _mdns_sort_addrs(addresses):
|
||||
"""Sort addresses: IPv4 first, then non-link-local IPv6, then link-local IPv6."""
|
||||
def key(a):
|
||||
if ":" not in a:
|
||||
return 0
|
||||
if a.lower().startswith("fe80:"):
|
||||
return 2
|
||||
return 1
|
||||
return sorted(addresses, key=key)
|
||||
|
||||
|
||||
def _mdns_last_seen(ts):
|
||||
"""Extract HH:MM:SS from RFC 3339 timestamp."""
|
||||
if not ts:
|
||||
return "-"
|
||||
try:
|
||||
return ts.split("T")[1][:8]
|
||||
except (IndexError, AttributeError):
|
||||
return "-"
|
||||
|
||||
|
||||
def _mdns_svc_name(stype):
|
||||
"""'_https._tcp' → 'https', '_netconf-ssh._tcp' → 'netconf-ssh'."""
|
||||
return stype.lstrip("_").split("._")[0]
|
||||
|
||||
|
||||
def show_mdns(json):
|
||||
mdns = json.get("infix-services:mdns", {})
|
||||
if not mdns:
|
||||
print("mDNS not configured.")
|
||||
return
|
||||
|
||||
# Configuration
|
||||
enabled = mdns.get("enabled")
|
||||
domain = mdns.get("domain", "local")
|
||||
hostname = mdns.get("hostname")
|
||||
reflector_on = mdns.get("reflector", {}).get("enabled")
|
||||
|
||||
if enabled is not None:
|
||||
print(f"{'Enabled':<16}: {'yes' if enabled else 'no'}")
|
||||
print(f"{'Domain':<16}: {domain}")
|
||||
if hostname:
|
||||
print(f"{'Hostname':<16}: {hostname}")
|
||||
|
||||
ifaces = mdns.get("interfaces", {})
|
||||
if ifaces.get("allow"):
|
||||
print(f"{'Allow':<16}: {', '.join(ifaces['allow'])}")
|
||||
if ifaces.get("deny"):
|
||||
print(f"{'Deny':<16}: {', '.join(ifaces['deny'])}")
|
||||
|
||||
reflector = mdns.get("reflector", {})
|
||||
if reflector_on is not None:
|
||||
print(f"{'Reflector':<16}: {'yes' if reflector_on else 'no'}")
|
||||
if reflector.get("service-filter"):
|
||||
print(f"{'Svc filter':<16}: {', '.join(reflector['service-filter'])}")
|
||||
|
||||
# Neighbors
|
||||
neighbors = mdns.get("neighbors", {}).get("neighbor", [])
|
||||
if not neighbors:
|
||||
print("\nNo mDNS neighbors.")
|
||||
return
|
||||
|
||||
print()
|
||||
table = SimpleTable([
|
||||
Column("HOSTNAME", flexible=True),
|
||||
Column("ADDRESS"),
|
||||
Column("LAST SEEN"),
|
||||
Column("SERVICES"),
|
||||
])
|
||||
|
||||
for nbr in sorted(neighbors, key=lambda n: n.get("hostname", "")):
|
||||
addrs = _mdns_sort_addrs(nbr.get("address", []))
|
||||
ts = _mdns_last_seen(nbr.get("last-seen", ""))
|
||||
svcs = nbr.get("service", [])
|
||||
svc_str = " ".join(
|
||||
f"{_mdns_svc_name(s.get('type', '?'))}({s.get('port', 0)})"
|
||||
for s in svcs
|
||||
) if svcs else "-"
|
||||
table.row(nbr.get("hostname", "?"), addrs[0] if addrs else "-", ts, svc_str)
|
||||
for addr in addrs[1:]:
|
||||
table.row("", addr, "", "")
|
||||
|
||||
table.print()
|
||||
|
||||
|
||||
def parse_firewall_log_line(line):
|
||||
"""Parse a single firewall log line into structured data"""
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user