diff --git a/net/avahi-reflector/Makefile b/net/avahi-reflector/Makefile new file mode 100644 index 000000000..d302d54c1 --- /dev/null +++ b/net/avahi-reflector/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= avahi-reflector +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= Avahi mDNS/DNS-SD reflector for cross-VLAN service discovery +PLUGIN_MAINTAINER= bflood@users.noreply.github.com +PLUGIN_DEPENDS= avahi-app + +.include "../../Mk/plugins.mk" diff --git a/net/avahi-reflector/pkg-descr b/net/avahi-reflector/pkg-descr new file mode 100644 index 000000000..98211b010 --- /dev/null +++ b/net/avahi-reflector/pkg-descr @@ -0,0 +1,16 @@ +Avahi mDNS/DNS-SD Reflector + +Runs avahi-daemon in reflector mode, proxying multicast DNS and +DNS Service Discovery traffic across VLANs. Unlike mdns-repeater, +Avahi handles full mDNS A/AAAA record queries and DNS-SD browsing, +enabling reliable hostname resolution and Bonjour service discovery +across network segments. + +WWW: https://avahi.org/ + +Plugin Changelog +================ + +1.0 + +* Initial release diff --git a/net/avahi-reflector/src/etc/inc/plugins.inc.d/avahireflector.inc b/net/avahi-reflector/src/etc/inc/plugins.inc.d/avahireflector.inc new file mode 100644 index 000000000..364e971e0 --- /dev/null +++ b/net/avahi-reflector/src/etc/inc/plugins.inc.d/avahireflector.inc @@ -0,0 +1,59 @@ +enabled === '1') { + $services[] = [ + 'description' => gettext('Avahi mDNS/DNS-SD Reflector'), + 'configd' => [ + 'start' => ['avahireflector start'], + 'stop' => ['avahireflector stop'], + 'restart' => ['avahireflector restart'], + ], + 'name' => 'avahi-daemon', + 'pidfile' => '/var/run/avahi-daemon/pid', + ]; + } + + return $services; +} + +function avahireflector_xmlrpc_sync() +{ + $result = []; + $result[] = [ + 'description' => gettext('Avahi mDNS/DNS-SD Reflector'), + 'section' => 'OPNsense.AvahiReflector', + 'id' => 'avahireflector', + ]; + return $result; +} diff --git a/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/Api/ServiceController.php b/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/Api/ServiceController.php new file mode 100644 index 000000000..fa0a8ffda --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/Api/ServiceController.php @@ -0,0 +1,51 @@ +configdRun('avahireflector diagnostics'); + $data = json_decode($response, true); + if ($data === null) { + return ['status' => 'error', 'message' => 'Failed to parse diagnostics output']; + } + return $data; + } +} diff --git a/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/Api/SettingsController.php b/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/Api/SettingsController.php new file mode 100644 index 000000000..f89165403 --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/Api/SettingsController.php @@ -0,0 +1,37 @@ +view->generalForm = $this->getForm('general'); + $this->view->pick('OPNsense/AvahiReflector/index'); + } +} diff --git a/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/forms/general.xml b/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/forms/general.xml new file mode 100644 index 000000000..9389c9347 --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/controllers/OPNsense/AvahiReflector/forms/general.xml @@ -0,0 +1,65 @@ +
+ + header + + + + avahireflector.enabled + + checkbox + + + + avahireflector.interfaces + + select_multiple +
Firewall requirement: Each selected interface needs a pass rule for UDP port 5353 destined to the mDNS multicast addresses: 224.0.0.251 (IPv4) and ff02::fb (IPv6). An IGMP pass rule is also recommended so the firewall can join the IPv4 multicast group. Without these rules, Avahi will not receive mDNS queries or announcements on that interface.

Service traffic: mDNS only handles discovery — clients still need unicast access to the actual service ports across VLANs. For example: AirPlay (TCP 7000, 7100), AirPrint/IPP (TCP 631), HomeKit (TCP 51827). Ensure your inter-VLAN rules allow the relevant service ports between client and device subnets.]]>
+
+ + header + + + + avahireflector.domain_name + + text + + + + avahireflector.use_ipv4 + + checkbox + Enable IPv4 mDNS traffic. + + + avahireflector.use_ipv6 + + checkbox + Enable IPv6 mDNS traffic. + + + header + + + + avahireflector.enable_reflector + + checkbox + + + + avahireflector.reflect_ipv + + checkbox + + + + avahireflector.reflect_filters + + select_multiple + + true + true +
The .local suffix is not required — Avahi uses substring matching, so _ipp._tcp will match _ipp._tcp.local. Omitting the suffix is recommended to stay within the 239-character limit imposed by the avahi-daemon config parser.

Examples: _airplay._tcp, _raop._tcp, _ipp._tcp, _hap._tcp, _matter._tcp]]>
+
+
diff --git a/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/ACL/ACL.xml b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/ACL/ACL.xml new file mode 100644 index 000000000..81ac65492 --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + Services: Avahi mDNS Reflector + + ui/avahireflector/* + api/avahireflector/* + + + diff --git a/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/AvahiReflector.php b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/AvahiReflector.php new file mode 100644 index 000000000..e413d0c6f --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/AvahiReflector.php @@ -0,0 +1,70 @@ +\n + * + * "reflect-filters=" is 16 characters, leaving 239 for the value itself + * (256 - 16 - 1 for the trailing newline). + */ + private const REFLECT_FILTERS_MAX_LENGTH = 239; + + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + + $filters = (string)$this->reflect_filters; + if (strlen($filters) > self::REFLECT_FILTERS_MAX_LENGTH) { + $messages->appendMessage(new Message( + sprintf( + 'Reflect filters exceed the %d-character limit imposed by the ' + . 'avahi-daemon config parser (currently %d). Remove entries or ' + . 'drop the ".local" suffix — Avahi uses substring matching so ' + . 'the suffix is not required.', + self::REFLECT_FILTERS_MAX_LENGTH, + strlen($filters) + ), + $this->reflect_filters->getInternalXMLTagName() + )); + } + + return $messages; + } +} diff --git a/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/AvahiReflector.xml b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/AvahiReflector.xml new file mode 100644 index 000000000..bb8891022 --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/AvahiReflector.xml @@ -0,0 +1,44 @@ + + //OPNsense/AvahiReflector + 1.0.0 + Avahi mDNS/DNS-SD reflector settings + + + 0 + Y + + + lan + Y + Y + + + local + Y + /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/ + Enter a valid domain name (e.g. "local" or "home.local"). + + + 1 + Y + + + 0 + Y + + + 1 + Y + + + 0 + Y + + + N + /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/ + Y + Each entry must be a valid DNS-SD service type (e.g. "_ipp._tcp"). Total length must not exceed 239 characters due to the avahi-daemon config parser line-length limit of 256 bytes. + + + diff --git a/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/Menu/Menu.xml b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/Menu/Menu.xml new file mode 100644 index 000000000..c0131643b --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/models/OPNsense/AvahiReflector/Menu/Menu.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/net/avahi-reflector/src/opnsense/mvc/app/views/OPNsense/AvahiReflector/index.volt b/net/avahi-reflector/src/opnsense/mvc/app/views/OPNsense/AvahiReflector/index.volt new file mode 100644 index 000000000..8650076e3 --- /dev/null +++ b/net/avahi-reflector/src/opnsense/mvc/app/views/OPNsense/AvahiReflector/index.volt @@ -0,0 +1,69 @@ +{# + # Copyright (C) 2024 OPNsense Community + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + +
+ {{ partial("layout_partials/base_form", ['fields': generalForm, 'id': 'frm_GeneralSettings']) }} +
+ +
+
diff --git a/net/avahi-reflector/src/opnsense/scripts/OPNsense/AvahiReflector/avahi_slot_check.sh b/net/avahi-reflector/src/opnsense/scripts/OPNsense/AvahiReflector/avahi_slot_check.sh new file mode 100644 index 000000000..58961a163 --- /dev/null +++ b/net/avahi-reflector/src/opnsense/scripts/OPNsense/AvahiReflector/avahi_slot_check.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# Monit custom check: detect avahi-daemon slot exhaustion +# Exit 0 = OK, Exit 1 = slot errors detected (Monit restarts avahi-daemon) +# +# The avahi-daemon reflector has a hardcoded 100-slot pool for legacy unicast +# reflection. Bursts of mDNS traffic can exhaust all slots, causing reflected +# services to appear offline for hours until manually restarted. + +LOG_FILE="/var/log/system/latest.log" +STATE_FILE="/var/run/avahi_slot_check.state" +PATTERN="No slot available for legacy unicast reflection" + +if [ ! -f "$LOG_FILE" ]; then + echo "OK - syslog not found" + exit 0 +fi + +CURRENT_INODE=$(stat -f %i "$LOG_FILE" 2>/dev/null) +FILE_SIZE=$(stat -f %z "$LOG_FILE" 2>/dev/null) +if [ -z "$FILE_SIZE" ] || [ -z "$CURRENT_INODE" ]; then + echo "OK - cannot stat syslog" + exit 0 +fi + +LAST_OFFSET=0 +LAST_INODE=0 +if [ -f "$STATE_FILE" ]; then + LAST_OFFSET=$(sed -n '1p' "$STATE_FILE") + LAST_INODE=$(sed -n '2p' "$STATE_FILE") + # Handle log rotation: reset if inode changed or file shrank + if [ "$CURRENT_INODE" != "$LAST_INODE" ] || [ "$FILE_SIZE" -lt "$LAST_OFFSET" ]; then + LAST_OFFSET=0 + fi +fi + +# Save current position for next run +printf '%s\n%s\n' "$FILE_SIZE" "$CURRENT_INODE" > "$STATE_FILE" + +BYTES_TO_READ=$((FILE_SIZE - LAST_OFFSET)) +if [ "$BYTES_TO_READ" -le 0 ]; then + echo "OK - No new data" + exit 0 +fi + +NEW_DATA=$(tail -c "$BYTES_TO_READ" "$LOG_FILE") +SLOT_COUNT=$(echo "$NEW_DATA" | grep -c "$PATTERN") + +if [ "$SLOT_COUNT" -gt 0 ]; then + LAST_LINE=$(echo "$NEW_DATA" | grep "$PATTERN" | tail -1) + echo "CRITICAL - $SLOT_COUNT slot exhaustion events since last check" + echo " Last: $LAST_LINE" + exit 1 +else + echo "OK - No slot errors" + exit 0 +fi diff --git a/net/avahi-reflector/src/opnsense/scripts/OPNsense/AvahiReflector/status.py b/net/avahi-reflector/src/opnsense/scripts/OPNsense/AvahiReflector/status.py new file mode 100755 index 000000000..b2daec6db --- /dev/null +++ b/net/avahi-reflector/src/opnsense/scripts/OPNsense/AvahiReflector/status.py @@ -0,0 +1,183 @@ +#!/usr/local/bin/python3 + +""" +Avahi mDNS/DNS-SD Reflector — diagnostics script. +Returns JSON status for the OPNsense dashboard widget and API. +""" + +import json +import os +import re +import subprocess +from datetime import datetime + +PID_FILE = '/var/run/avahi-daemon/pid' +CONF_FILE = '/usr/local/etc/avahi/avahi-daemon.conf' +SYSLOG_FILE = '/var/log/system/latest.log' +SLOT_ERROR_PATTERN = 'No slot available for legacy unicast reflection' + + +def _read_pid(): + try: + with open(PID_FILE, 'r') as fh: + return int(fh.read().strip()) + except (FileNotFoundError, ValueError): + return None + + +def _process_running(pid): + try: + os.kill(pid, 0) + return True + except (OSError, TypeError): + return False + + +def _process_uptime(pid): + try: + result = subprocess.run( + ['ps', '-o', 'etime=', '-p', str(pid)], + capture_output=True, text=True, timeout=5 + ) + return result.stdout.strip() if result.returncode == 0 else None + except Exception: + return None + + +def _process_memory_mb(pid): + try: + result = subprocess.run( + ['ps', '-o', 'rss=', '-p', str(pid)], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + rss_kb = int(result.stdout.strip()) + return round(rss_kb / 1024) + except Exception: + pass + return None + + +def _parse_conf(): + conf = { + 'domain': 'local', + 'interfaces': '', + 'reflector_enabled': False, + 'use_ipv4': True, + 'use_ipv6': False, + 'reflect_ipv': False, + 'reflect_filters': '', + } + try: + with open(CONF_FILE, 'r') as fh: + for line in fh: + line = line.strip() + if line.startswith('#') or '=' not in line: + continue + key, _, val = line.partition('=') + key = key.strip() + val = val.strip() + if key == 'domain-name': + conf['domain'] = val + elif key == 'allow-interfaces': + conf['interfaces'] = val + elif key == 'enable-reflector': + conf['reflector_enabled'] = val == 'yes' + elif key == 'use-ipv4': + conf['use_ipv4'] = val == 'yes' + elif key == 'use-ipv6': + conf['use_ipv6'] = val == 'yes' + elif key == 'reflect-ipv': + conf['reflect_ipv'] = val == 'yes' + elif key == 'reflect-filters': + conf['reflect_filters'] = val + except FileNotFoundError: + pass + return conf + + +def _mdns_repeater_running(): + try: + result = subprocess.run( + ['pgrep', '-x', 'mdns-repeater'], + capture_output=True, timeout=5 + ) + return result.returncode == 0 + except Exception: + return False + + +def _process_start_time(pid): + try: + result = subprocess.run( + ['ps', '-o', 'lstart=', '-p', str(pid)], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + raw = result.stdout.strip() + dt = datetime.strptime(raw, '%a %b %d %H:%M:%S %Y') + return dt.strftime('%Y-%m-%d %H:%M:%S') + except Exception: + pass + return None + + +def _slot_error_summary(): + summary = { + 'status': 'healthy', + 'slot_errors_today': 0, + 'last_slot_error': None, + } + try: + now = datetime.now() + # Syslog uses space-padded day: "Feb 4" or "Feb 14" + today_prefix = now.strftime('%b ') + '{:>2d}'.format(now.day) + with open(SYSLOG_FILE, 'r') as fh: + for line in fh: + if SLOT_ERROR_PATTERN not in line: + continue + # Extract timestamp from syslog line (e.g. "Feb 14 10:30:45") + match = re.match(r'^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})', line) + if match: + timestamp = match.group(1) + if line.startswith(today_prefix): + summary['slot_errors_today'] += 1 + summary['last_slot_error'] = timestamp + except FileNotFoundError: + pass + if summary['slot_errors_today'] > 0: + summary['status'] = 'degraded' + elif summary['last_slot_error'] is not None: + summary['status'] = 'warning' + return summary + + +def main(): + pid = _read_pid() + running = pid is not None and _process_running(pid) + conf = _parse_conf() + + status = { + 'running': running, + 'pid': pid if running else None, + 'uptime': _process_uptime(pid) if running else None, + 'memory_mb': _process_memory_mb(pid) if running else None, + 'domain': conf['domain'], + 'interfaces': conf['interfaces'], + 'reflector_enabled': conf['reflector_enabled'], + 'use_ipv4': conf['use_ipv4'], + 'use_ipv6': conf['use_ipv6'], + 'reflect_ipv': conf['reflect_ipv'], + 'reflect_filters': conf['reflect_filters'], + 'mdns_repeater_running': _mdns_repeater_running(), + } + + health = _slot_error_summary() + health['last_restart'] = _process_start_time(pid) if running else None + status['health'] = health + + print(json.dumps(status)) + + +if __name__ == '__main__': + main() diff --git a/net/avahi-reflector/src/opnsense/service/conf/actions.d/actions_avahireflector.conf b/net/avahi-reflector/src/opnsense/service/conf/actions.d/actions_avahireflector.conf new file mode 100644 index 000000000..6d270dada --- /dev/null +++ b/net/avahi-reflector/src/opnsense/service/conf/actions.d/actions_avahireflector.conf @@ -0,0 +1,35 @@ +[start] +command:/bin/sh -c "service mdns-repeater onestop 2>/dev/null; service dbus onestart 2>/dev/null; service avahi-daemon onestart 2>&1" +parameters: +type:script +message:starting avahi-daemon + +[stop] +command:/bin/sh -c "service avahi-daemon onestop 2>&1" +parameters: +type:script +message:stopping avahi-daemon + +[restart] +command:/bin/sh -c "service mdns-repeater onestop 2>/dev/null; service dbus onestart 2>/dev/null; service avahi-daemon onerestart 2>&1" +parameters: +type:script +message:restarting avahi-daemon + +[status] +command:/bin/sh -c "service avahi-daemon status 2>&1; true" +parameters: +type:script_output +message:requesting avahi-daemon status + +[diagnostics] +command:/usr/local/opnsense/scripts/OPNsense/AvahiReflector/status.py +parameters: +type:script_output +message:requesting avahi-daemon diagnostics + +[healthcheck] +command:/usr/local/opnsense/scripts/OPNsense/AvahiReflector/avahi_slot_check.sh +parameters: +type:script_output +message:checking avahi-daemon slot health diff --git a/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/+TARGETS b/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/+TARGETS new file mode 100644 index 000000000..9d4006cdd --- /dev/null +++ b/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/+TARGETS @@ -0,0 +1,2 @@ +avahidaemon.conf:/usr/local/etc/avahi/avahi-daemon.conf +rc.conf.d:/etc/rc.conf.d/avahi_daemon diff --git a/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/avahidaemon.conf b/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/avahidaemon.conf new file mode 100644 index 000000000..12598753d --- /dev/null +++ b/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/avahidaemon.conf @@ -0,0 +1,35 @@ +{% from 'OPNsense/Macros/interface.macro' import physical_interface %} +{% if OPNsense.AvahiReflector.enabled|default("0") == "1" %} +{% set intf_list = OPNsense.AvahiReflector.interfaces|default("lan") %} +{% set phys_names = [] %} +{% for intf in intf_list.split(",") %} +{% set phys = physical_interface(intf) %} +{% if phys %} +{% do phys_names.append(phys) %} +{% endif %} +{% endfor %} +[server] +domain-name={{ OPNsense.AvahiReflector.domain_name|default("local") }} +use-ipv4={{ "yes" if OPNsense.AvahiReflector.use_ipv4|default("1") == "1" else "no" }} +use-ipv6={{ "yes" if OPNsense.AvahiReflector.use_ipv6|default("0") == "1" else "no" }} +{% if phys_names %} +allow-interfaces={{ phys_names|join(",") }} +{% endif %} +check-response-ttl=no +allow-point-to-point=no + +[reflector] +enable-reflector={{ "yes" if OPNsense.AvahiReflector.enable_reflector|default("1") == "1" else "no" }} +reflect-ipv={{ "yes" if OPNsense.AvahiReflector.reflect_ipv|default("0") == "1" else "no" }} +{% if OPNsense.AvahiReflector.reflect_filters|default("") != "" %} +reflect-filters={{ OPNsense.AvahiReflector.reflect_filters }} +{% endif %} + +[rlimits] +rlimit-core=0 +rlimit-data=4194304 +rlimit-fsize=0 +rlimit-nofile=768 +rlimit-stack=4194304 +rlimit-nproc=3 +{% endif %} diff --git a/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/rc.conf.d b/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/rc.conf.d new file mode 100644 index 000000000..82725066e --- /dev/null +++ b/net/avahi-reflector/src/opnsense/service/templates/OPNsense/AvahiReflector/rc.conf.d @@ -0,0 +1,5 @@ +{% if OPNsense.AvahiReflector.enabled|default("0") == "1" %} +avahi_daemon_enable="YES" +{% else %} +avahi_daemon_enable="NO" +{% endif %} diff --git a/net/avahi-reflector/src/opnsense/www/js/widgets/AvahiReflector.js b/net/avahi-reflector/src/opnsense/www/js/widgets/AvahiReflector.js new file mode 100644 index 000000000..384fb8a5d --- /dev/null +++ b/net/avahi-reflector/src/opnsense/www/js/widgets/AvahiReflector.js @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 OPNsense Community + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +export default class AvahiReflector extends BaseTableWidget { + constructor() { + super(); + this.tickTimeout = 30; + } + + getGridOptions() { + return { + sizeToContent: 650 + }; + } + + getMarkup() { + let $container = $('
'); + let $table = this.createTable('avahiReflectorTable', { + headerPosition: 'left' + }); + $container.append($table); + return $container; + } + + async onWidgetTick() { + const response = await this.ajaxCall('/api/avahireflector/service/diagnostics'); + + if (!response || response.status === 'error') { + this.displayError(this.translations['unconfigured']); + return; + } + + const rows = []; + + // Status + Health on one line with colored circle indicator + let statusColor = 'text-success'; + let statusText = `${this.translations['running']} / ${this.translations['healthy']}`; + if (!response.running) { + statusColor = 'text-danger'; + statusText = this.translations['stopped']; + } else if (response.health && response.health.status === 'degraded') { + statusColor = 'text-danger'; + statusText = `${this.translations['running']} / ${this.translations['degraded']}`; + } else if (response.health && response.health.status === 'warning') { + statusColor = 'text-warning'; + statusText = `${this.translations['running']} / ${this.translations['warning']}`; + } + rows.push([ + `
${this.translations['status']}
`, + `
${statusText}
` + ]); + + if (response.health) { + const h = response.health; + if (h.slot_errors_today > 0) { + rows.push([this.translations['slot_errors_today'], h.slot_errors_today]); + } + if (h.last_slot_error) { + rows.push([this.translations['last_slot_error'], h.last_slot_error]); + } + if (h.last_restart) { + rows.push([this.translations['last_restart'], h.last_restart]); + } + } + + rows.push([this.translations['domain'], response.domain || '-']); + + if (response.interfaces) { + rows.push([this.translations['interfaces'], response.interfaces]); + } + + rows.push([this.translations['reflector'], response.reflector_enabled + ? this.translations['enabled'] + : this.translations['disabled']]); + + if (response.reflect_filters) { + rows.push([this.translations['reflect_filters'], response.reflect_filters]); + } + + if (response.mdns_repeater_running) { + rows.push([ + `${this.translations['conflict']}`, + this.translations['conflict_detail'] + ]); + } + + super.updateTable('avahiReflectorTable', rows); + } + + displayError(message) { + super.updateTable('avahiReflectorTable', [[message, '']]); + } +} diff --git a/net/avahi-reflector/src/opnsense/www/js/widgets/Metadata/AvahiReflector.xml b/net/avahi-reflector/src/opnsense/www/js/widgets/Metadata/AvahiReflector.xml new file mode 100644 index 000000000..d629eea67 --- /dev/null +++ b/net/avahi-reflector/src/opnsense/www/js/widgets/Metadata/AvahiReflector.xml @@ -0,0 +1,33 @@ + + + AvahiReflector.js + + /api/avahireflector/service/diagnostics + + + Avahi mDNS Reflector + Status + Running + Stopped + PID + Uptime + Memory + Domain + Interfaces + Reflector + Enabled + Disabled + Conflict + mdns-repeater is also running - disable it to avoid interference. + Reflect Filters + Health + Healthy + Degraded + Warning + Slot Errors Today + Last Slot Error + Last Restart + Service not configured + + +