mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
Merge pull request #1453 from kernelkit/discovery-docs
mDNS discovery improvements
This commit is contained in:
@@ -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
|
||||
|
||||
+86
-92
@@ -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
|
||||
<pre class="cli"><code>linux-pc:# <b>ping -6 -L -c 3 ff02::1%tap0</b>
|
||||
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:#
|
||||
</code></pre>
|
||||
|
||||
> [!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
|
||||
<pre class="cli"><code>linux-pc:# <b>ssh admin@fe80::ff:fec0:ffed%tap0</b>
|
||||
admin@fe80::ff:fec0:ffed%tap0's password: admin
|
||||
admin@infix-c0-ff-ee:~$
|
||||
```
|
||||
admin@infix-c0-ff-ee:~$
|
||||
</code></pre>
|
||||
|
||||
### 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
|
||||
```
|
||||
<pre class="cli"><code>C:\> <b>ping infix-c0-ff-ee.local</b>
|
||||
C:\> <b>ssh admin@infix-c0-ff-ee.local</b>
|
||||
</code></pre>
|
||||
|
||||
> [!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
|
||||
<pre class="cli"><code>linux-pc:# <b>tcpdump -i tap0 -Qin -v ether proto 0x88cc</b>
|
||||
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:#
|
||||
</code></pre>
|
||||
|
||||
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
|
||||
<pre class="cli"><code>linux-pc:# <b>sudo tcpdump -i tap0 -Qin -v ether proto 0x88cc</b>
|
||||
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:#
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
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:/>
|
||||
```
|
||||
<pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
|
||||
admin@infix-c0-ff-ee:/config/> <b>no lldp</b>
|
||||
admin@infix-c0-ff-ee:/config/> <b>leave</b>
|
||||
admin@infix-c0-ff-ee:/>
|
||||
</code></pre>
|
||||
|
||||
To reenable it from the CLI config mode:
|
||||
|
||||
```
|
||||
admin@test-00-01-00:/config/> set lldp enabled
|
||||
admin@test-00-01-00:/config/> leave
|
||||
```
|
||||
<pre class="cli"><code>admin@test-00-01-00:/config/> <b>set lldp enabled</b>
|
||||
admin@test-00-01-00:/config/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
### 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
|
||||
```
|
||||
<pre class="cli"><code>admin@test-00-01-00:/config/> <b>set lldp message-tx-interval 1</b>
|
||||
admin@test-00-01-00:/config/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
### 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
|
||||
```
|
||||
<pre class="cli"><code>admin@test-00-01-00:/config/> <b>set lldp port e8 dest-mac-address 01:80:C2:00:00:0E admin-status disabled</b>
|
||||
admin@test-00-01-00:/config/> <b>set lldp port e5 dest-mac-address 01:80:C2:00:00:0E admin-status rx-only</b>
|
||||
admin@test-00-01-00:/config/> <b>set lldp port e6 dest-mac-address 01:80:C2:00:00:0E admin-status tx-only</b>
|
||||
admin@test-00-01-00:/config/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
> [!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
|
||||
<pre class="cli"><code>admin@test-00-01-00:/> <b>show lldp</b>
|
||||
<span class="header">INTERFACE REM-IDX TIME CHASSIS-ID PORT-ID </span>
|
||||
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
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
## 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
|
||||
```
|
||||
<pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
|
||||
admin@infix-c0-ff-ee:/config/> <b>edit mdns</b>
|
||||
admin@infix-c0-ff-ee:/config/mdns/> <b>set domain lan</b>
|
||||
</code></pre>
|
||||
|
||||
Other available settings include limiting the interfaces mDNS responder
|
||||
acts on:
|
||||
acts on, `allow`:
|
||||
|
||||
```
|
||||
admin@infix-c0-ff-ee:/config/> set interfaces allow e1
|
||||
```
|
||||
<pre class="cli"><code>admin@infix-c0-ff-ee:/config/> <b>set interfaces allow e1</b>
|
||||
</code></pre>
|
||||
|
||||
or
|
||||
or `deny`. The `allow` and `deny` settings are complementary, `deny` always wins.
|
||||
|
||||
```
|
||||
admin@infix-c0-ff-ee:/config/> set interfaces deny wan
|
||||
```
|
||||
<pre class="cli"><code>admin@infix-c0-ff-ee:/config/> <b>set interfaces deny wan</b>
|
||||
</code></pre>
|
||||
|
||||
The `allow` and `deny` settings are complementary, `deny` always wins.
|
||||
Use `leave` to activate the new settings, then inspect the operational
|
||||
state and any detected neighbors with `show mdns` from admin-exec
|
||||
context:
|
||||
|
||||
<pre class="cli"><code>admin@gateway:/> <b>show mdns</b>
|
||||
Enabled : yes
|
||||
Domain : local
|
||||
Deny : wan
|
||||
|
||||
<span class="header">HOSTNAME ADDRESS LAST SEEN SERVICES </span>
|
||||
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)
|
||||
</code></pre>
|
||||
|
||||
----
|
||||
|
||||
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
|
||||
<pre class="cli"><code>linux-pc:# <b>avahi-browse -ar</b>
|
||||
+ 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:#
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
> [!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
|
||||
<pre class="cli"><code>linux-pc:# <b>avahi-resolve-host-name infix-c0-ff-ee.local</b>
|
||||
infix-c0-ff-ee.local 10.0.1.1
|
||||
linux-pc:#
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
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
|
||||
<pre class="cli"><code>linux-pc:# <b>ping infix-c0-ff-ee.local -c 3</b>
|
||||
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:# <b>ssh admin@infix-c0-ff-ee.local</b>
|
||||
(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:#
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
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
|
||||
```
|
||||
<pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
|
||||
admin@infix-c0-ff-ee:/config/> <b>no mdns</b>
|
||||
admin@infix-c0-ff-ee:/config/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
### 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
|
||||
<pre class="cli"><code>linux-pc:# <b>ping infix.local -c 3</b>
|
||||
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:# <b>ssh admin@infix.local</b>
|
||||
(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:~$
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
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
|
||||
```
|
||||
<pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
|
||||
admin@infix-c0-ff-ee:/config/> <b>edit web</b>
|
||||
admin@infix-c0-ff-ee:/config/web/> <b>no netbrowse</b>
|
||||
admin@infix-c0-ff-ee:/config/web/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
|
||||
[^1]: E.g., [lldpd](https://github.com/lldp/lldpd) which includes the
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 73 KiB |
@@ -1,3 +1,3 @@
|
||||
# Locally calculated
|
||||
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
|
||||
sha256 cd9bc969350b8b30d9a7a31b0f19fb3218c602f04fe7e6a1d80682a75ed26d18 klish-3ae496c43d90354ffa94d364d7775c089f0e119a-git4.tar.gz
|
||||
sha256 be6548a5a4f8c35906b02ea0ccb64cdd94d48bfe7133801353fbf658aa33d5c0 klish-8dca4da70a7794d5f4e0b047724bfe9e2088ebf3-git4.tar.gz
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <pid/mdns> 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 --
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
service <!pid/confd,usr/bootstrap> 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] <!pid/confd,usr/bootstrap> avahi-daemon -s $AVAHI_ARGS \
|
||||
-- Avahi mDNS-SD daemon
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
#set DEBUG=1
|
||||
|
||||
service name:statd [12345] <pid/confd> statd -f -p /run/statd.pid -n -- Status daemon
|
||||
|
||||
+245
-65
@@ -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
|
||||
}
|
||||
if oj == 0 {
|
||||
oj = 999
|
||||
}
|
||||
return oi < oj
|
||||
})
|
||||
}
|
||||
return buildHosts(svcsMap, addrMap, productMap, versionMap, vvHosts, legHosts, mgmtHosts)
|
||||
}
|
||||
|
||||
// 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,
|
||||
// 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 !strings.Contains(a, ":") {
|
||||
addrMap[link] = a // IPv4 wins, stop looking
|
||||
break
|
||||
}
|
||||
if addrMap[link] == "" {
|
||||
addrMap[link] = a // IPv6 fallback
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
@@ -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(' · ');
|
||||
}
|
||||
|
||||
+22
-9
@@ -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 {
|
||||
|
||||
+129
-19
@@ -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.
|
||||
*
|
||||
* Example log (mDNS enabled, daemon stays down):
|
||||
* avahi: mDNS daemon not responding (attempt 3/3) — check that the mdns
|
||||
* service is running
|
||||
* 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.
|
||||
*
|
||||
* 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);
|
||||
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+12
-1
@@ -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);
|
||||
|
||||
+22
-2
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user