diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml
index 045953f84..187d7636e 100644
--- a/.github/workflows/coverity.yml
+++ b/.github/workflows/coverity.yml
@@ -8,6 +8,8 @@ on:
env:
PROJECT_NAME: Infix
CONTACT_EMAIL: troglobit@gmail.com
+ LIBYANG_VERSION: 4.2.2
+ SYSREPO_VERSION: 4.2.10
jobs:
coverity:
@@ -59,12 +61,20 @@ jobs:
- name: Build dependencies
run: |
- git clone https://github.com/CESNET/libyang.git
+ git clone -b v${LIBYANG_VERSION} --depth 1 https://github.com/CESNET/libyang.git
+ for p in patches/libyang/${LIBYANG_VERSION}/*.patch; do
+ git -C libyang apply < "$p"
+ done
mkdir libyang/build
(cd libyang/build && cmake .. && make all && sudo make install)
- git clone https://github.com/sysrepo/sysrepo.git
+
+ git clone -b v${SYSREPO_VERSION} --depth 1 https://github.com/sysrepo/sysrepo.git
+ for p in patches/sysrepo/${SYSREPO_VERSION}/*.patch; do
+ git -C sysrepo apply < "$p"
+ done
mkdir sysrepo/build
(cd sysrepo/build && cmake .. && make all && sudo make install)
+
git clone https://github.com/troglobit/libite.git
(cd libite && ./autogen.sh && ./configure && make && sudo make install)
make dep
diff --git a/doc/discovery.md b/doc/discovery.md
index aa5fd8d77..b0822da03 100644
--- a/doc/discovery.md
+++ b/doc/discovery.md
@@ -18,8 +18,7 @@ link-local IPv6 address is then seen in the response. In the following
example, the PC here uses *tap0* as *if1*, Infix responds with address
*fe80::ff:fec0:ffed*.
-```
-linux-pc:# ping -6 -L -c 3 ff02::1%tap0
+
linux-pc:# ping -6 -L -c 3 ff02::1%tap0
PING ff02::1%tap0(ff02::1%tap0) 56 data bytes
64 bytes from fe80::ff:fec0:ffed%tap0: icmp_seq=1 ttl=64 time=0.558 ms
64 bytes from fe80::ff:fec0:ffed%tap0: icmp_seq=2 ttl=64 time=0.419 ms
@@ -28,8 +27,8 @@ PING ff02::1%tap0(ff02::1%tap0) 56 data bytes
--- ff02::1%tap0 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2043ms
rtt min/avg/max/mdev = 0.389/0.455/0.558/0.073 ms
-linux-pc:#
-```
+linux-pc:#
+
> [!TIP]
> The `-L` option ignores local responses from the PC.
@@ -37,11 +36,10 @@ linux-pc:#
This address can then be used to connect to the device, e.g., using SSH.
Notice the syntax `username@address%interface`:
-```
-linux-pc:# ssh admin@fe80::ff:fec0:ffed%tap0
+linux-pc:# ssh admin@fe80::ff:fec0:ffed%tap0
admin@fe80::ff:fec0:ffed%tap0's password: admin
-admin@infix-c0-ff-ee:~$
-```
+admin@infix-c0-ff-ee:~$
+
### Windows
@@ -52,10 +50,9 @@ no extra software required.
From the command line, use the `.local` hostname directly:
-```cmd
-C:\> ping infix-c0-ff-ee.local
-C:\> ssh admin@infix-c0-ff-ee.local
-```
+C:\> ping infix-c0-ff-ee.local
+C:\> ssh admin@infix-c0-ff-ee.local
+
> [!NOTE]
> IPv6 multicast ping (`ping ff02::1%if1`) may not display responses on
@@ -63,9 +60,8 @@ C:\> ssh admin@infix-c0-ff-ee.local
> confirm connectivity, Wireshark will show the ICMPv6 echo replies
> arriving. Use mDNS (see [mDNS-SD](#mdns-sd) below) as the reliable
> alternative.
-
-
-
+>
+> 
## LLDP
@@ -73,8 +69,7 @@ Infix supports LLDP (IEEE 802.1AB). For a device with factory default
settings, the link-local IPv6 address can be read from the Management
Address TLV using *tcpdump* or other sniffing tools[^1]:
-```
-linux-pc:# tcpdump -i tap0 -Qin -v ether proto 0x88cc
+linux-pc:# tcpdump -i tap0 -Qin -v ether proto 0x88cc
tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
15:51:52.061071 LLDP, length 193
Chassis ID TLV (1), length 7
@@ -103,8 +98,8 @@ tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144
End TLV (0), length 0
^C
1 packet captured
-linux-pc:#
-```
+linux-pc:#
+
If the device has an IPv4 address assigned, it is shown in an additional
Management Address TLV.
@@ -116,8 +111,7 @@ Management Address TLV.
In the example below, the IPv4 address (10.0.1.1) happens to be
assigned to *eth0*, while the IPv6 address (2001:db8::1) is not.
-```
-linux-pc:# sudo tcpdump -i tap0 -Qin -v ether proto 0x88cc
+linux-pc:# sudo tcpdump -i tap0 -Qin -v ether proto 0x88cc
tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
15:46:07.908665 LLDP, length 207
Chassis ID TLV (1), length 7
@@ -152,7 +146,7 @@ tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144
2 packets received by filter
0 packets dropped by kernel
linux-pc:#
-```
+
The following capabilities are available via NETCONF/RESTCONF or the Infix CLI.
@@ -160,19 +154,17 @@ The following capabilities are available via NETCONF/RESTCONF or the Infix CLI.
The LLDP service can be disabled using the following commands.
-```
-admin@infix-c0-ff-ee:/> configure
-admin@infix-c0-ff-ee:/config/> no lldp
-admin@infix-c0-ff-ee:/config/> leave
-admin@infix-c0-ff-ee:/>
-```
+admin@infix-c0-ff-ee:/> configure
+admin@infix-c0-ff-ee:/config/> no lldp
+admin@infix-c0-ff-ee:/config/> leave
+admin@infix-c0-ff-ee:/>
+
To reenable it from the CLI config mode:
-```
-admin@test-00-01-00:/config/> set lldp enabled
-admin@test-00-01-00:/config/> leave
-```
+admin@test-00-01-00:/config/> set lldp enabled
+admin@test-00-01-00:/config/> leave
+
### LLDP Message Transmission Interval
@@ -180,15 +172,14 @@ By default, LLDP uses a `message-tx-interval` of 30 seconds, as defined
by the IEEE standard. Infix allows this value to be customized.
To change it using the CLI:
-```
-admin@test-00-01-00:/config/> set lldp message-tx-interval 1
-admin@test-00-01-00:/config/> leave
-```
+admin@test-00-01-00:/config/> set lldp message-tx-interval 1
+admin@test-00-01-00:/config/> leave
+
### LLDP Administrative Status per Interface
Infix supports configuring the LLDP administrative status on a per-port
-basis. The default mode is `tx-and-rx`, but the following options are
+basis. The default mode is `tx-and-rx`, but the following options are
also supported:
- `rx-only` – Receive LLDP packets only
@@ -197,15 +188,14 @@ also supported:
Example configuration:
-```
-admin@test-00-01-00:/config/> set lldp port e8 dest-mac-address 01:80:C2:00:00:0E admin-status disabled
-admin@test-00-01-00:/config/> set lldp port e5 dest-mac-address 01:80:C2:00:00:0E admin-status rx-only
-admin@test-00-01-00:/config/> set lldp port e6 dest-mac-address 01:80:C2:00:00:0E admin-status tx-only
-admin@test-00-01-00:/config/> leave
-```
+admin@test-00-01-00:/config/> set lldp port e8 dest-mac-address 01:80:C2:00:00:0E admin-status disabled
+admin@test-00-01-00:/config/> set lldp port e5 dest-mac-address 01:80:C2:00:00:0E admin-status rx-only
+admin@test-00-01-00:/config/> set lldp port e6 dest-mac-address 01:80:C2:00:00:0E admin-status tx-only
+admin@test-00-01-00:/config/> leave
+
> [!NOTE]
-> The destination MAC address must be the standard LLDP multicast
+> The destination MAC address must be the standard LLDP multicast
> address: `01:80:C2:00:00:0E`.
### Displaying LLDP Neighbor Information
@@ -213,13 +203,12 @@ admin@test-00-01-00:/config/> leave
In CLI mode, Infix also provides a convenient `show lldp` command to
list LLDP neighbors detected on each interface:
-```
-admin@test-00-01-00:/> show lldp
-INTERFACE REM-IDX TIME CHASSIS-ID PORT-ID
-e5 1 902 00:a0:85:00:04:01 00:a0:85:00:04:07
-e6 3 897 00:a0:85:00:03:01 00:a0:85:00:03:07
+admin@test-00-01-00:/> show lldp
+
+e5 1 902 00:a0:85:00:04:01 00:a0:85:00:04:07
+e6 3 897 00:a0:85:00:03:01 00:a0:85:00:03:07
e8 2 901 00:a0:85:00:02:01 00:a0:85:00:02:05
-```
+
## mDNS-SD
@@ -227,34 +216,44 @@ DNS-SD/mDNS-SD can be used to discover Infix devices and services. By
default, Infix use the `.local` domain for advertising services. Some
networks use `.lan` instead, so this configurable:
-```
-admin@infix-c0-ff-ee:/> configure
-admin@infix-c0-ff-ee:/config/> edit mdns
-admin@infix-c0-ff-ee:/config/mdns/> set domain lan
-```
+admin@infix-c0-ff-ee:/> configure
+admin@infix-c0-ff-ee:/config/> edit mdns
+admin@infix-c0-ff-ee:/config/mdns/> set domain lan
+
Other available settings include limiting the interfaces mDNS responder
-acts on:
+acts on, `allow`:
-```
-admin@infix-c0-ff-ee:/config/> set interfaces allow e1
-```
+admin@infix-c0-ff-ee:/config/> set interfaces allow e1
+
-or
+or `deny`. The `allow` and `deny` settings are complementary, `deny` always wins.
-```
-admin@infix-c0-ff-ee:/config/> set interfaces deny wan
-```
+admin@infix-c0-ff-ee:/config/> set interfaces deny wan
+
+
+Use `leave` to activate the new settings, then inspect the operational
+state and any detected neighbors with `show mdns` from admin-exec
+context:
-The `allow` and `deny` settings are complementary, `deny` always wins.
+admin@gateway:/> show mdns
+Enabled : yes
+Domain : local
+Deny : wan
+
+
+Living-Room.local 192.168.0.139 17:28:43 trel(59813) sleep-proxy(61936) raop(7000) srpl-tls(853)
+firefly-4.local 192.168.0.122 17:28:37 workstation(9)
+gimli.local 192.168.0.180 17:28:37 smb(445)
+infix.local 192.168.0.1 17:28:38 https(443) workstation(9) ssh(22) https(443)
+
----
In Linux, tools such as *avahi-browse* or *mdns-scan*[^2] can be used to
search for devices advertising their services via mDNS.
-```
-linux-pc:# avahi-browse -ar
+linux-pc:# avahi-browse -ar
+ tap0 IPv6 infix-c0-ff-ee SFTP File Transfer local
+ tap0 IPv4 infix-c0-ff-ee SFTP File Transfer local
+ tap0 IPv6 infix-c0-ff-ee SSH Remote Terminal local
@@ -281,7 +280,7 @@ linux-pc:# avahi-browse -ar
txt = []
^C
linux-pc:#
-```
+
> [!TIP]
> The `-t` option is also very useful, it stops browsing automatically
@@ -294,17 +293,15 @@ name mappings for IP addresses. By default, it translates from IPv4
addresses. This function allows users to confirm that addresses are
mapped correctly.
-```
-linux-pc:# avahi-resolve-host-name infix-c0-ff-ee.local
+linux-pc:# avahi-resolve-host-name infix-c0-ff-ee.local
infix-c0-ff-ee.local 10.0.1.1
linux-pc:#
-```
+
Thanks to mDNS we can use the advertised name instead of the IP
address for operations like `ping` and `ssh` as shown below:
-```
-linux-pc:# ping infix-c0-ff-ee.local -c 3
+linux-pc:# ping infix-c0-ff-ee.local -c 3
PING infix-c0-ff-ee.local (10.0.1.1) 56(84) bytes of data.
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.852 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=1.12 ms
@@ -314,8 +311,8 @@ PING infix-c0-ff-ee.local (10.0.1.1) 56(84) bytes of data.
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.852/1.105/1.348/0.202 ms
-linux-pc:# ssh admin@infix-c0-ff-ee.local
-(admin@infix-c0-ff-ee.local) Password:
+linux-pc:# ssh admin@infix-c0-ff-ee.local
+(admin@infix-c0-ff-ee.local) Password:
.-------.
| . . | Infix OS — Immutable.Friendly.Secure
|-. v .-| https://www.kernelkit.org
@@ -324,27 +321,25 @@ linux-pc:# ssh admin@infix-c0-ff-ee.local
Run the command 'cli' for interactive OAM
linux-pc:#
-```
+
To disable mDNS/mDNS-SD, type the commands:
-```
-admin@infix-c0-ff-ee:/> configure
-admin@infix-c0-ff-ee:/config/> no mdns
-admin@infix-c0-ff-ee:/config/> leave
-```
+admin@infix-c0-ff-ee:/> configure
+admin@infix-c0-ff-ee:/config/> no mdns
+admin@infix-c0-ff-ee:/config/> leave
+
### Human-Friendly Hostname Alias
-Each Infix deviuce advertise itself as *infix.local*, in addition to its
+Each Infix device advertises itself as *infix.local*, in addition to its
full hostname (e.g., *infix-c0-ff-ee.local* or *foo.local*). This alias
works seamlessly on a network with a single Infix device, and makes it
easy to connect when the exact hostname is not known in advance. The
examples below show how the alias can be used for actions such as
pinging or establishing an SSH connection:
-```
-linux-pc:# ping infix.local -c 3
+linux-pc:# ping infix.local -c 3
PING infix.local (10.0.1.1) 56(84) bytes of data.
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.751 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=2.28 ms
@@ -354,8 +349,8 @@ PING infix.local (10.0.1.1) 56(84) bytes of data.
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.751/1.482/2.281/0.626 ms
-linux-pc:# ssh admin@infix.local
-(admin@infix.local) Password:
+linux-pc:# ssh admin@infix.local
+(admin@infix.local) Password:
.-------.
| . . | Infix OS — Immutable.Friendly.Secure
|-. v .-| https://www.kernelkit.org
@@ -364,7 +359,7 @@ linux-pc:# ssh admin@infix.local
Run the command 'cli' for interactive OAM
admin@infix-c0-ff-ee:~$
-```
+
When multiple Infix devices are present on the LAN the alias will not
uniquely identify a device; *infix.local* will refer to any of the
@@ -390,12 +385,11 @@ portal to access all others, if it goes down another takes its place.
To disable the netbrowse service, and the *network.local* alias, the
following commands can be used:
-```
-admin@infix-c0-ff-ee:/> configure
-admin@infix-c0-ff-ee:/config/> edit web
-admin@infix-c0-ff-ee:/config/web/> no netbrowse
-admin@infix-c0-ff-ee:/config/web/> leave
-```
+admin@infix-c0-ff-ee:/> configure
+admin@infix-c0-ff-ee:/config/> edit web
+admin@infix-c0-ff-ee:/config/web/> no netbrowse
+admin@infix-c0-ff-ee:/config/web/> leave
+
[^1]: E.g., [lldpd](https://github.com/lldp/lldpd) which includes the
diff --git a/doc/img/network-local.png b/doc/img/network-local.png
index 112ea5d6f..47bc59791 100644
Binary files a/doc/img/network-local.png and b/doc/img/network-local.png differ
diff --git a/package/klish/klish.hash b/package/klish/klish.hash
index e8e93c693..8d8b6eedf 100644
--- a/package/klish/klish.hash
+++ b/package/klish/klish.hash
@@ -1,3 +1,3 @@
# Locally calculated
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
-sha256 cd9bc969350b8b30d9a7a31b0f19fb3218c602f04fe7e6a1d80682a75ed26d18 klish-3ae496c43d90354ffa94d364d7775c089f0e119a-git4.tar.gz
+sha256 be6548a5a4f8c35906b02ea0ccb64cdd94d48bfe7133801353fbf658aa33d5c0 klish-8dca4da70a7794d5f4e0b047724bfe9e2088ebf3-git4.tar.gz
diff --git a/package/klish/klish.mk b/package/klish/klish.mk
index 57066b279..a1d7ff6f8 100644
--- a/package/klish/klish.mk
+++ b/package/klish/klish.mk
@@ -4,7 +4,7 @@
#
################################################################################
-KLISH_VERSION = 3ae496c43d90354ffa94d364d7775c089f0e119a
+KLISH_VERSION = 8dca4da70a7794d5f4e0b047724bfe9e2088ebf3
KLISH_SITE = https://github.com/kernelkit/klish.git
#KLISH_VERSION = tags/3.0.0
#KLISH_SITE = https://src.libcode.org/pkun/klish.git
diff --git a/package/mdns-alias/mdns-alias.hash b/package/mdns-alias/mdns-alias.hash
index 63c92b712..7263ae672 100644
--- a/package/mdns-alias/mdns-alias.hash
+++ b/package/mdns-alias/mdns-alias.hash
@@ -1,5 +1,5 @@
# From GitHub release
-sha256 cf32b3c224325b3b660669a82e6dc30ad8438016d7492433cea591fa3a8a1dd9 mdns-alias-1.1.tar.gz
+sha256 9f194fa0b6e34fd915054394ef5b820a4f6b1755ace5ed1011bfba6df550accf mdns-alias-1.2.tar.gz
# Locally generated
sha256 3d6f910b5e198f3daab48047b8ee6949040f7abee3927daf2e231f265faf7d91 LICENSE
diff --git a/package/mdns-alias/mdns-alias.mk b/package/mdns-alias/mdns-alias.mk
index d961b429d..f17147658 100644
--- a/package/mdns-alias/mdns-alias.mk
+++ b/package/mdns-alias/mdns-alias.mk
@@ -4,7 +4,7 @@
#
################################################################################
-MDNS_ALIAS_VERSION = 1.1
+MDNS_ALIAS_VERSION = 1.2
MDNS_ALIAS_SITE = https://github.com/troglobit/mdns-alias/releases/download/v$(MDNS_ALIAS_VERSION)
MDNS_ALIAS_LICENSE = ISC
MDNS_ALIAS_LICENSE_FILES = LICENSE
diff --git a/package/mdns-alias/mdns-alias.svc b/package/mdns-alias/mdns-alias.svc
index 241edc618..1f5dc87e8 100644
--- a/package/mdns-alias/mdns-alias.svc
+++ b/package/mdns-alias/mdns-alias.svc
@@ -1,5 +1,4 @@
# 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 env:-/etc/default/mdns-alias \
- [2345] mdns-alias -H $MDNS_ALIAS_ARGS --
+service [2345] env:-/etc/default/mdns-alias mdns-alias -H $MDNS_ALIAS_ARGS --
diff --git a/package/netbrowse/netbrowse.svc b/package/netbrowse/netbrowse.svc
index cf9da41ff..d09d1c8b7 100644
--- a/package/netbrowse/netbrowse.svc
+++ b/package/netbrowse/netbrowse.svc
@@ -1,2 +1,3 @@
service name:netbrowse log:prio:daemon.debug,tag:netbrowse \
- [2345] netbrowse -l 127.0.0.1:8000 -- Network browser
+ [2345] netbrowse -l 127.0.0.1:8000 \
+ -- Network browser
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf
index dfcf76ba5..0d8f24761 100644
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf
@@ -1,2 +1,3 @@
-service name:mdns env:-/etc/default/avahi \
- [2345] avahi-daemon -s $AVAHI_ARGS -- Avahi mDNS-SD daemon
+service name:mdns pid:!/run/avahi-daemon/pid env:-/etc/default/avahi \
+ [2345] avahi-daemon -s $AVAHI_ARGS \
+ -- Avahi mDNS-SD daemon
diff --git a/package/statd/statd.conf b/package/statd/statd.conf
index d88025583..5d76de659 100644
--- a/package/statd/statd.conf
+++ b/package/statd/statd.conf
@@ -1,3 +1,2 @@
#set DEBUG=1
-
service name:statd [12345] statd -f -p /run/statd.pid -n -- Status daemon
diff --git a/src/netbrowse/browse.go b/src/netbrowse/browse.go
index 83e71bbba..2f32b0410 100644
--- a/src/netbrowse/browse.go
+++ b/src/netbrowse/browse.go
@@ -2,6 +2,8 @@
package main
import (
+ "encoding/json"
+ "fmt"
"log"
"os/exec"
"sort"
@@ -42,6 +44,74 @@ var typeOrder = map[string]int{
"HTTPS": 1, "HTTP": 2, "SSH": 3, "SFTP": 4,
}
+// txtMeta holds fields extracted from a set of DNS-SD TXT records.
+type txtMeta struct {
+ vv1 bool
+ legacy bool
+ path string
+ adminurl string
+ product string
+ version string
+}
+
+// parseTxt extracts well-known fields from a slice of individual TXT record
+// strings. Each element must already be a single record without quoting.
+// avahi-browse \DDD escape sequences in values are resolved via decode().
+func parseTxt(records []string) txtMeta {
+ var m txtMeta
+ for _, r := range records {
+ switch {
+ case r == "vv=1":
+ m.vv1 = true
+ case r == "on=Infix":
+ m.legacy = true
+ case m.path == "" && strings.HasPrefix(r, "path="):
+ m.path = decode(r[5:])
+ case m.adminurl == "" && strings.HasPrefix(r, "adminurl="):
+ m.adminurl = decode(r[9:])
+ case m.product == "" && strings.HasPrefix(r, "product="):
+ m.product = decode(r[8:])
+ case m.product == "" && strings.HasPrefix(r, "am="):
+ // RAOP/AirPlay 1 Apple model key
+ m.product = decode(r[3:])
+ case m.product == "" && strings.HasPrefix(r, "model="):
+ // AirPlay 2 Apple model key
+ m.product = decode(r[6:])
+ case m.version == "" && strings.HasPrefix(r, "ov="):
+ m.version = decode(r[3:])
+ }
+ }
+ return m
+}
+
+// buildHosts sorts services and assembles the final host map from the
+// per-host accumulator maps that both scan() and scanOperational() maintain.
+func buildHosts(svcsMap map[string][]Service, addrMap, productMap, versionMap map[string]string,
+ vvHosts, legHosts, mgmtHosts map[string]bool) map[string]Host {
+ hosts := make(map[string]Host)
+ for link, svcs := range svcsMap {
+ sort.SliceStable(svcs, func(i, j int) bool {
+ oi, oj := typeOrder[svcs[i].Type], typeOrder[svcs[j].Type]
+ if oi == 0 {
+ oi = 999
+ }
+ if oj == 0 {
+ oj = 999
+ }
+ return oi < oj
+ })
+ 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
+}
+
// hasK checks whether avahi-browse supports the -k flag.
func hasK() bool {
out, err := exec.Command("avahi-browse", "--help").CombinedOutput()
@@ -82,23 +152,21 @@ func scan() map[string]Host {
continue
}
- family := parts[2]
+ family := parts[2]
serviceName := parts[3]
serviceType := parts[4]
- link := parts[6]
- address := parts[7]
- port := parts[8]
- txt := parts[9]
+ link := parts[6]
+ address := parts[7]
+ port := parts[8]
+ txt := strings.Join(parts[9:], ";")
if family != "IPv4" && family != "IPv6" {
continue
}
info, known := knownServices[serviceType]
- displayName := info.displayName
- urlTemplate := info.urlTemplate
- if !known {
- displayName = serviceType
+ if known {
+ mgmtHosts[link] = true
}
// vv=1 is the platform marker set by confd/services.c, survives OS
@@ -106,9 +174,6 @@ func scan() map[string]Host {
// (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
@@ -128,42 +193,41 @@ func scan() map[string]Host {
// "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:]
- }
+ var records []string
+ for _, rec := range strings.Split(txt, "\" \"") {
+ records = append(records, strings.Trim(rec, "\""))
}
+ meta := parseTxt(records)
+ if meta.vv1 {
+ vvHosts[link] = true
+ }
+ if meta.legacy {
+ legHosts[link] = true
+ }
+
// IPP/Bonjour printers encode product as "(Name)" — strip the parens.
- product = strings.TrimPrefix(strings.TrimSuffix(product, ")"), "(")
+ product := strings.TrimPrefix(strings.TrimSuffix(meta.product, ")"), "(")
if product != "" && productMap[link] == "" {
productMap[link] = product
}
- if version != "" && versionMap[link] == "" {
- versionMap[link] = version
+ if meta.version != "" && versionMap[link] == "" {
+ versionMap[link] = meta.version
+ }
+
+ displayName := info.displayName
+ if !known {
+ displayName = serviceType
}
var url string
- if adminurl != "" {
- url = adminurl
- } else if urlTemplate != "" {
+ if meta.adminurl != "" {
+ url = meta.adminurl
+ } else if info.urlTemplate != "" {
url = strings.NewReplacer(
"{address}", address,
"{port}", port,
- "{path}", path,
- ).Replace(urlTemplate)
+ "{path}", meta.path,
+ ).Replace(info.urlTemplate)
}
svc := Service{
@@ -172,7 +236,9 @@ func scan() map[string]Host {
URL: url,
}
- // Deduplicate
+ // Deduplicate: avahi-browse reports each service once per
+ // (interface, protocol) combination, so the same entry can appear
+ // for both eth0/IPv4 and eth0/IPv6.
dup := false
for _, existing := range svcsMap[link] {
if existing.Type == svc.Type && existing.Name == svc.Name && existing.URL == svc.URL {
@@ -185,37 +251,151 @@ func scan() map[string]Host {
}
}
- // 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
+ return buildHosts(svcsMap, addrMap, productMap, versionMap, vvHosts, legHosts, mgmtHosts)
+}
+
+// JSON types for the operational-state backend.
+type opRoot struct {
+ MDNS struct {
+ Enabled bool `json:"enabled"`
+ Neighbors struct {
+ Neighbor []opNeighbor `json:"neighbor"`
+ } `json:"neighbors"`
+ } `json:"infix-services:mdns"`
+}
+
+type opNeighbor struct {
+ Hostname string `json:"hostname"`
+ Addresses []string `json:"address"`
+ Services []opService `json:"service"`
+}
+
+type opService struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Port uint16 `json:"port"`
+ Txt []string `json:"txt"`
+}
+
+// parseOperational builds a host map from an already-decoded opRoot.
+func parseOperational(root *opRoot) map[string]Host {
+ svcsMap := make(map[string][]Service)
+ addrMap := make(map[string]string)
+ productMap := make(map[string]string)
+ versionMap := make(map[string]string)
+ vvHosts := make(map[string]bool)
+ legHosts := make(map[string]bool)
+ mgmtHosts := make(map[string]bool)
+
+ for _, n := range root.MDNS.Neighbors.Neighbor {
+ link := n.Hostname
+
+ // Pick best address: prefer IPv4, skip link-local.
+ // Loopback is already excluded by statd before storing.
+ for _, a := range n.Addresses {
+ if strings.HasPrefix(a, "fe80:") {
+ continue
}
- if oj == 0 {
- oj = 999
+ if !strings.Contains(a, ":") {
+ addrMap[link] = a // IPv4 wins, stop looking
+ break
}
- return oi < oj
- })
- }
+ if addrMap[link] == "" {
+ addrMap[link] = a // IPv6 fallback
+ }
+ }
- // 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,
+ for _, svc := range n.Services {
+ info, known := knownServices[svc.Type]
+ if known {
+ mgmtHosts[link] = true
+ }
+
+ meta := parseTxt(svc.Txt)
+ if meta.vv1 {
+ vvHosts[link] = true
+ }
+ if meta.legacy {
+ legHosts[link] = true
+ }
+
+ // IPP/Bonjour printers encode product as "(Name)" — strip the parens.
+ product := strings.TrimPrefix(strings.TrimSuffix(meta.product, ")"), "(")
+ if product != "" && productMap[link] == "" {
+ productMap[link] = product
+ }
+ if meta.version != "" && versionMap[link] == "" {
+ versionMap[link] = meta.version
+ }
+
+ displayName := info.displayName
+ if !known {
+ displayName = svc.Type
+ }
+
+ addr := addrMap[link]
+ if addr == "" {
+ addr = n.Hostname // fall back to hostname if no usable address
+ }
+
+ var url string
+ if meta.adminurl != "" {
+ url = meta.adminurl
+ } else if info.urlTemplate != "" {
+ url = strings.NewReplacer(
+ "{address}", addr,
+ "{port}", fmt.Sprintf("%d", svc.Port),
+ "{path}", meta.path,
+ ).Replace(info.urlTemplate)
+ }
+
+ svcsMap[link] = append(svcsMap[link], Service{
+ Type: displayName,
+ Name: svc.Name,
+ URL: url,
+ })
}
}
- return hosts
+ return buildHosts(svcsMap, addrMap, productMap, versionMap, vvHosts, legHosts, mgmtHosts)
+}
+
+// fetchOpRoot runs `copy operational-state -x /mdns` and decodes the result.
+// Returns nil on any error (command failure or JSON parse error).
+func fetchOpRoot() *opRoot {
+ out, err := exec.Command("copy", "operational-state", "-x", "/mdns").Output()
+ if err != nil {
+ log.Printf("copy operational-state: %v", err)
+ return nil
+ }
+ var root opRoot
+ if err := json.Unmarshal(out, &root); err != nil {
+ log.Printf("copy operational-state: json: %v", err)
+ return nil
+ }
+ return &root
+}
+
+// scanOperational fetches the mDNS neighbor table from the sysrepo
+// operational datastore via `copy operational-state -x /mdns` and returns
+// the same host map as scan().
+func scanOperational() map[string]Host {
+ root := fetchOpRoot()
+ if root == nil {
+ return nil
+ }
+ return parseOperational(root)
+}
+
+// scanAuto tries the operational backend first. If the `copy` command is
+// unavailable or mDNS is disabled in the operational state, it falls back
+// to the avahi-browse backend transparently.
+func scanAuto() map[string]Host {
+ root := fetchOpRoot()
+ if root == nil || !root.MDNS.Enabled {
+ return scan()
+ }
+ return parseOperational(root)
}
// decode handles avahi's DNS-SD escape sequences in service names:
diff --git a/src/netbrowse/browse.html b/src/netbrowse/browse.html
index e91c44493..e781d9d68 100644
--- a/src/netbrowse/browse.html
+++ b/src/netbrowse/browse.html
@@ -458,7 +458,7 @@
}
var parts = [label];
if (lastTs) {
- parts.push('updated ' + lastTs.toLocaleTimeString());
+ parts.push('updated ' + lastTs.toLocaleTimeString(undefined, { hour12: false }));
}
foot.textContent = parts.join(' · ');
}
diff --git a/src/netbrowse/main.go b/src/netbrowse/main.go
index 0d68054d6..a5656d768 100644
--- a/src/netbrowse/main.go
+++ b/src/netbrowse/main.go
@@ -25,24 +25,37 @@ func browseHandler(w http.ResponseWriter, r *http.Request) {
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 dataHandler(scanFn func() map[string]Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ hosts := scanFn()
+ 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")
+ listen := flag.String("l", "127.0.0.1:8000", "listen address:port")
+ backend := flag.String("backend", "auto", "discovery backend: auto, avahi, or operational")
flag.Parse()
+ var scanFn func() map[string]Host
+ switch *backend {
+ case "operational":
+ scanFn = scanOperational
+ case "avahi":
+ scanFn = scan
+ default:
+ scanFn = scanAuto
+ }
+
mux := http.NewServeMux()
mux.HandleFunc("/", browseHandler)
mux.HandleFunc("/netbrowse", browseHandler)
- mux.HandleFunc("/data", dataHandler)
- mux.HandleFunc("/netbrowse/data", dataHandler)
+ mux.HandleFunc("/data", dataHandler(scanFn))
+ mux.HandleFunc("/netbrowse/data", dataHandler(scanFn))
staticSub, err := fs.Sub(staticFS, "static")
if err != nil {
diff --git a/src/statd/avahi.c b/src/statd/avahi.c
index 4d27a8ecc..b6619cc5e 100644
--- a/src/statd/avahi.c
+++ b/src/statd/avahi.c
@@ -380,7 +380,7 @@ static void ds_push_resolver(struct mdns_ctx *ctx, struct avahi_service *svc,
char val[64];
struct avahi_txt *t;
char ts[32];
- int err = 0;
+ int err;
xpath_str(qname, sizeof(qname), svc->name);
@@ -388,7 +388,7 @@ static void ds_push_resolver(struct mdns_ctx *ctx, struct avahi_service *svc,
* 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);
+ err = sr_setstr(ctx->sr_ses, xpath, NULL);
/* address (only if a new one was added) */
if (new_addr) {
@@ -542,16 +542,49 @@ static void resolver_cb(AvahiServiceResolver *r,
svc->port = port;
- /* Copy TXT records verbatim */
+ /* Copy TXT records, skipping any that are not valid UTF-8 or contain
+ * bytes that are illegal in XML/YANG strings. Apple devices sometimes
+ * embed raw binary tokens (device keys, protocol blobs) in TXT records;
+ * passing them to sr_set_item_str() would return EINVAL. */
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);
+ size_t i;
+
+ /* Validate: must be well-formed UTF-8 with no XML-illegal bytes */
+ for (i = 0; i < len; ) {
+ uint8_t b = data[i];
+ int extra;
+
+ if (b < 0x80) {
+ /* ASCII: reject control chars invalid in XML */
+ if ((b < 0x09) || (b > 0x0D && b < 0x20) || b == 0x7F)
+ goto skip;
+ i++;
+ continue;
+ }
+
+ /* Multi-byte UTF-8 lead byte */
+ if ((b & 0xE0) == 0xC0) extra = 1;
+ else if ((b & 0xF0) == 0xE0) extra = 2;
+ else if ((b & 0xF8) == 0xF0) extra = 3;
+ else goto skip; /* invalid lead byte */
+
+ i++;
+ for (; extra-- > 0; i++) {
+ if (i >= len || (data[i] & 0xC0) != 0x80)
+ goto skip; /* truncated sequence */
+ }
+ }
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);
+ continue;
+skip:
+ DEBUG("mdns: skipping binary TXT record for '%s' (len=%zu)", name, len);
}
ds_push_resolver(ctx, svc, new_addr);
@@ -714,31 +747,59 @@ static bool mdns_is_enabled(struct mdns_ctx *ctx)
return enabled;
}
+static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata);
+
/*
- * Retry timer callback: fires 2 s after AVAHI_CLIENT_FAILURE (and repeats up
- * to 3 times). Only logs ERROR once all retries are exhausted AND mDNS is
- * enabled in the running config — this avoids noisy errors when the operator
- * has simply disabled the mDNS service.
+ * Reconnect timer: fires MDNS_RECONN_DELAY seconds after AVAHI_CLIENT_FAILURE.
+ * Frees the broken client and creates a fresh one. libavahi's own AVAHI_CLIENT_NO_FAIL
+ * reconnection can miss D-Bus NameOwnerChanged events; explicit free+recreate is
+ * more reliable (same pattern used by mdns-alias).
+ */
+#define MDNS_RECONN_DELAY 3.0
+
+static void reconn_cb(struct ev_loop *loop, ev_timer *w, int revents)
+{
+ struct mdns_ctx *ctx = (struct mdns_ctx *)
+ ((char *)w - offsetof(struct mdns_ctx, reconn_timer));
+ int avahi_err;
+
+ (void)loop;
+ (void)revents;
+
+ if (ctx->client) {
+ avahi_client_free(ctx->client);
+ ctx->client = NULL;
+ }
+
+ ctx->client = avahi_client_new(&ctx->poll_api, AVAHI_CLIENT_NO_FAIL,
+ client_cb, ctx, &avahi_err);
+ if (!ctx->client)
+ ERROR("mdns: failed to recreate avahi client: %s", avahi_strerror(avahi_err));
+}
+
+/*
+ * Log-delay timer: fires MDNS_WARN_DELAY seconds after AVAHI_CLIENT_FAILURE.
+ * Logs a single warning if mDNS is still enabled in the running config —
+ * suppresses noise when the operator has simply disabled the mDNS service or
+ * avahi is just restarting briefly. Reconnection itself is handled by the
+ * libavahi client (AVAHI_CLIENT_NO_FAIL) — we never give up.
*
- * Example log (mDNS enabled, daemon stays down):
- * avahi: mDNS daemon not responding (attempt 3/3) — check that the mdns
- * service is running
+ * The delay must exceed libavahi's internal reconnect-poll interval (~5 s so
+ * that a normal daemon restart cancels this timer before it fires.
*/
+#define MDNS_WARN_DELAY 10.0
+
static void mdns_retry_cb(struct ev_loop *loop, ev_timer *w, int revents)
{
struct mdns_ctx *ctx = (struct mdns_ctx *)
((char *)w - offsetof(struct mdns_ctx, retry_timer));
+ (void)loop;
+ (void)revents;
ctx->fail_count++;
- if (ctx->fail_count < 3) {
- ev_timer_set(w, 2.0, 0.0);
- ev_timer_start(loop, w);
- return;
- }
if (mdns_is_enabled(ctx))
- ERROR("mdns: mDNS daemon not responding (attempt %d/3) — "
- "check that the mdns service is running", ctx->fail_count);
+ WARN("mdns: mDNS daemon not responding, will reconnect automatically");
}
static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
@@ -750,6 +811,7 @@ static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
switch (state) {
case AVAHI_CLIENT_S_RUNNING:
if (ctx->fail_count > 0) {
+ ev_timer_stop(ctx->loop, &ctx->reconn_timer);
ev_timer_stop(ctx->loop, &ctx->retry_timer);
NOTE("mdns: mDNS daemon reconnected");
ctx->fail_count = 0;
@@ -779,8 +841,12 @@ static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
* will log only if the daemon stays down for 3 attempts (~6 s)
* and mDNS is enabled in the running config.
*/
+ if (!ev_is_active(&ctx->reconn_timer)) {
+ ev_timer_init(&ctx->reconn_timer, reconn_cb, MDNS_RECONN_DELAY, 0.0);
+ ev_timer_start(ctx->loop, &ctx->reconn_timer);
+ }
if (!ev_is_active(&ctx->retry_timer)) {
- ev_timer_init(&ctx->retry_timer, mdns_retry_cb, 2.0, 0.0);
+ ev_timer_init(&ctx->retry_timer, mdns_retry_cb, MDNS_WARN_DELAY, 0.0);
ev_timer_start(ctx->loop, &ctx->retry_timer);
}
@@ -857,10 +923,54 @@ int mdns_ctx_init(struct mdns_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_
return 0;
}
+void mdns_ctx_reconnect(struct mdns_ctx *ctx)
+{
+ struct avahi_type_entry *te;
+ int avahi_err;
+
+ if (!mdns_is_enabled(ctx)) {
+ NOTE("mdns: mDNS is disabled, ignoring reconnect request");
+ return;
+ }
+
+ NOTE("mdns: reconnecting on request");
+
+ ev_timer_stop(ctx->loop, &ctx->reconn_timer);
+ ev_timer_stop(ctx->loop, &ctx->retry_timer);
+ ctx->fail_count = 0;
+
+ /* Clean up browsers 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;
+ }
+
+ free_all(ctx);
+ ds_clear_all(ctx);
+
+ if (ctx->client) {
+ avahi_client_free(ctx->client);
+ ctx->client = NULL;
+ }
+
+ ctx->client = avahi_client_new(&ctx->poll_api, AVAHI_CLIENT_NO_FAIL,
+ client_cb, ctx, &avahi_err);
+ if (!ctx->client)
+ ERROR("mdns: failed to recreate avahi client: %s", avahi_strerror(avahi_err));
+}
+
void mdns_ctx_exit(struct mdns_ctx *ctx)
{
struct avahi_type_entry *te;
+ if (ev_is_active(&ctx->reconn_timer))
+ ev_timer_stop(ctx->loop, &ctx->reconn_timer);
if (ev_is_active(&ctx->retry_timer))
ev_timer_stop(ctx->loop, &ctx->retry_timer);
diff --git a/src/statd/avahi.h b/src/statd/avahi.h
index fde93e368..88f06a288 100644
--- a/src/statd/avahi.h
+++ b/src/statd/avahi.h
@@ -58,14 +58,16 @@ struct mdns_ctx {
AvahiClient *client;
AvahiServiceTypeBrowser *type_browser;
AvahiPoll poll_api; /* libev-backed vtable */
- unsigned int fail_count; /* Consecutive avahi-daemon connection failures */
- ev_timer retry_timer; /* Deferred error-log timer */
+ unsigned int fail_count; /* Non-zero while avahi-daemon is absent */
+ ev_timer reconn_timer; /* Free+recreate client after brief delay */
+ ev_timer retry_timer; /* Deferred warn-log timer */
LIST_HEAD(, avahi_neighbor) neighbors;
LIST_HEAD(, avahi_service) services; /* Flat list; keyed by 5-tuple */
LIST_HEAD(, avahi_type_entry) type_entries;
};
int mdns_ctx_init(struct mdns_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn);
+void mdns_ctx_reconnect(struct mdns_ctx *ctx);
void mdns_ctx_exit(struct mdns_ctx *ctx);
#endif
diff --git a/src/statd/statd.c b/src/statd/statd.c
index 532760730..affc90740 100644
--- a/src/statd/statd.c
+++ b/src/statd/statd.c
@@ -352,6 +352,13 @@ static void sigusr1_cb(struct ev_loop *, struct ev_signal *, int)
debug ^= 1;
}
+static void sighup_cb(struct ev_loop *, struct ev_signal *w, int)
+{
+ struct statd *statd = w->data;
+
+ mdns_ctx_reconnect(&statd->mdns);
+}
+
static void sr_event_cb(struct ev_loop *, struct ev_io *w, int)
{
@@ -455,7 +462,7 @@ static int subscribe_to_all(struct statd *statd)
int main(int argc, char *argv[])
{
- struct ev_signal sigint_watcher, sigusr1_watcher;
+ struct ev_signal sigint_watcher, sigusr1_watcher, sighup_watcher;
int log_opts = LOG_PID | LOG_NDELAY;
struct statd statd = {};
const char *env;
@@ -516,6 +523,10 @@ int main(int argc, char *argv[])
sigusr1_watcher.data = &statd;
ev_signal_start(statd.ev_loop, &sigusr1_watcher);
+ ev_signal_init(&sighup_watcher, sighup_cb, SIGHUP);
+ sighup_watcher.data = &statd;
+ ev_signal_start(statd.ev_loop, &sighup_watcher);
+
err = journal_start(&statd.journal, statd.sr_query_ses);
if (err) {
sr_session_stop(statd.sr_query_ses);
diff --git a/utils/libll.sh b/utils/libll.sh
index 5698e765a..8d15c2c24 100644
--- a/utils/libll.sh
+++ b/utils/libll.sh
@@ -197,12 +197,31 @@ llscan_mdns()
{
local all="$1"
local flags="-tarp"
+ local tmpfile i c pid
if avahi-browse --help 2>&1 | grep -q -- '-k'; then
flags="-tarpk"
fi
- avahi-browse $flags 2>/dev/null | awk -F';' -v show_all="$all" '
+ tmpfile=$(mktemp)
+ avahi-browse $flags 2>/dev/null >"$tmpfile" &
+ pid=$!
+
+ if [ -t 2 ]; then
+ i=0
+ while kill -0 "$pid" 2>/dev/null; do
+ case $((i % 4)) in
+ 0) c='-';; 1) c="\\";; 2) c='|';; 3) c='/';;
+ esac
+ printf "\r[%s] Scanning, please wait ..." "$c" >&2
+ i=$((i + 1))
+ sleep 0.2
+ done
+ printf "\r\033[2K" >&2
+ fi
+
+ wait "$pid"
+ awk -F';' -v show_all="$all" '
$1 == "=" {
host = $7
proto = $3
@@ -279,7 +298,8 @@ llscan_mdns()
printf "\n%d device(s) found.\n", ndevs
}
- '
+ ' "$tmpfile"
+ rm -f "$tmpfile"
}
llscan_ll()