diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d735514c..9be712f0 100755 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 \ diff --git a/Dockerfile b/Dockerfile index 913b64e4..1e2eefdb 100755 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/Dockerfile.debian b/Dockerfile.debian index e43eb1c2..ddd5a7d1 100755 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -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 diff --git a/front/plugins/avahi_scan/avahi_scan.py b/front/plugins/avahi_scan/avahi_scan.py index 3f4528b4..7cb2b665 100755 --- a/front/plugins/avahi_scan/avahi_scan.py +++ b/front/plugins/avahi_scan/avahi_scan.py @@ -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() diff --git a/install/debian12/install_dependencies.debian12.sh b/install/debian12/install_dependencies.debian12.sh index 4fc4174f..0697adad 100755 --- a/install/debian12/install_dependencies.debian12.sh +++ b/install/debian12/install_dependencies.debian12.sh @@ -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 diff --git a/install/proxmox/requirements.txt b/install/proxmox/requirements.txt index 2525b62e..fdd30017 100755 --- a/install/proxmox/requirements.txt +++ b/install/proxmox/requirements.txt @@ -22,4 +22,5 @@ python-nmap dnspython librouteros yattag +zeroconf git+https://github.com/foreign-sub/aiofreepybox.git diff --git a/install/ubuntu24/requirements.txt b/install/ubuntu24/requirements.txt index 2525b62e..fdd30017 100755 --- a/install/ubuntu24/requirements.txt +++ b/install/ubuntu24/requirements.txt @@ -22,4 +22,5 @@ python-nmap dnspython librouteros yattag +zeroconf git+https://github.com/foreign-sub/aiofreepybox.git