BE: AVAHISCAN -> zeroconf

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2025-10-10 13:48:39 +11:00
parent 81ac72bbd6
commit 0093441457
7 changed files with 106 additions and 186 deletions

View File

@@ -15,7 +15,7 @@ RUN apk add --no-cache bash shadow python3 python3-dev gcc musl-dev libffi-dev o
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag zeroconf git+https://github.com/foreign-sub/aiofreepybox.git
# Append Iliadbox certificate to aiofreepybox
@@ -40,7 +40,7 @@ ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
RUN apk update --no-cache \
&& apk add --no-cache bash libbsd zip lsblk gettext-envsubst sudo mtr tzdata s6-overlay \
&& apk add --no-cache curl arp-scan iproute2 iproute2-ss nmap nmap-scripts traceroute nbtscan avahi avahi-tools openrc dbus net-tools net-snmp-tools bind-tools awake ca-certificates \
&& apk add --no-cache curl arp-scan iproute2 iproute2-ss nmap nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
&& apk add --no-cache sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session \
&& apk add --no-cache python3 nginx \
&& ln -s /usr/bin/awake /usr/bin/wakeonlan \

View File

@@ -13,7 +13,7 @@ ENV PATH="/opt/venv/bin:$PATH"
COPY . ${INSTALL_DIR}/
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag zeroconf git+https://github.com/foreign-sub/aiofreepybox.git \
&& bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \
&& bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \
&& bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;"
@@ -42,7 +42,7 @@ ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
RUN apk update --no-cache \
&& apk add --no-cache bash libbsd zip lsblk gettext-envsubst sudo mtr tzdata s6-overlay \
&& apk add --no-cache curl arp-scan iproute2 iproute2-ss nmap nmap-scripts traceroute nbtscan avahi avahi-tools openrc dbus net-tools net-snmp-tools bind-tools awake ca-certificates \
&& apk add --no-cache curl arp-scan iproute2 iproute2-ss nmap nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
&& apk add --no-cache sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session \
&& apk add --no-cache python3 nginx \
&& ln -s /usr/bin/awake /usr/bin/wakeonlan \

View File

@@ -33,7 +33,7 @@ COPY --chmod=775 --chown=${USER_ID}:${USER_GID} . ${INSTALL_DIR}/
RUN apt-get install -y \
tini snmp ca-certificates curl libwww-perl arp-scan perl apt-utils cron sudo \
nginx-light php php-cgi php-fpm php-sqlite3 php-curl sqlite3 dnsutils net-tools php-openssl \
python3 python3-dev iproute2 nmap python3-pip zip systemctl usbutils traceroute nbtscan avahi avahi-tools openrc dbus
python3 python3-dev iproute2 nmap python3-pip zip systemctl usbutils traceroute nbtscan
# Alternate dependencies
RUN apt-get install nginx nginx-core mtr php-fpm php8.2-fpm php-cli php8.2 php8.2-sqlite3 -y
@@ -43,7 +43,7 @@ RUN phpenmod -v 8.2 sqlite3
RUN apt-get install -y python3-venv
RUN python3 -m venv myenv
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag zeroconf "
# Create a buildtimestamp.txt to later check if a new version was released
RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt

View File

@@ -1,215 +1,133 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import os
import pathlib
import sys
import json
import sqlite3
import subprocess
import socket
import ipaddress
from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo, InterfaceChoice, IPVersion
from zeroconf.asyncio import AsyncZeroconf
# Define the installation path and extend the system path for plugin imports
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from plugin_utils import get_plugins_configs
from plugin_helper import Plugin_Objects
from logger import mylog, Logger
from const import pluginsPath, fullDbPath, logPath
from helper import timeNowTZ, get_setting_value
from messaging.in_app import write_notification
from const import logPath
from helper import get_setting_value
from database import DB
from models.device_instance import DeviceInstance
import conf
from pytz import timezone
# Make sure the TIMEZONE for logging is correct
conf.tz = timezone(get_setting_value('TIMEZONE'))
# Configure timezone and logging
conf.tz = timezone(get_setting_value("TIMEZONE"))
Logger(get_setting_value("LOG_LEVEL"))
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
pluginName = "AVAHISCAN"
pluginName = 'AVAHISCAN'
# Define log paths
LOG_PATH = os.path.join(logPath, "plugins")
LOG_FILE = os.path.join(LOG_PATH, f"script.{pluginName}.log")
RESULT_FILE = os.path.join(LOG_PATH, f"last_result.{pluginName}.log")
# Define the current path and log file paths
LOG_PATH = logPath + '/plugins'
LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
# Initialize the Plugin obj output file
# Initialize plugin results
plugin_objects = Plugin_Objects(RESULT_FILE)
# =============================================================================
# Helper functions
# =============================================================================
def resolve_mdns_name(ip: str, timeout: int = 5) -> str:
"""
Attempts to resolve a hostname via multicast DNS using the Zeroconf library.
Args:
ip (str): The IP address to resolve.
timeout (int): Timeout in seconds for mDNS resolution.
Returns:
str: Resolved hostname (or empty string if not found).
"""
mylog("debug", [f"[{pluginName}] Resolving mDNS for {ip}"])
# Convert string IP to an address object
try:
addr = ipaddress.ip_address(ip)
except ValueError:
mylog("none", [f"[{pluginName}] Invalid IP: {ip}"])
return ""
# Reverse lookup name, e.g. "121.1.168.192.in-addr.arpa"
if addr.version == 4:
rev_name = ipaddress.ip_address(ip).reverse_pointer
else:
rev_name = ipaddress.ip_address(ip).reverse_pointer
try:
zeroconf = Zeroconf()
hostname = socket.getnameinfo((ip, 0), socket.NI_NAMEREQD)[0]
zeroconf.close()
if hostname and hostname != ip:
mylog("debug", [f"[{pluginName}] Found mDNS name: {hostname}"])
return hostname
except Exception as e:
mylog("debug", [f"[{pluginName}] Zeroconf lookup failed for {ip}: {e}"])
finally:
try:
zeroconf.close()
except Exception:
pass
return ""
# =============================================================================
# Main logic
# =============================================================================
def main():
mylog('verbose', [f'[{pluginName}] In script'])
mylog("verbose", [f"[{pluginName}] Script started"])
# timeout = get_setting_value('AVAHI_RUN_TIMEOUT')
timeout = 20
# Create a database connection
db = DB() # instance of class DB
timeout = 10
db = DB()
db.open()
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
# Create a DeviceInstance instance
device_handler = DeviceInstance(db)
devices = (
device_handler.getAll()
if get_setting_value("REFRESH_FQDN")
else device_handler.getUnknown()
)
# Retrieve devices
if get_setting_value("REFRESH_FQDN"):
devices = device_handler.getAll()
else:
devices = device_handler.getUnknown()
mylog('verbose', [f'[{pluginName}] Devices count: {len(devices)}'])
# Mock list of devices (replace with actual device_handler.getUnknown() in production)
# devices = [
# {'devMac': '00:11:22:33:44:55', 'devLastIP': '192.168.1.121'},
# {'devMac': '00:11:22:33:44:56', 'devLastIP': '192.168.1.9'},
# {'devMac': '00:11:22:33:44:57', 'devLastIP': '192.168.1.82'},
# ]
if len(devices) > 0:
# ensure service is running
ensure_avahi_running()
mylog("verbose", [f"[{pluginName}] Devices count: {len(devices)}"])
for device in devices:
domain_name = execute_name_lookup(device['devLastIP'], timeout)
ip = device["devLastIP"]
mac = device["devMac"]
# check if found and not a timeout ('to')
if domain_name != '' and domain_name != 'to':
hostname = resolve_mdns_name(ip, timeout)
if hostname:
plugin_objects.add_object(
# "MAC", "IP", "Server", "Name"
primaryId = device['devMac'],
secondaryId = device['devLastIP'],
watched1 = '', # You can add any relevant info here if needed
watched2 = domain_name,
watched3 = '',
watched4 = '',
extra = '',
foreignKey = device['devMac'])
primaryId=mac,
secondaryId=ip,
watched1="",
watched2=hostname,
watched3="",
watched4="",
extra="",
foreignKey=mac,
)
plugin_objects.write_result_file()
mylog('verbose', [f'[{pluginName}] Script finished'])
mylog("verbose", [f"[{pluginName}] Script finished"])
return 0
#===============================================================================
# Execute scan
#===============================================================================
def execute_name_lookup(ip, timeout):
"""
Execute the avahi-resolve command on the IP.
"""
args = ['avahi-resolve', '-a', ip]
# Execute command
output = ""
try:
mylog('debug', [f'[{pluginName}] DEBUG CMD :', args])
# Run the subprocess with a forced timeout
output = subprocess.check_output(args, universal_newlines=True, stderr=subprocess.STDOUT, timeout=timeout)
mylog('debug', [f'[{pluginName}] DEBUG OUTPUT : {output}'])
domain_name = ''
# Split the output into lines
lines = output.splitlines()
# Look for the resolved IP address
for line in lines:
if ip in line:
parts = line.split()
if len(parts) > 1:
domain_name = parts[1] # Second part is the resolved domain name
else:
mylog('verbose', [f'[{pluginName}] ⚠ ERROR - Unexpected output format: {line}'])
mylog('debug', [f'[{pluginName}] Domain Name: {domain_name}'])
return domain_name
except subprocess.CalledProcessError as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR - {e.output}'])
except subprocess.TimeoutExpired:
mylog('none', [f'[{pluginName}] TIMEOUT - the process forcefully terminated as timeout reached'])
if output == "":
mylog('none', [f'[{pluginName}] Scan: FAIL - check logs'])
else:
mylog('debug', [f'[{pluginName}] Scan: SUCCESS'])
return ''
# Function to ensure Avahi and its dependencies are running
def ensure_avahi_running(attempt=1, max_retries=2):
"""
Ensure that D-Bus is running and the Avahi daemon is started, with recursive retry logic.
"""
mylog('debug', [f'[{pluginName}] Attempt {attempt} - Ensuring D-Bus and Avahi daemon are running...'])
# Check rc-status
try:
subprocess.run(['rc-status'], check=True)
except subprocess.CalledProcessError as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR - Failed to check rc-status: {e.output}'])
return
# Create OpenRC soft level
subprocess.run(['touch', '/run/openrc/softlevel'], check=True)
# Add Avahi daemon to runlevel
try:
subprocess.run(['rc-update', 'add', 'avahi-daemon'], check=True)
except subprocess.CalledProcessError as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR - Failed to add Avahi to runlevel: {e.output}'])
return
# Start the D-Bus service
try:
subprocess.run(['rc-service', 'dbus', 'start'], check=True)
except subprocess.CalledProcessError as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR - Failed to start D-Bus: {e.output}'])
return
# Check Avahi status
status_output = subprocess.run(['rc-service', 'avahi-daemon', 'status'], capture_output=True, text=True)
if 'started' in status_output.stdout:
mylog('debug', [f'[{pluginName}] Avahi Daemon is already running.'])
return
mylog('none', [f'[{pluginName}] Avahi Daemon is not running, attempting to start... (Attempt {attempt})'])
# Start the Avahi daemon
try:
subprocess.run(['rc-service', 'avahi-daemon', 'start'], check=True)
except subprocess.CalledProcessError as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR - Failed to start Avahi daemon: {e.output}'])
# Check status after starting
status_output = subprocess.run(['rc-service', 'avahi-daemon', 'status'], capture_output=True, text=True)
if 'started' in status_output.stdout:
mylog('debug', [f'[{pluginName}] Avahi Daemon successfully started.'])
return
# Retry if not started and attempts are left
if attempt < max_retries:
mylog('debug', [f'[{pluginName}] Retrying... ({attempt + 1}/{max_retries})'])
ensure_avahi_running(attempt + 1, max_retries)
else:
mylog('none', [f'[{pluginName}] ⚠ ERROR - Avahi Daemon failed to start after {max_retries} attempts.'])
# rc-update add avahi-daemon
# rc-service avahi-daemon status
# rc-service avahi-daemon start
if __name__ == '__main__':
# =============================================================================
# Entrypoint
# =============================================================================
if __name__ == "__main__":
main()

View File

@@ -30,4 +30,4 @@ source myenv/bin/activate
update-alternatives --install /usr/bin/python python /usr/bin/python3 10
# install packages thru pip3
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag zeroconf git+https://github.com/foreign-sub/aiofreepybox.git

View File

@@ -22,4 +22,5 @@ python-nmap
dnspython
librouteros
yattag
zeroconf
git+https://github.com/foreign-sub/aiofreepybox.git

View File

@@ -22,4 +22,5 @@ python-nmap
dnspython
librouteros
yattag
zeroconf
git+https://github.com/foreign-sub/aiofreepybox.git