diff --git a/README.md b/README.md index 81fc748..f009421 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Mender-enabled OS images for Raspberry Pi 5 radar nodes. The image comes pre-loa - **SDRconnect v1.0.5** for standalone SDR analysis - **Chrony** for NTP clock disciplining - **Cloudflared** for secure tunneling -- **Avahi mDNS** for `.local` discovery +- **Avahi mDNS** for `ret.local` per-node discovery, plus a shared `owl.local` - **WiFi Connect** captive portal for network setup - **Mender client** for OTA updates @@ -50,6 +50,16 @@ During a retina-node install, retina-gui holds an `install.lock` in `/data/retin After deploying retina-node, visit `http://owl.local` to configure capture settings, location, ADS-B truth source, and tar1090. See [retina-node](https://github.com/offworldlabs/retina-node) for details. +### Addressing nodes + +Each node has a permanent name of its own, `ret.local`, derived from the Mender node_id and unchanged for the life of the board. It is shown under **Configuration > This node** and is the address to bookmark. + +`owl.local` is a shared entry point published by *every* node at once, so it reaches whichever one answers first. With a single node on the network it redirects to that node. With more than one it shows a list of every node found, each linking to its own `ret.local`. A node dropping off the network does not take `owl.local` with it — the others were already answering it. + +Use `owl.local` to find a node, and `ret.local` to work with one. In particular **do not use `owl.local` for SSH**: it can resolve to a different node between connections, which will trip `REMOTE HOST IDENTIFICATION HAS CHANGED`. + +Nodes can be given a friendly name under **Configuration > This node**. It is only a label for the node list, so renaming never breaks a bookmark or an SSH config. That section also shows the node's own `ret.local` address. + ### Cloudflare Tunnel (Optional) To enable Cloudflare tunnel forwarding, create a token file on the node: @@ -67,11 +77,13 @@ The token persists across OTA updates. **End users:** Add your SSH key via the web GUI at `http://owl.local` after boot. Once added, connect with: ```bash -ssh node@owl.local +ssh node@ret.local # or by IP ssh node@ ``` +Always SSH to the node's own `ret.local`, never to `owl.local` — that name is answered by every node on the network, so which host you land on can change between connections and SSH will refuse on the host key mismatch. **Configuration > This node** shows its address. + Keys persist across reboots and OTA updates. **Developers:** Public keys can be baked into the image at build time by adding them to `ssh_pub_keys/`: diff --git a/plugins/playbooks/os_setup/main.yml b/plugins/playbooks/os_setup/main.yml index 0802fe6..fb1619e 100644 --- a/plugins/playbooks/os_setup/main.yml +++ b/plugins/playbooks/os_setup/main.yml @@ -87,6 +87,9 @@ - role: wifi_reconnect # Rejoin the saved network after an outage become: True become_user: root + - role: mdns_identity # Per-node ret.local, shared owl.local + become: True + become_user: root - role: cpu_governor # Set CPU governor to schedutil for better performance become: True become_user: root diff --git a/plugins/playbooks/os_setup/roles/mdns_identity/files/owl-mdns-alias b/plugins/playbooks/os_setup/roles/mdns_identity/files/owl-mdns-alias new file mode 100644 index 0000000..5971bf5 --- /dev/null +++ b/plugins/playbooks/os_setup/roles/mdns_identity/files/owl-mdns-alias @@ -0,0 +1,256 @@ +#!/usr/bin/python3 + +"""Publish owl.local as a *shared* mDNS address record. + +owl.local is the fleet's entry point: whichever node answers it serves the +landing page, and since every node serves the same page it does not matter +which one that is. What does matter is that the name never goes away while any +node is up. + +Avahi's own conflict resolution cannot give us that. A host name is a unique +record, so the first node to claim owl.local keeps it, every other node backs +off to owl-2.local, and when the holder is unplugged the name simply dies — +Avahi has no logic to reclaim a name it lost. The obvious workaround, an +election between the nodes, means a watcher loop, claim/backoff state, and a +split-brain case when multicast drops briefly and two nodes each believe the +name is vacant. + +None of that is necessary, because mDNS already allows several hosts to answer +one name: a *shared* record is never probed and never conflicts. All nodes +answer owl.local at once, the client picks one, and a node dying is a non-event +because there was never a single holder to fail over from. + +## Why this talks DBus instead of running avahi-publish + +avahi-publish -a cannot produce a shared record, and neither can the +AddAddress call underneath it. avahi_server_add_address() forces the flags, +discarding what the caller asked for (avahi-core/entry.c in 0.8): + + entry = server_add_internal(s, g, interface, protocol, + (flags & ~AVAHI_PUBLISH_NO_REVERSE) + | AVAHI_PUBLISH_UNIQUE | AVAHI_PUBLISH_ALLOW_MULTIPLE, r); + +UNIQUE is always set, so the record is always probed and always collides. That +is exactly the bug the old avahi-alias-retina.service had: every node published +retina.local this way and each collided with the others. avahi-publish does not +even fail cleanly: it tries to pick an alternative name, gets NULL because +there is no alternative for an address record, and aborts on an assertion. +Observed on a live node, 1438 restarts deep: + + Name collision, picking new name '(null)'. + avahi-publish: entrygroup.c:738: avahi_entry_group_add_address: + Assertion `name' failed. + Main process exited, code=killed, status=6/ABRT + +Restart=always turned that into a permanent ten-second flap. + +ALLOW_MULTIPLE is not the escape hatch it sounds like. AddAddress rejects it +outright as an invalid flag, and forces it on internally regardless — and it +would not have helped either way, because it means "allow multiple *local* +records of this type" (for a multi-homed host), not "let other hosts answer +this name too". + +EntryGroup.AddRecord is the one path that works: avahi_server_add() passes +flags through untouched, so omitting AVAHI_PUBLISH_UNIQUE gives a genuinely +shared record. It also takes an explicit TTL, which AddAddress hardcodes. + +## Why it holds its addresses under review + +The record is the node's own IP, so it goes stale when that changes — a DHCP +renewal onto a new address, or a switch between Ethernet and WiFi. The old +alias captured the address once at service start and never looked again, which +is why a node could end up advertising an address it no longer had. + +This re-checks on a short interval and republishes on change, which covers the +DHCP and interface cases together and needs no NetworkManager dispatcher hook +to do it. +""" + +import json +import os +import signal +import socket +import subprocess +import sys +import time + +import dbus + +ALIAS = "owl.local" + +# Deliberately shorter than Avahi's 120s default for host addresses. A node +# powered off at the wall sends no goodbye packet, so every client that cached +# its address for owl.local keeps trying it until the TTL runs out. That window +# is the one real cost of publishing this as a shared record, and it is the +# only knob on it. A clean shutdown withdraws the record immediately and is +# unaffected either way. +TTL_SECONDS = 30 + +# How often to re-read the node's addresses. One `ip` invocation, so the cost +# is nil; short enough that a DHCP change is corrected before anyone notices. +POLL_SECONDS = 15 + +# From avahi-common/defs.h. +PROTO_INET = 0 +DNS_CLASS_IN = 1 +DNS_TYPE_A = 1 +# The whole point: no AVAHI_PUBLISH_UNIQUE (1). Without it the record is +# shared — not probed, and free to coexist with the same name on other hosts. +PUBLISH_FLAGS_SHARED = 0 + + +def is_physical(name): + """Is this a real network interface, rather than a virtual one? + + Docker is the reason this matters. A node running the radar stack has half + a dozen bridges — docker0 and a br- per compose network — each with a + 172.x address that means nothing outside the node. Publishing those under + owl.local hands clients addresses they can never reach, and it is not + hypothetical: before this existed, `avahi-resolve -n owl.local` on a live + node answered 172.18.0.1. + + The test is whether the kernel gives the interface a backing device. Real + NICs (end0, wlan0) have one; bridges, veths and tunnels do not. That beats + matching on names, because the br- ones are generated per network and + there is no pattern worth trusting. + """ + return os.path.exists(f"/sys/class/net/{name}/device") + + +def current_addresses(): + """Every global IPv4 address on a physical interface, as {ifindex: [...]}. + + Published per interface rather than on AVAHI_IF_UNSPEC so a multi-homed + node does not announce its WiFi address to the Ethernet segment, where + nothing can reach it. + + IPv4 only for now. retina-gui listens dual-stack, so adding AAAA later is + possible, but IPv6 on these LANs is mostly link-local and needs scope + handling that buys nothing while every client reaches the node over v4. + """ + try: + output = subprocess.run( + ["ip", "-j", "-4", "addr", "show", "scope", "global"], + capture_output=True, text=True, timeout=10, check=True).stdout + interfaces = json.loads(output) + except (subprocess.SubprocessError, ValueError) as e: + print(f"owl-mdns-alias: could not read addresses: {e}", file=sys.stderr) + return {} + + found = {} + for interface in interfaces: + index = interface.get("ifindex") + name = interface.get("ifname", "") + if not index or not is_physical(name): + continue + addresses = [a["local"] for a in interface.get("addr_info", []) + if a.get("family") == "inet" and a.get("local")] + if addresses: + found[index] = sorted(addresses) + return found + + +class Publisher: + """Holds one Avahi entry group carrying the alias, and keeps it current.""" + + def __init__(self): + self.bus = dbus.SystemBus() + server = dbus.Interface( + self.bus.get_object("org.freedesktop.Avahi", "/"), + "org.freedesktop.Avahi.Server") + self.group = dbus.Interface( + self.bus.get_object("org.freedesktop.Avahi", server.EntryGroupNew()), + "org.freedesktop.Avahi.EntryGroup") + self.published = {} + + def publish(self, addresses): + # Reset rather than update in place: the set of interfaces can change, + # not just the addresses on them, and a reset withdraws whatever was + # there before with goodbye packets rather than leaving an orphan. + self.group.Reset() + + for index, on_interface in addresses.items(): + for address in on_interface: + self.group.AddRecord( + dbus.Int32(index), + dbus.Int32(PROTO_INET), + dbus.UInt32(PUBLISH_FLAGS_SHARED), + ALIAS, + dbus.UInt16(DNS_CLASS_IN), + dbus.UInt16(DNS_TYPE_A), + dbus.UInt32(TTL_SECONDS), + dbus.ByteArray(socket.inet_aton(address))) + + self.group.Commit() + self.published = addresses + flat = ", ".join(a for on_if in addresses.values() for a in on_if) + print(f"owl-mdns-alias: {ALIAS} -> {flat or '(nothing)'}", flush=True) + + def check(self): + """Raise if the entry group is no longer ours to hold. + + The poll below only touches DBus when an address actually changes, so + on a node with a stable IP nothing would otherwise ever notice that + avahi-daemon had gone away and taken the entry group with it. The + service would sit there looking healthy while publishing nothing. + + That is not a hypothetical: avahi-daemon is socket-activated, so it can + be stopped and brought back by something else entirely, without this + unit being restarted by systemd. + """ + state = self.group.GetState() + # 3 = AVAHI_ENTRY_GROUP_COLLISION, 4 = FAILURE. Neither should happen + # for a shared record, but if one does, a clean rebuild beats staying + # up in a state that publishes nothing. + if int(state) in (3, 4): + raise dbus.DBusException(f"entry group in state {state}") + + def withdraw(self): + """Take the alias off the network now, rather than at TTL expiry.""" + try: + self.group.Reset() + except dbus.DBusException: + pass + + +def main(): + try: + publisher = Publisher() + except dbus.DBusException as e: + # avahi-daemon not up yet, or gone. systemd restarts us; there is + # nothing useful to do from in here. + print(f"owl-mdns-alias: cannot reach avahi: {e}", file=sys.stderr) + return 1 + + running = True + + def stop(_signum, _frame): + nonlocal running + running = False + + signal.signal(signal.SIGTERM, stop) + signal.signal(signal.SIGINT, stop) + + try: + publisher.publish(current_addresses()) + while running: + time.sleep(POLL_SECONDS) + if not running: + break + publisher.check() + addresses = current_addresses() + if addresses != publisher.published: + publisher.publish(addresses) + except dbus.DBusException as e: + # Most likely avahi-daemon restarted under us and took the entry group + # with it. Exiting lets systemd rebuild the whole thing cleanly. + print(f"owl-mdns-alias: lost avahi: {e}", file=sys.stderr) + return 1 + finally: + publisher.withdraw() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/playbooks/os_setup/roles/mdns_identity/files/owl-mdns-identity b/plugins/playbooks/os_setup/roles/mdns_identity/files/owl-mdns-identity new file mode 100644 index 0000000..9499c97 --- /dev/null +++ b/plugins/playbooks/os_setup/roles/mdns_identity/files/owl-mdns-identity @@ -0,0 +1,181 @@ +#!/bin/bash + +# ---------------------------------------------------------------------------- +# Give this node an mDNS name of its own, and advertise it for discovery. +# ---------------------------------------------------------------------------- +# +# Every node ships with /etc/hostname set to "owl", so on a LAN with more than +# one of them Avahi's conflict resolution decides who is owl.local and who +# becomes owl-2.local. The winner is whoever booted first, and it reshuffles on +# every reboot, so no node has an address you can write down. +# +# This sets Avahi's advertised host name to the node's Mender node_id instead +# (ret4c844c20.local), which is derived from the Pi's serial and therefore the +# same on every boot for the life of the board. Nodes stop contending for a +# name, so owl-2.local never occurs again. +# +# /etc/hostname is deliberately left alone. It stays "owl" for Docker, the +# shell prompt, log tags and Mender, none of which want a per-device name; only +# what Avahi puts on the wire changes. +# +# Runs on every boot rather than once, so it is self-healing after a manual +# edit and picks up a friendly name set through the GUI. It is ordered before +# avahi-daemon so the name is in place when the daemon first probes. + +set -o nounset +set -o errexit +set -o pipefail + +AVAHI_CONF=/etc/avahi/avahi-daemon.conf +SERVICE_FILE=/etc/avahi/services/owl-node.service +NODE_ID_FILE=/data/mender/node_id +NODE_NAME_FILE=/data/retina-gui/node-name + +log() { echo "owl-mdns-identity: $*"; } + +# ---------------------------------------------------------------- the node_id +# +# Must agree exactly with configuration/mender/identity/mender-device-identity. +# Two formats are live in the fleet and both are valid: ret + 16 hex (current, +# the whole serial) and ret + 8 hex (legacy, the last 8 only). The persisted +# file is checked first and wins, which is what keeps an enrolled legacy node on +# the identity it already has — the same reason that script reads it first. +derive_node_id() { + if [ -f "${NODE_ID_FILE}" ]; then + local persisted + persisted=$(tr -d '[:space:]' < "${NODE_ID_FILE}") + if [ -n "${persisted}" ]; then + echo "${persisted}" + return 0 + fi + fi + + if ! grep -q "Raspberry Pi\|BCM" /proc/cpuinfo 2>/dev/null; then + return 1 + fi + + local serial + serial=$(grep "^Serial" /proc/cpuinfo | cut -d: -f2 | tr -d ' ' | tr '[:upper:]' '[:lower:]') + if [ -z "${serial}" ] || [ "${serial}" = "0000000000000000" ]; then + return 1 + fi + + if printf '%s' "${serial}" | grep -qE '^[0-9a-f]{16}$'; then + echo "ret${serial}" + elif printf '%s' "${serial: -8}" | grep -qE '^[0-9a-f]{8}$'; then + echo "ret${serial: -8}" + else + return 1 + fi +} + +# A name we cannot derive is not a name we should invent. Falling back to the +# stock "owl" leaves the node exactly as it behaves today — findable, with +# Avahi's suffixing to sort out collisions — whereas a made-up identity could +# collide with a real one, and would follow the board around forever. +if NODE_ID=$(derive_node_id); then + log "mDNS host name: ${NODE_ID}" +else + NODE_ID=$(cat /etc/hostname) + log "warning: no node_id available; leaving mDNS host name as ${NODE_ID}" +fi + +# ------------------------------------------------------------ the host name +# +# Avahi has no drop-in directory, so the shipped config file has to be edited +# in place. Idempotent: matches the setting whether it is currently commented +# out (as Debian ships it), already set to something else, or already correct. +if grep -qE '^[#[:space:]]*host-name=' "${AVAHI_CONF}"; then + sed -i -E "s|^[#[:space:]]*host-name=.*|host-name=${NODE_ID}|" "${AVAHI_CONF}" +else + sed -i "/^\[server\]/a host-name=${NODE_ID}" "${AVAHI_CONF}" +fi + +# ------------------------------------------------------- the interfaces to use +# +# A node running the radar stack has half a dozen Docker bridges — docker0 plus +# a br- per compose network — each with a 172.x address that is +# meaningless outside the node. Avahi advertises on every interface by default, +# so those addresses go out under the node's name and clients get handed +# routes to nowhere. Observed on a live node before this was added: +# +# $ avahi-resolve -n owl.local +# owl.local 172.18.0.1 +# +# Restricting Avahi to interfaces with a backing device in /sys leaves the real +# NICs (end0, wlan0) and drops every bridge and veth. Names are not used for +# the test: the br- ones are generated per network, so there is no +# pattern worth trusting. +PHYSICAL="" +for interface in /sys/class/net/*; do + name=$(basename "${interface}") + if [ -e "${interface}/device" ]; then + PHYSICAL="${PHYSICAL}${PHYSICAL:+,}${name}" + fi +done + +# An empty list would tell Avahi to publish on nothing at all, which is a worse +# failure than publishing too much — so if the probe finds nothing, leave the +# setting alone and let Avahi use every interface as before. +if [ -n "${PHYSICAL}" ]; then + log "publishing on: ${PHYSICAL}" + if grep -qE '^[#[:space:]]*allow-interfaces=' "${AVAHI_CONF}"; then + sed -i -E "s|^[#[:space:]]*allow-interfaces=.*|allow-interfaces=${PHYSICAL}|" "${AVAHI_CONF}" + else + sed -i "/^\[server\]/a allow-interfaces=${PHYSICAL}" "${AVAHI_CONF}" + fi +else + log "warning: no physical interfaces found; leaving allow-interfaces unset" +fi + +# ------------------------------------------------------------ the friendly name +# +# Optional, set through the GUI and stored on /data so it survives an OS +# update. ret4c844c20 is stable but unreadable, and a landing page listing +# several of them is not something anyone can navigate. +FRIENDLY_NAME="" +if [ -f "${NODE_NAME_FILE}" ]; then + FRIENDLY_NAME=$(head -c 64 "${NODE_NAME_FILE}" | tr -d '\n\r') +fi + +# The value lands inside an XML attribute-free text node, so the five XML +# entities have to go. The GUI validates on the way in as well; this is the +# second line of defence, and covers a file edited by hand over SSH. +xml_escape() { + printf '%s' "$1" \ + | sed -e 's/&/\&/g' \ + -e 's//\>/g' \ + -e 's/"/\"/g' \ + -e "s/'/\'/g" +} + +# ------------------------------------------------------- the DNS-SD advertisement +# +# This is how peers find each other. A service type rather than a host name +# because DNS-SD is multi-instance by design — every node advertising +# _owl-node._tcp is normal, so there is nothing to collide over. The SRV record +# it generates points at the host *name*, not an address, so it can never go +# stale the way the old retina.local alias did. +# +# %h expands to Avahi's host name, so the instance tracks whatever was set +# above without this file having to repeat it. avahi-daemon watches this +# directory and reloads on its own; no restart is needed when only the friendly +# name changes, which is what lets the GUI rename a node live. +mkdir -p "$(dirname "${SERVICE_FILE}")" +cat > "${SERVICE_FILE}" < + + + + %h + + _owl-node._tcp + 80 + node_id=$(xml_escape "${NODE_ID}") + name=$(xml_escape "${FRIENDLY_NAME}") + + +EOF + +log "advertised _owl-node._tcp as ${NODE_ID}${FRIENDLY_NAME:+ (${FRIENDLY_NAME})}" diff --git a/plugins/playbooks/os_setup/roles/mdns_identity/tasks/main.yml b/plugins/playbooks/os_setup/roles/mdns_identity/tasks/main.yml new file mode 100644 index 0000000..41f3a25 --- /dev/null +++ b/plugins/playbooks/os_setup/roles/mdns_identity/tasks/main.yml @@ -0,0 +1,109 @@ +--- +# Playbook: mdns_identity +# Version: 0.1.0 +# +# Purpose: Make every node individually addressable on a shared LAN, and make +# owl.local a fleet entry point that any live node can answer. +# +# The problem: all nodes ship with /etc/hostname set to "owl", so Avahi decides +# by boot race who gets owl.local and who gets owl-2.local, and the answer +# changes on every reboot. Nothing has a stable address, and when the node +# holding owl.local goes away the name dies with it — Avahi never reclaims a +# name it lost. +# +# Two names, published two different ways: +# +# ret4c844c20.local unique, probed, Avahi's own host-name. One node only. +# Derived from the Mender node_id, so it is the same for +# the life of the board. Nodes no longer contend for a +# name, so owl-2.local stops occurring at all. +# +# owl.local shared, unprobed. Published by every node, always. All +# of them answer, the client picks one, and a node dying +# is a non-event because there was never a single holder. +# +# Nothing here depends on how many nodes exist: every node does exactly the +# same thing on every boot. The only place the node count is consulted is +# retina-gui, deciding whether to serve the landing page or redirect to the one +# node it can see. +# +# Discovery is DNS-SD (_owl-node._tcp), which is multi-instance by design and +# so has no uniqueness problem to work around. +# +# Replaces avahi-alias-retina.service, removed from the radar_packages role. +# That published retina.local via avahi-publish -a, which cannot make a shared +# record — see the long comment in owl-mdns-alias for why, and why this talks +# to Avahi over DBus instead. + +- name: Install owl-mdns-identity + copy: + src: owl-mdns-identity + dest: /usr/local/sbin/owl-mdns-identity + mode: '0755' + +- name: Install owl-mdns-alias + copy: + src: owl-mdns-alias + dest: /usr/local/sbin/owl-mdns-alias + mode: '0755' + +- name: Create owl-mdns-identity systemd service + copy: + dest: /etc/systemd/system/owl-mdns-identity.service + mode: '0644' + content: | + [Unit] + Description=Set this node's mDNS host name and advertise it for discovery + # The host name has to be in avahi-daemon.conf before the daemon reads it, + # and the daemon only reads it at startup. + Before=avahi-daemon.service + # The node_id is persisted on /data, which is its own partition. + RequiresMountsFor=/data + + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=/usr/local/sbin/owl-mdns-identity + + [Install] + WantedBy=multi-user.target + +- name: Enable owl-mdns-identity for boot + file: + src: /etc/systemd/system/owl-mdns-identity.service + dest: /etc/systemd/system/multi-user.target.wants/owl-mdns-identity.service + state: link + +- name: Create owl-mdns-alias systemd service + copy: + dest: /etc/systemd/system/owl-mdns-alias.service + mode: '0644' + content: | + [Unit] + Description=Publish owl.local as a shared mDNS alias for this node + Wants=avahi-daemon.service + After=avahi-daemon.service + + [Service] + Type=simple + ExecStart=/usr/local/sbin/owl-mdns-alias + # Deliberately not bound to avahi-daemon's lifecycle. PartOf= was tried + # and is wrong: it stops this unit when avahi stops but cannot start it + # again, and avahi-daemon is socket-activated, so it comes back on its + # own without systemd restarting anything that depends on it. A node was + # left advertising its own name but not owl.local that way. + # + # Restart=always plus the script's own bus check covers every case + # instead. While avahi is down the script exits and is retried every 5s; + # once it is back the alias republishes on the next attempt. + Restart=always + RestartSec=5 + + [Install] + WantedBy=multi-user.target + +- name: Enable owl-mdns-alias for boot + file: + src: /etc/systemd/system/owl-mdns-alias.service + dest: /etc/systemd/system/multi-user.target.wants/owl-mdns-alias.service + state: link diff --git a/plugins/playbooks/os_setup/roles/radar_packages/tasks/main.yml b/plugins/playbooks/os_setup/roles/radar_packages/tasks/main.yml index 4da22d9..cc99850 100644 --- a/plugins/playbooks/os_setup/roles/radar_packages/tasks/main.yml +++ b/plugins/playbooks/os_setup/roles/radar_packages/tasks/main.yml @@ -116,12 +116,23 @@ state: link # 2b. Install Avahi for mDNS .local hostname resolution +# +# The names themselves are set up by the mdns_identity role, which runs later: +# ret.local per node, plus owl.local shared across all of them. This +# task only puts the daemon and its dependencies in place. +# +# retina.local used to be published here by an avahi-alias-retina.service. It +# has been retired — it could never work on a LAN with more than one node +# (avahi-publish -a cannot make a shared record, so every node collided with +# every other and flapped on a ten-second restart loop), and owl.local now +# covers what it was for. - name: Install Avahi mDNS daemon apt: name: - avahi-daemon # mDNS service for .local hostname advertisement - - avahi-utils # Provides avahi-publish for mDNS alias publishing + - avahi-utils # avahi-browse, used by retina-gui to discover peers - libnss-mdns # Name service switch module for mDNS resolution + - python3-dbus # owl-mdns-alias publishes the shared alias over DBus state: present update_cache: yes @@ -131,31 +142,6 @@ dest: /etc/systemd/system/multi-user.target.wants/avahi-daemon.service state: link -# 2b. Install retina.local mDNS alias (backwards compatibility for existing nodes) -- name: Install retina.local avahi alias service - copy: - dest: /etc/systemd/system/avahi-alias-retina.service - content: | - [Unit] - Description=Publish retina.local as mDNS alias for this device - Requires=avahi-daemon.service - After=avahi-daemon.service network-online.target - - [Service] - Type=simple - ExecStart=/bin/bash -c "/usr/bin/avahi-publish -a -R retina.local $(ip route get 1 | awk '{print $7;exit}')" - Restart=always - RestartSec=10 - - [Install] - WantedBy=multi-user.target - -- name: Enable retina.local alias service on boot - file: - src: /etc/systemd/system/avahi-alias-retina.service - dest: /etc/systemd/system/multi-user.target.wants/avahi-alias-retina.service - state: link - # 3. Install SDRplay API # 3a) Create directory - name: Create SDRplay lib directory