From 8045da2dbe1e43aacf6c313a57796315887456e0 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Thu, 12 Mar 2026 16:09:56 +0100 Subject: [PATCH] Add custom domain monitoring and update documentation --- README.md | 11 ++++++++++- list.example.txt | 13 ++++++++++++ monitor.py | 51 ++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 list.example.txt diff --git a/README.md b/README.md index c5b1720..fd62e95 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Adguard Monitor is designed for openclaw, which should usually NEVER make a requ - **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. +- **Custom Domain Monitoring**: Monitor specific domains using [list.txt](list.txt) (wildcards supported). - **Structured Logging**: UTF-8 encoded logging for unicode emoji support. ## Setup @@ -24,7 +25,15 @@ Adguard Monitor is designed for openclaw, which should usually NEVER make a requ ```bash cp .env.example .env ``` -4. **Run the monitor**: +4. **Custom Domains (Optional)**: + Copy [list.example.txt](list.example.txt) to `list.txt` and add domains: + ```bash + cp list.example.txt list.txt + ``` + - Lines starting with `!` are **negative filters** (they will be ignored). + - Wildcards like `*.example.com` are supported. + +5. **Run the monitor**: ```bash python monitor.py --interval 15 ``` diff --git a/list.example.txt b/list.example.txt new file mode 100644 index 0000000..80d394b --- /dev/null +++ b/list.example.txt @@ -0,0 +1,13 @@ +# Add domains to monitor here (one per line) +# Supports simple wildcard matches if implemented +# These are NOT block rules, just alert rules!!!!! + +# Example custom domains to monitor +# Use '!' at the start of a line to exclude a domain (negative filter) +example.com +*.doubleclick.net +ads.google.com + +# Negative Filters (exclude these domains from monitoring) +!discord.com +!*.discord.com diff --git a/monitor.py b/monitor.py index a533abb..a786d68 100644 --- a/monitor.py +++ b/monitor.py @@ -4,6 +4,7 @@ import json import time import argparse import os +import fnmatch from dotenv import load_dotenv from requests.auth import HTTPBasicAuth from datetime import datetime @@ -35,6 +36,31 @@ try: "Authorization": f"Bearer {HASS_TOKEN}", "Content-Type": "application/json", } + + # --- Custom Domain List --- + CUSTOM_DOMAINS = [] + NEGATIVE_FILTERS = [] + + list_file = "list.txt" if os.path.exists("list.txt") else "list.example.txt" + + if os.path.exists(list_file): + with open(list_file, "r", encoding="utf-8") as f: + for line in f: + line = line.strip().lower() + if not line or line.startswith("#"): + continue + if line.startswith("!"): + NEGATIVE_FILTERS.append(line[1:]) + else: + CUSTOM_DOMAINS.append(line) + + status_msg = f"Loaded {len(CUSTOM_DOMAINS)} custom domains and {len(NEGATIVE_FILTERS)} negative filters from {list_file}" + if list_file == "list.example.txt" and not os.path.exists("list.txt"): + status_msg += " (FALLBACK)" + print(status_msg) + else: + print("Note: No domain list found. Create list.txt or list.example.txt to monitor custom domains.") + except (EnvironmentError, json.JSONDecodeError) as e: print(f"Error during startup: {e}") exit(1) @@ -95,9 +121,17 @@ def get_auth_session(): session.auth = HTTPBasicAuth(USERNAME, PASSWORD) return session -def hour_and_minute(): - now = datetime.now() - return now.strftime("%H:%M") +def is_custom_match(host): + # Check negative filters first + for pattern in NEGATIVE_FILTERS: + if fnmatch.fnmatchcase(host.lower(), pattern.lower()): + return False + + # Check custom domains + for pattern in CUSTOM_DOMAINS: + if fnmatch.fnmatchcase(host.lower(), pattern.lower()): + return True + return False def fetch_and_analyze(session, verbose=False): stats.log_api_call() @@ -135,16 +169,21 @@ def fetch_and_analyze(session, verbose=False): if verbose: logger.info(f"[{client_name}] Query: {host} | Reason: {reason}") - if reason in ["FilteredBlackList", "FilteredParental", "FilteredSafeBrowsing"]: + is_blocked = reason in ["FilteredBlackList", "FilteredParental", "FilteredSafeBrowsing"] + is_custom = is_custom_match(host) + + if is_blocked or is_custom: # Check for duplicate notifications within the same "event" # Using host + client_ip as a unique key for the last notification + event_type = "BLOCKED" if is_blocked else "CUSTOM_MATCH" notification_key = f"{client_ip}:{host}:{reason}:{hour_and_minute()}" if stats.last_notified_key == notification_key: continue stats.log_blocked() - logger.warning(f"BLOCKED: {client_name} -> {host} ({reason})") - notify_hass(client_name, host, reason) + title = "🛡️ Adguard Blocked a Request" if is_blocked else "⚠️ Custom Domain Match" + logger.warning(f"{event_type}: {client_name} -> {host} ({reason if is_blocked else 'Manual match'})") + notify_hass(client_name, host, reason if is_blocked else "Custom List", title=title) stats.last_notified_key = notification_key # Dynamic limit adjustment