Merge pull request #1453 from kernelkit/discovery-docs

mDNS discovery improvements
This commit is contained in:
Joachim Wiberg
2026-03-26 14:20:12 +01:00
committed by GitHub
18 changed files with 543 additions and 203 deletions
+12 -2
View File
@@ -8,6 +8,8 @@ on:
env: env:
PROJECT_NAME: Infix PROJECT_NAME: Infix
CONTACT_EMAIL: troglobit@gmail.com CONTACT_EMAIL: troglobit@gmail.com
LIBYANG_VERSION: 4.2.2
SYSREPO_VERSION: 4.2.10
jobs: jobs:
coverity: coverity:
@@ -59,12 +61,20 @@ jobs:
- name: Build dependencies - name: Build dependencies
run: | 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 mkdir libyang/build
(cd libyang/build && cmake .. && make all && sudo make install) (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 mkdir sysrepo/build
(cd sysrepo/build && cmake .. && make all && sudo make install) (cd sysrepo/build && cmake .. && make all && sudo make install)
git clone https://github.com/troglobit/libite.git git clone https://github.com/troglobit/libite.git
(cd libite && ./autogen.sh && ./configure && make && sudo make install) (cd libite && ./autogen.sh && ./configure && make && sudo make install)
make dep make dep
+76 -82
View File
@@ -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 example, the PC here uses *tap0* as *if1*, Infix responds with address
*fe80::ff:fec0:ffed*. *fe80::ff:fec0:ffed*.
``` <pre class="cli"><code>linux-pc:# <b>ping -6 -L -c 3 ff02::1%tap0</b>
linux-pc:# ping -6 -L -c 3 ff02::1%tap0
PING ff02::1%tap0(ff02::1%tap0) 56 data bytes 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=1 ttl=64 time=0.558 ms
64 bytes from fe80::ff:fec0:ffed%tap0: icmp_seq=2 ttl=64 time=0.419 ms 64 bytes from fe80::ff:fec0:ffed%tap0: icmp_seq=2 ttl=64 time=0.419 ms
@@ -29,7 +28,7 @@ PING ff02::1%tap0(ff02::1%tap0) 56 data bytes
3 packets transmitted, 3 received, 0% packet loss, time 2043ms 3 packets transmitted, 3 received, 0% packet loss, time 2043ms
rtt min/avg/max/mdev = 0.389/0.455/0.558/0.073 ms rtt min/avg/max/mdev = 0.389/0.455/0.558/0.073 ms
linux-pc:# linux-pc:#
``` </code></pre>
> [!TIP] > [!TIP]
> The `-L` option ignores local responses from the PC. > 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. This address can then be used to connect to the device, e.g., using SSH.
Notice the syntax `username@address%interface`: Notice the syntax `username@address%interface`:
``` <pre class="cli"><code>linux-pc:# <b>ssh admin@fe80::ff:fec0:ffed%tap0</b>
linux-pc:# ssh admin@fe80::ff:fec0:ffed%tap0
admin@fe80::ff:fec0:ffed%tap0's password: admin admin@fe80::ff:fec0:ffed%tap0's password: admin
admin@infix-c0-ff-ee:~$ admin@infix-c0-ff-ee:~$
``` </code></pre>
### Windows ### Windows
@@ -52,10 +50,9 @@ no extra software required.
From the command line, use the `.local` hostname directly: From the command line, use the `.local` hostname directly:
```cmd <pre class="cli"><code>C:\> <b>ping infix-c0-ff-ee.local</b>
C:\> ping infix-c0-ff-ee.local C:\> <b>ssh admin@infix-c0-ff-ee.local</b>
C:\> ssh admin@infix-c0-ff-ee.local </code></pre>
```
> [!NOTE] > [!NOTE]
> IPv6 multicast ping (`ping ff02::1%if1`) may not display responses on > 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 > confirm connectivity, Wireshark will show the ICMPv6 echo replies
> arriving. Use mDNS (see [mDNS-SD](#mdns-sd) below) as the reliable > arriving. Use mDNS (see [mDNS-SD](#mdns-sd) below) as the reliable
> alternative. > alternative.
>
![Wireshark showing IPv6 ping responses](https://github.com/addiva-elektronik/alder/assets/122900029/c45d7726-448f-4c30-878e-bcf976dff531) > ![Wireshark showing IPv6 ping responses](img/windows-ipv6-ping-reply.png)
## LLDP ## 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 settings, the link-local IPv6 address can be read from the Management
Address TLV using *tcpdump* or other sniffing tools[^1]: Address TLV using *tcpdump* or other sniffing tools[^1]:
``` <pre class="cli"><code>linux-pc:# <b>tcpdump -i tap0 -Qin -v ether proto 0x88cc</b>
linux-pc:# tcpdump -i tap0 -Qin -v ether proto 0x88cc
tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
15:51:52.061071 LLDP, length 193 15:51:52.061071 LLDP, length 193
Chassis ID TLV (1), length 7 Chassis ID TLV (1), length 7
@@ -104,7 +99,7 @@ tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144
^C ^C
1 packet captured 1 packet captured
linux-pc:# linux-pc:#
``` </code></pre>
If the device has an IPv4 address assigned, it is shown in an additional If the device has an IPv4 address assigned, it is shown in an additional
Management Address TLV. Management Address TLV.
@@ -116,8 +111,7 @@ Management Address TLV.
In the example below, the IPv4 address (10.0.1.1) happens to be 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. assigned to *eth0*, while the IPv6 address (2001:db8::1) is not.
``` <pre class="cli"><code>linux-pc:# <b>sudo tcpdump -i tap0 -Qin -v ether proto 0x88cc</b>
linux-pc:# sudo tcpdump -i tap0 -Qin -v ether proto 0x88cc
tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
15:46:07.908665 LLDP, length 207 15:46:07.908665 LLDP, length 207
Chassis ID TLV (1), length 7 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 2 packets received by filter
0 packets dropped by kernel 0 packets dropped by kernel
linux-pc:# linux-pc:#
``` </code></pre>
The following capabilities are available via NETCONF/RESTCONF or the Infix CLI. 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. The LLDP service can be disabled using the following commands.
``` <pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
admin@infix-c0-ff-ee:/> configure admin@infix-c0-ff-ee:/config/> <b>no lldp</b>
admin@infix-c0-ff-ee:/config/> no lldp admin@infix-c0-ff-ee:/config/> <b>leave</b>
admin@infix-c0-ff-ee:/config/> leave
admin@infix-c0-ff-ee:/> admin@infix-c0-ff-ee:/>
``` </code></pre>
To reenable it from the CLI config mode: To reenable it from the CLI config mode:
``` <pre class="cli"><code>admin@test-00-01-00:/config/> <b>set lldp enabled</b>
admin@test-00-01-00:/config/> set lldp enabled admin@test-00-01-00:/config/> <b>leave</b>
admin@test-00-01-00:/config/> leave </code></pre>
```
### LLDP Message Transmission Interval ### LLDP Message Transmission Interval
@@ -180,10 +172,9 @@ By default, LLDP uses a `message-tx-interval` of 30 seconds, as defined
by the IEEE standard. Infix allows this value to be customized. by the IEEE standard. Infix allows this value to be customized.
To change it using the CLI: To change it using the CLI:
``` <pre class="cli"><code>admin@test-00-01-00:/config/> <b>set lldp message-tx-interval 1</b>
admin@test-00-01-00:/config/> set lldp message-tx-interval 1 admin@test-00-01-00:/config/> <b>leave</b>
admin@test-00-01-00:/config/> leave </code></pre>
```
### LLDP Administrative Status per Interface ### LLDP Administrative Status per Interface
@@ -197,12 +188,11 @@ also supported:
Example configuration: Example configuration:
``` <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/> set lldp port e8 dest-mac-address 01:80:C2:00:00:0E admin-status disabled 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/> set lldp port e5 dest-mac-address 01:80:C2:00:00:0E admin-status rx-only 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/> set lldp port e6 dest-mac-address 01:80:C2:00:00:0E admin-status tx-only admin@test-00-01-00:/config/> <b>leave</b>
admin@test-00-01-00:/config/> leave </code></pre>
```
> [!NOTE] > [!NOTE]
> The destination MAC address must be the standard LLDP multicast > The destination MAC address must be the standard LLDP multicast
@@ -213,13 +203,12 @@ admin@test-00-01-00:/config/> leave
In CLI mode, Infix also provides a convenient `show lldp` command to In CLI mode, Infix also provides a convenient `show lldp` command to
list LLDP neighbors detected on each interface: list LLDP neighbors detected on each interface:
``` <pre class="cli"><code>admin@test-00-01-00:/> <b>show lldp</b>
admin@test-00-01-00:/> show lldp <span class="header">INTERFACE REM-IDX TIME CHASSIS-ID PORT-ID </span>
INTERFACE REM-IDX TIME CHASSIS-ID PORT-ID
e5 1 902 00:a0:85:00:04:01 00:a0:85:00:04:07 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 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 e8 2 901 00:a0:85:00:02:01 00:a0:85:00:02:05
``` </code></pre>
## mDNS-SD ## 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 default, Infix use the `.local` domain for advertising services. Some
networks use `.lan` instead, so this configurable: networks use `.lan` instead, so this configurable:
``` <pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
admin@infix-c0-ff-ee:/> configure admin@infix-c0-ff-ee:/config/> <b>edit mdns</b>
admin@infix-c0-ff-ee:/config/> edit mdns admin@infix-c0-ff-ee:/config/mdns/> <b>set domain lan</b>
admin@infix-c0-ff-ee:/config/mdns/> set domain lan </code></pre>
```
Other available settings include limiting the interfaces mDNS responder Other available settings include limiting the interfaces mDNS responder
acts on: acts on, `allow`:
``` <pre class="cli"><code>admin@infix-c0-ff-ee:/config/> <b>set interfaces allow e1</b>
admin@infix-c0-ff-ee:/config/> set interfaces allow e1 </code></pre>
```
or or `deny`. The `allow` and `deny` settings are complementary, `deny` always wins.
``` <pre class="cli"><code>admin@infix-c0-ff-ee:/config/> <b>set interfaces deny wan</b>
admin@infix-c0-ff-ee:/config/> set interfaces deny wan </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 In Linux, tools such as *avahi-browse* or *mdns-scan*[^2] can be used to
search for devices advertising their services via mDNS. search for devices advertising their services via mDNS.
``` <pre class="cli"><code>linux-pc:# <b>avahi-browse -ar</b>
linux-pc:# avahi-browse -ar
+ tap0 IPv6 infix-c0-ff-ee SFTP File Transfer local + tap0 IPv6 infix-c0-ff-ee SFTP File Transfer local
+ tap0 IPv4 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 + tap0 IPv6 infix-c0-ff-ee SSH Remote Terminal local
@@ -281,7 +280,7 @@ linux-pc:# avahi-browse -ar
txt = [] txt = []
^C ^C
linux-pc:# linux-pc:#
``` </code></pre>
> [!TIP] > [!TIP]
> The `-t` option is also very useful, it stops browsing automatically > 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 addresses. This function allows users to confirm that addresses are
mapped correctly. mapped correctly.
``` <pre class="cli"><code>linux-pc:# <b>avahi-resolve-host-name infix-c0-ff-ee.local</b>
linux-pc:# avahi-resolve-host-name infix-c0-ff-ee.local
infix-c0-ff-ee.local 10.0.1.1 infix-c0-ff-ee.local 10.0.1.1
linux-pc:# linux-pc:#
``` </code></pre>
Thanks to mDNS we can use the advertised name instead of the IP Thanks to mDNS we can use the advertised name instead of the IP
address for operations like `ping` and `ssh` as shown below: address for operations like `ping` and `ssh` as shown below:
``` <pre class="cli"><code>linux-pc:# <b>ping infix-c0-ff-ee.local -c 3</b>
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. 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=1 ttl=64 time=0.852 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=1.12 ms 64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=1.12 ms
@@ -314,7 +311,7 @@ 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 3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.852/1.105/1.348/0.202 ms rtt min/avg/max/mdev = 0.852/1.105/1.348/0.202 ms
linux-pc:# ssh admin@infix-c0-ff-ee.local linux-pc:# <b>ssh admin@infix-c0-ff-ee.local</b>
(admin@infix-c0-ff-ee.local) Password: (admin@infix-c0-ff-ee.local) Password:
.-------. .-------.
| . . | Infix OS — Immutable.Friendly.Secure | . . | Infix OS — Immutable.Friendly.Secure
@@ -324,27 +321,25 @@ linux-pc:# ssh admin@infix-c0-ff-ee.local
Run the command 'cli' for interactive OAM Run the command 'cli' for interactive OAM
linux-pc:# linux-pc:#
``` </code></pre>
To disable mDNS/mDNS-SD, type the commands: To disable mDNS/mDNS-SD, type the commands:
``` <pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
admin@infix-c0-ff-ee:/> configure admin@infix-c0-ff-ee:/config/> <b>no mdns</b>
admin@infix-c0-ff-ee:/config/> no mdns admin@infix-c0-ff-ee:/config/> <b>leave</b>
admin@infix-c0-ff-ee:/config/> leave </code></pre>
```
### Human-Friendly Hostname Alias ### 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 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 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 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 examples below show how the alias can be used for actions such as
pinging or establishing an SSH connection: pinging or establishing an SSH connection:
``` <pre class="cli"><code>linux-pc:# <b>ping infix.local -c 3</b>
linux-pc:# ping infix.local -c 3
PING infix.local (10.0.1.1) 56(84) bytes of data. 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=1 ttl=64 time=0.751 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=2.28 ms 64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=2.28 ms
@@ -354,7 +349,7 @@ PING infix.local (10.0.1.1) 56(84) bytes of data.
3 packets transmitted, 3 received, 0% packet loss, time 2003ms 3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.751/1.482/2.281/0.626 ms rtt min/avg/max/mdev = 0.751/1.482/2.281/0.626 ms
linux-pc:# ssh admin@infix.local linux-pc:# <b>ssh admin@infix.local</b>
(admin@infix.local) Password: (admin@infix.local) Password:
.-------. .-------.
| . . | Infix OS — Immutable.Friendly.Secure | . . | Infix OS — Immutable.Friendly.Secure
@@ -364,7 +359,7 @@ linux-pc:# ssh admin@infix.local
Run the command 'cli' for interactive OAM Run the command 'cli' for interactive OAM
admin@infix-c0-ff-ee:~$ admin@infix-c0-ff-ee:~$
``` </code></pre>
When multiple Infix devices are present on the LAN the alias will not 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 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 To disable the netbrowse service, and the *network.local* alias, the
following commands can be used: following commands can be used:
``` <pre class="cli"><code>admin@infix-c0-ff-ee:/> <b>configure</b>
admin@infix-c0-ff-ee:/> configure admin@infix-c0-ff-ee:/config/> <b>edit web</b>
admin@infix-c0-ff-ee:/config/> edit web admin@infix-c0-ff-ee:/config/web/> <b>no netbrowse</b>
admin@infix-c0-ff-ee:/config/web/> no netbrowse admin@infix-c0-ff-ee:/config/web/> <b>leave</b>
admin@infix-c0-ff-ee:/config/web/> leave </code></pre>
```
[^1]: E.g., [lldpd](https://github.com/lldp/lldpd) which includes the [^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 -1
View File
@@ -1,3 +1,3 @@
# Locally calculated # Locally calculated
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
sha256 cd9bc969350b8b30d9a7a31b0f19fb3218c602f04fe7e6a1d80682a75ed26d18 klish-3ae496c43d90354ffa94d364d7775c089f0e119a-git4.tar.gz sha256 be6548a5a4f8c35906b02ea0ccb64cdd94d48bfe7133801353fbf658aa33d5c0 klish-8dca4da70a7794d5f4e0b047724bfe9e2088ebf3-git4.tar.gz
+1 -1
View File
@@ -4,7 +4,7 @@
# #
################################################################################ ################################################################################
KLISH_VERSION = 3ae496c43d90354ffa94d364d7775c089f0e119a KLISH_VERSION = 8dca4da70a7794d5f4e0b047724bfe9e2088ebf3
KLISH_SITE = https://github.com/kernelkit/klish.git KLISH_SITE = https://github.com/kernelkit/klish.git
#KLISH_VERSION = tags/3.0.0 #KLISH_VERSION = tags/3.0.0
#KLISH_SITE = https://src.libcode.org/pkun/klish.git #KLISH_SITE = https://src.libcode.org/pkun/klish.git
+1 -1
View File
@@ -1,5 +1,5 @@
# From GitHub release # From GitHub release
sha256 cf32b3c224325b3b660669a82e6dc30ad8438016d7492433cea591fa3a8a1dd9 mdns-alias-1.1.tar.gz sha256 9f194fa0b6e34fd915054394ef5b820a4f6b1755ace5ed1011bfba6df550accf mdns-alias-1.2.tar.gz
# Locally generated # Locally generated
sha256 3d6f910b5e198f3daab48047b8ee6949040f7abee3927daf2e231f265faf7d91 LICENSE sha256 3d6f910b5e198f3daab48047b8ee6949040f7abee3927daf2e231f265faf7d91 LICENSE
+1 -1
View File
@@ -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_SITE = https://github.com/troglobit/mdns-alias/releases/download/v$(MDNS_ALIAS_VERSION)
MDNS_ALIAS_LICENSE = ISC MDNS_ALIAS_LICENSE = ISC
MDNS_ALIAS_LICENSE_FILES = LICENSE MDNS_ALIAS_LICENSE_FILES = LICENSE
+1 -2
View File
@@ -1,5 +1,4 @@
# Avahi advertises the system default hostname, this service advertises # Avahi advertises the system default hostname, this service advertises
# /etc/hostname (-H) and, optionally, network.local as CNAMEs. Changes # /etc/hostname (-H) and, optionally, network.local as CNAMEs. Changes
# to /etc/default/mdns-alias will cause Finit to restart not reload. # to /etc/default/mdns-alias will cause Finit to restart not reload.
service <pid/mdns> env:-/etc/default/mdns-alias \ service [2345] env:-/etc/default/mdns-alias mdns-alias -H $MDNS_ALIAS_ARGS --
[2345] mdns-alias -H $MDNS_ALIAS_ARGS --
+2 -1
View File
@@ -1,2 +1,3 @@
service <!> name:netbrowse log:prio:daemon.debug,tag:netbrowse \ 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 \ service name:mdns pid:!/run/avahi-daemon/pid env:-/etc/default/avahi \
[2345] avahi-daemon -s $AVAHI_ARGS -- Avahi mDNS-SD daemon [2345] <!pid/confd,usr/bootstrap> avahi-daemon -s $AVAHI_ARGS \
-- Avahi mDNS-SD daemon
-1
View File
@@ -1,3 +1,2 @@
#set DEBUG=1 #set DEBUG=1
service name:statd [12345] <pid/confd> statd -f -p /run/statd.pid -n -- Status daemon service name:statd [12345] <pid/confd> statd -f -p /run/statd.pid -n -- Status daemon
+245 -65
View File
@@ -2,6 +2,8 @@
package main package main
import ( import (
"encoding/json"
"fmt"
"log" "log"
"os/exec" "os/exec"
"sort" "sort"
@@ -42,6 +44,74 @@ var typeOrder = map[string]int{
"HTTPS": 1, "HTTP": 2, "SSH": 3, "SFTP": 4, "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. // hasK checks whether avahi-browse supports the -k flag.
func hasK() bool { func hasK() bool {
out, err := exec.Command("avahi-browse", "--help").CombinedOutput() out, err := exec.Command("avahi-browse", "--help").CombinedOutput()
@@ -82,23 +152,21 @@ func scan() map[string]Host {
continue continue
} }
family := parts[2] family := parts[2]
serviceName := parts[3] serviceName := parts[3]
serviceType := parts[4] serviceType := parts[4]
link := parts[6] link := parts[6]
address := parts[7] address := parts[7]
port := parts[8] port := parts[8]
txt := parts[9] txt := strings.Join(parts[9:], ";")
if family != "IPv4" && family != "IPv6" { if family != "IPv4" && family != "IPv6" {
continue continue
} }
info, known := knownServices[serviceType] info, known := knownServices[serviceType]
displayName := info.displayName if known {
urlTemplate := info.urlTemplate mgmtHosts[link] = true
if !known {
displayName = serviceType
} }
// vv=1 is the platform marker set by confd/services.c, survives OS // 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 // (ssh, web, netconf, restconf) to avoid false positives from Apple
// devices, which also use vv=1 in their AirPlay/RAOP TXT records. // 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. // 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. // Prefer real IPv4; skip loopback and link-local.
// Loopback (127.x / ::1) appears when avahi resolves local-machine // 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://..." // "ty=Brother DCP-L3550CDW series" "adminurl=http://..."
// Split on the between-record boundary `" "` (close-quote space // Split on the between-record boundary `" "` (close-quote space
// open-quote) to keep each record intact, then trim outer quotes. // open-quote) to keep each record intact, then trim outer quotes.
var path, adminurl, product, version string var records []string
for _, record := range strings.Split(txt, "\" \"") { for _, rec := range strings.Split(txt, "\" \"") {
stripped := strings.Trim(record, "\"") records = append(records, strings.Trim(rec, "\""))
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:]
}
} }
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. // 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] == "" { if product != "" && productMap[link] == "" {
productMap[link] = product productMap[link] = product
} }
if version != "" && versionMap[link] == "" { if meta.version != "" && versionMap[link] == "" {
versionMap[link] = version versionMap[link] = meta.version
}
displayName := info.displayName
if !known {
displayName = serviceType
} }
var url string var url string
if adminurl != "" { if meta.adminurl != "" {
url = adminurl url = meta.adminurl
} else if urlTemplate != "" { } else if info.urlTemplate != "" {
url = strings.NewReplacer( url = strings.NewReplacer(
"{address}", address, "{address}", address,
"{port}", port, "{port}", port,
"{path}", path, "{path}", meta.path,
).Replace(urlTemplate) ).Replace(info.urlTemplate)
} }
svc := Service{ svc := Service{
@@ -172,7 +236,9 @@ func scan() map[string]Host {
URL: url, 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 dup := false
for _, existing := range svcsMap[link] { for _, existing := range svcsMap[link] {
if existing.Type == svc.Type && existing.Name == svc.Name && existing.URL == svc.URL { 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 return buildHosts(svcsMap, addrMap, productMap, versionMap, vvHosts, legHosts, mgmtHosts)
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 // JSON types for the operational-state backend.
// qualifies if it has vv=1 on a management service (to exclude Apple type opRoot struct {
// AirPlay collisions), or on=Infix for older firmware predating vv=1. MDNS struct {
hosts := make(map[string]Host) Enabled bool `json:"enabled"`
for link, svcs := range svcsMap { Neighbors struct {
isInfix := (vvHosts[link] && mgmtHosts[link]) || legHosts[link] Neighbor []opNeighbor `json:"neighbor"`
hosts[link] = Host{ } `json:"neighbors"`
Addr: addrMap[link], } `json:"infix-services:mdns"`
Product: productMap[link], }
Version: versionMap[link],
Other: !isInfix, type opNeighbor struct {
Svcs: svcs, 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: // decode handles avahi's DNS-SD escape sequences in service names:
+1 -1
View File
@@ -458,7 +458,7 @@
} }
var parts = [label]; var parts = [label];
if (lastTs) { if (lastTs) {
parts.push('updated ' + lastTs.toLocaleTimeString()); parts.push('updated ' + lastTs.toLocaleTimeString(undefined, { hour12: false }));
} }
foot.textContent = parts.join(' · '); foot.textContent = parts.join(' · ');
} }
+22 -9
View File
@@ -25,24 +25,37 @@ func browseHandler(w http.ResponseWriter, r *http.Request) {
w.Write(browseHTML) w.Write(browseHTML)
} }
func dataHandler(w http.ResponseWriter, r *http.Request) { func dataHandler(scanFn func() map[string]Host) http.HandlerFunc {
hosts := scan() return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") hosts := scanFn()
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(hosts); err != nil { w.Header().Set("Cache-Control", "no-store")
log.Printf("json: %v", err) if err := json.NewEncoder(w).Encode(hosts); err != nil {
log.Printf("json: %v", err)
}
} }
} }
func main() { 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() 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 := http.NewServeMux()
mux.HandleFunc("/", browseHandler) mux.HandleFunc("/", browseHandler)
mux.HandleFunc("/netbrowse", browseHandler) mux.HandleFunc("/netbrowse", browseHandler)
mux.HandleFunc("/data", dataHandler) mux.HandleFunc("/data", dataHandler(scanFn))
mux.HandleFunc("/netbrowse/data", dataHandler) mux.HandleFunc("/netbrowse/data", dataHandler(scanFn))
staticSub, err := fs.Sub(staticFS, "static") staticSub, err := fs.Sub(staticFS, "static")
if err != nil { if err != nil {
+129 -19
View File
@@ -380,7 +380,7 @@ static void ds_push_resolver(struct mdns_ctx *ctx, struct avahi_service *svc,
char val[64]; char val[64];
struct avahi_txt *t; struct avahi_txt *t;
char ts[32]; char ts[32];
int err = 0; int err;
xpath_str(qname, sizeof(qname), svc->name); 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) */ * rejects editing list-key leaves directly — set the list entry instead) */
snprintf(xpath, sizeof(xpath), snprintf(xpath, sizeof(xpath),
XPATH_BASE "/neighbor[hostname='%s']", svc->hostname); 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) */ /* address (only if a new one was added) */
if (new_addr) { if (new_addr) {
@@ -542,16 +542,49 @@ static void resolver_cb(AvahiServiceResolver *r,
svc->port = port; 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)) { for (s = txtlist; s; s = avahi_string_list_get_next(s)) {
uint8_t *data = avahi_string_list_get_text(s); uint8_t *data = avahi_string_list_get_text(s);
size_t len = avahi_string_list_get_size(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)); t = calloc(1, sizeof(*t));
if (!t) if (!t)
break; break;
snprintf(t->val, sizeof(t->val), "%.*s", (int)len, (char *)data); snprintf(t->val, sizeof(t->val), "%.*s", (int)len, (char *)data);
LIST_INSERT_HEAD(&svc->txts, t, link); 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); ds_push_resolver(ctx, svc, new_addr);
@@ -714,31 +747,59 @@ static bool mdns_is_enabled(struct mdns_ctx *ctx)
return enabled; 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 * Reconnect timer: fires MDNS_RECONN_DELAY seconds after AVAHI_CLIENT_FAILURE.
* to 3 times). Only logs ERROR once all retries are exhausted AND mDNS is * Frees the broken client and creates a fresh one. libavahi's own AVAHI_CLIENT_NO_FAIL
* enabled in the running config — this avoids noisy errors when the operator * reconnection can miss D-Bus NameOwnerChanged events; explicit free+recreate is
* has simply disabled the mDNS service. * more reliable (same pattern used by mdns-alias).
*
* Example log (mDNS enabled, daemon stays down):
* avahi: mDNS daemon not responding (attempt 3/3) — check that the mdns
* service is running
*/ */
#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) static void mdns_retry_cb(struct ev_loop *loop, ev_timer *w, int revents)
{ {
struct mdns_ctx *ctx = (struct mdns_ctx *) struct mdns_ctx *ctx = (struct mdns_ctx *)
((char *)w - offsetof(struct mdns_ctx, retry_timer)); ((char *)w - offsetof(struct mdns_ctx, retry_timer));
(void)loop;
(void)revents;
ctx->fail_count++; 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)) if (mdns_is_enabled(ctx))
ERROR("mdns: mDNS daemon not responding (attempt %d/3) — " WARN("mdns: mDNS daemon not responding, will reconnect automatically");
"check that the mdns service is running", ctx->fail_count);
} }
static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata) 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) { switch (state) {
case AVAHI_CLIENT_S_RUNNING: case AVAHI_CLIENT_S_RUNNING:
if (ctx->fail_count > 0) { if (ctx->fail_count > 0) {
ev_timer_stop(ctx->loop, &ctx->reconn_timer);
ev_timer_stop(ctx->loop, &ctx->retry_timer); ev_timer_stop(ctx->loop, &ctx->retry_timer);
NOTE("mdns: mDNS daemon reconnected"); NOTE("mdns: mDNS daemon reconnected");
ctx->fail_count = 0; 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) * will log only if the daemon stays down for 3 attempts (~6 s)
* and mDNS is enabled in the running config. * 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)) { 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); 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; 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) void mdns_ctx_exit(struct mdns_ctx *ctx)
{ {
struct avahi_type_entry *te; 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)) if (ev_is_active(&ctx->retry_timer))
ev_timer_stop(ctx->loop, &ctx->retry_timer); ev_timer_stop(ctx->loop, &ctx->retry_timer);
+4 -2
View File
@@ -58,14 +58,16 @@ struct mdns_ctx {
AvahiClient *client; AvahiClient *client;
AvahiServiceTypeBrowser *type_browser; AvahiServiceTypeBrowser *type_browser;
AvahiPoll poll_api; /* libev-backed vtable */ AvahiPoll poll_api; /* libev-backed vtable */
unsigned int fail_count; /* Consecutive avahi-daemon connection failures */ unsigned int fail_count; /* Non-zero while avahi-daemon is absent */
ev_timer retry_timer; /* Deferred error-log timer */ 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_neighbor) neighbors;
LIST_HEAD(, avahi_service) services; /* Flat list; keyed by 5-tuple */ LIST_HEAD(, avahi_service) services; /* Flat list; keyed by 5-tuple */
LIST_HEAD(, avahi_type_entry) type_entries; 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); 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); void mdns_ctx_exit(struct mdns_ctx *ctx);
#endif #endif
+12 -1
View File
@@ -352,6 +352,13 @@ static void sigusr1_cb(struct ev_loop *, struct ev_signal *, int)
debug ^= 1; 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) 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[]) 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; int log_opts = LOG_PID | LOG_NDELAY;
struct statd statd = {}; struct statd statd = {};
const char *env; const char *env;
@@ -516,6 +523,10 @@ int main(int argc, char *argv[])
sigusr1_watcher.data = &statd; sigusr1_watcher.data = &statd;
ev_signal_start(statd.ev_loop, &sigusr1_watcher); 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); err = journal_start(&statd.journal, statd.sr_query_ses);
if (err) { if (err) {
sr_session_stop(statd.sr_query_ses); sr_session_stop(statd.sr_query_ses);
+22 -2
View File
@@ -197,12 +197,31 @@ llscan_mdns()
{ {
local all="$1" local all="$1"
local flags="-tarp" local flags="-tarp"
local tmpfile i c pid
if avahi-browse --help 2>&1 | grep -q -- '-k'; then if avahi-browse --help 2>&1 | grep -q -- '-k'; then
flags="-tarpk" flags="-tarpk"
fi 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 == "=" { $1 == "=" {
host = $7 host = $7
proto = $3 proto = $3
@@ -279,7 +298,8 @@ llscan_mdns()
printf "\n%d device(s) found.\n", ndevs printf "\n%d device(s) found.\n", ndevs
} }
' ' "$tmpfile"
rm -f "$tmpfile"
} }
llscan_ll() llscan_ll()