Add custom domain monitoring and update documentation

This commit is contained in:
Space-Banane
2026-03-12 16:09:56 +01:00
parent d340d60b52
commit 8045da2dbe
3 changed files with 68 additions and 7 deletions

View File

@@ -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"). - **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. - **Strict Configuration**: Fails fast if environment variables are missing.
- **Home Assistant Notifications**: Native mobile push messages for blocked queries. - **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. - **Structured Logging**: UTF-8 encoded logging for unicode emoji support.
## Setup ## Setup
@@ -24,7 +25,15 @@ Adguard Monitor is designed for openclaw, which should usually NEVER make a requ
```bash ```bash
cp .env.example .env 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 ```bash
python monitor.py --interval 15 python monitor.py --interval 15
``` ```

13
list.example.txt Normal file
View File

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

View File

@@ -4,6 +4,7 @@ import json
import time import time
import argparse import argparse
import os import os
import fnmatch
from dotenv import load_dotenv from dotenv import load_dotenv
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from datetime import datetime from datetime import datetime
@@ -35,6 +36,31 @@ try:
"Authorization": f"Bearer {HASS_TOKEN}", "Authorization": f"Bearer {HASS_TOKEN}",
"Content-Type": "application/json", "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: except (EnvironmentError, json.JSONDecodeError) as e:
print(f"Error during startup: {e}") print(f"Error during startup: {e}")
exit(1) exit(1)
@@ -95,9 +121,17 @@ def get_auth_session():
session.auth = HTTPBasicAuth(USERNAME, PASSWORD) session.auth = HTTPBasicAuth(USERNAME, PASSWORD)
return session return session
def hour_and_minute(): def is_custom_match(host):
now = datetime.now() # Check negative filters first
return now.strftime("%H:%M") 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): def fetch_and_analyze(session, verbose=False):
stats.log_api_call() stats.log_api_call()
@@ -135,16 +169,21 @@ def fetch_and_analyze(session, verbose=False):
if verbose: if verbose:
logger.info(f"[{client_name}] Query: {host} | Reason: {reason}") 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" # Check for duplicate notifications within the same "event"
# Using host + client_ip as a unique key for the last notification # 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()}" notification_key = f"{client_ip}:{host}:{reason}:{hour_and_minute()}"
if stats.last_notified_key == notification_key: if stats.last_notified_key == notification_key:
continue continue
stats.log_blocked() stats.log_blocked()
logger.warning(f"BLOCKED: {client_name} -> {host} ({reason})") title = "🛡️ Adguard Blocked a Request" if is_blocked else "⚠️ Custom Domain Match"
notify_hass(client_name, host, reason) 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 stats.last_notified_key = notification_key
# Dynamic limit adjustment # Dynamic limit adjustment