From 66ec4945d50e5f8b78bf750ffdd4cdfef1edde8b Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Thu, 12 Mar 2026 15:47:58 +0100 Subject: [PATCH] Initial commit with cleaned history and environment configuration --- .env.example | 11 ++++ .gitignore | 4 ++ README.md | 37 ++++++++++++ monitor.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 5 files changed, 203 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 monitor.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..377949e --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# AdGuard Configuration +ADGUARD_URL=http://your-adguard-ip +ADGUARD_USER=your-username +ADGUARD_PASSWORD=your-password + +# Home Assistant Configuration +HASS_URL=https://your-hass-instance.com/api/services/notify/mobile_app_your_phone +HASS_TOKEN=your-long-lived-access-token + +# Monitored Clients (JSON Format: {"IP": "Nickname"}) +CLIENTS='{"192.168.1.100": "My Laptop", "192.168.1.101": "My Phone"}' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eb56d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +*.log +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5b1720 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# AdGuard Monitor + +Monitoring service for AdGuard DNS queries with notifications sent to Home Assistant. + +## Who is this for? +> This is for me mainly, so let's see if you need it too. +Adguard Monitor is designed for openclaw, which should usually NEVER make a request that needs to be blocked. If it does, I want to know about it immediately. + +## Features +- **Multi-Client Support**: Monitor specific IP addresses and receive nicknamed notifications (e.g., "BLOCKED: My Laptop"). +- **Strict Configuration**: Fails fast if environment variables are missing. +- **Home Assistant Notifications**: Native mobile push messages for blocked queries. +- **Structured Logging**: UTF-8 encoded logging for unicode emoji support. + +## Setup + +1. **Clone the repository.** +2. **Install dependencies**: + ```bash + pip install -r requirements.txt + ``` +3. **Configure environment**: + Copy `.env.example` to `.env` and fill in your details: + ```bash + cp .env.example .env + ``` +4. **Run the monitor**: + ```bash + python monitor.py --interval 15 + ``` + +## Environment Variables +- `ADGUARD_URL`: Full URL of your AdGuard instance (e.g., `http://192.168.1.50`). +- `ADGUARD_USER/PASSWORD`: API credentials. +- `CLIENTS`: A JSON-formatted dictionary mapping IPs to display names. +- `HASS_URL/TOKEN`: Home Assistant mobile notification endpoint and Long-Lived Access Token. + diff --git a/monitor.py b/monitor.py new file mode 100644 index 0000000..10dbf59 --- /dev/null +++ b/monitor.py @@ -0,0 +1,149 @@ +import logging +import requests +import json +import time +import argparse +import os +from dotenv import load_dotenv +from requests.auth import HTTPBasicAuth +from datetime import datetime + +# Load environment variables +load_dotenv() + +def get_env_or_fail(key): + value = os.getenv(key) + if not value or value.strip() == "": + raise EnvironmentError(f"CRITICAL: Missing required environment variable: {key}") + return value + +# --- Configuration --- +try: + ADGUARD_URL = get_env_or_fail("ADGUARD_URL") + QUERY_LOG_URL = f"{ADGUARD_URL}/control/querylog" + USERNAME = get_env_or_fail("ADGUARD_USER") + PASSWORD = get_env_or_fail("ADGUARD_PASSWORD") + + # Clients configuration via JSON string in .env: {"IP": "NAME"} + CLIENTS_RAW = get_env_or_fail("CLIENTS") + CLIENTS = json.loads(CLIENTS_RAW) + + # --- Home Assistant Configuration --- + HASS_URL = get_env_or_fail("HASS_URL") + HASS_TOKEN = get_env_or_fail("HASS_TOKEN") + HASS_HEADERS = { + "Authorization": f"Bearer {HASS_TOKEN}", + "Content-Type": "application/json", + } +except (EnvironmentError, json.JSONDecodeError) as e: + print(f"Error during startup: {e}") + exit(1) + +def notify_hass(message,title): + payload = { + "message": message, + "title": title, + "data": { + "push": { + "sound": "default", + "badge": 1, + }, + "icon_url": "https://adguard.com/favicon.ico", + } + } + try: + requests.post(HASS_URL, headers=HASS_HEADERS, json=payload, timeout=5) + except Exception as e: + logger.error(f"Failed to send HASS notification: {e}") + + +# --- Logging Setup --- +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(levelname)s] %(message)s', + handlers=[ + logging.FileHandler("adguard_monitor.log", encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# --- State Management --- +class MonitorStats: + def __init__(self): + self.api_calls = 0 + self.total_blocked = 0 + self.last_log_id = None + + def log_api_call(self): + self.api_calls += 1 + + def log_blocked(self): + self.total_blocked += 1 + + def get_summary(self): + return f"API Polls: {self.api_calls} | Total Blocked: {self.total_blocked}" + +stats = MonitorStats() + +def get_auth_session(): + session = requests.Session() + session.auth = HTTPBasicAuth(USERNAME, PASSWORD) + return session + +def fetch_and_analyze(session): + stats.log_api_call() + try: + response = session.get(QUERY_LOG_URL, params={'limit': 50}, timeout=10) + response.raise_for_status() + + data = response.json() + logs = data.get('data', []) + + # Log pull event + logger.info(f"Pulled {len(logs)} entries from API") + + if not logs: + return + + # Process logs in reverse order (oldest to newest) to maintain sequence + new_items_processed = 0 + for log in reversed(logs): + log_id = log.get('time') + if stats.last_log_id and log_id <= stats.last_log_id: + continue + + client_ip = log.get('client') + if client_ip not in CLIENTS: + continue + + stats.last_log_id = log_id + new_items_processed += 1 + + reason = log.get('reason') + host = log.get('question', {}).get('name') + client_name = CLIENTS[client_ip] + + if reason in ["FilteredBlackList", "Rewrite"]: + stats.log_blocked() + msg = f"🚫 Blocked: {host}\nReason: {reason}" + logger.warning(msg) + notify_hass(msg, f"BLOCKED: {client_name}") + + logger.info(f"Analyzed {new_items_processed} new entries since last pull.") + logger.debug(f"Status Update: {stats.get_summary()}") + + except requests.exceptions.RequestException as e: + logger.error(f"API Error: {e}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="AdGuard Monitor Service") + parser.add_argument("--interval", type=int, default=15, help="Update interval in seconds") + args = parser.parse_args() + + logger.info(f"AdGuard Monitor Service Started. Interval: {args.interval}s") + session = get_auth_session() + + while True: + fetch_and_analyze(session) + time.sleep(args.interval) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..df7458c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +python-dotenv