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 @@
+
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
+
+
+