diff --git a/Dockerfile b/Dockerfile index 927cb31..28bf560 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,12 @@ RUN apt-get update && apt-get install -y \ tzdata \ && rm -rf /var/lib/apt/lists/* +# Install python dependencies +RUN pip install --no-cache-dir requests + +# Create crontabs directory (if needed) +RUN mkdir -p /etc/crontabs + # Copy scripts COPY update-blocklist.py /usr/local/bin/update-blocklist.py COPY entrypoint.py /usr/local/bin/entrypoint.py @@ -17,7 +23,7 @@ RUN chmod +x /usr/local/bin/update-blocklist.py /usr/local/bin/entrypoint.py # Set default timezone (can be overridden with TZ env var) ENV TZ=UTC -# Configure timezone (tzdata) — important for /usr/share/zoneinfo/* +# Configure timezone (tzdata) RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # Set entrypoint diff --git a/README.md b/README.md index 8345e13..3527d2c 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,6 @@ 5. **Check logs to verify updates** ```bash - docker-compose logs -f + docker compose logs -f ``` diff --git a/entrypoint.py b/entrypoint.py index 7116c05..2dd856d 100644 --- a/entrypoint.py +++ b/entrypoint.py @@ -14,6 +14,20 @@ logging.basicConfig( ADGUARD_YAML = Path("/adguard/AdGuardHome.yaml") FIRST_BACKUP = Path("/adguard/AdGuardHome.yaml.first-start.bak") +def setup_cron(): + cron_expr = os.getenv("BLOCKLIST_CRON", "0 6 * * *") + cron_line = f"{cron_expr} root /usr/local/bin/update-blocklist.py\n" + cron_dir = "/etc/crontabs" + cron_file = f"{cron_dir}/root" + + logging.info(f"Setting cron job: {cron_line.strip()}") + + # Ensure cron directory exists + os.makedirs(cron_dir, exist_ok=True) + + with open(cron_file, "w") as f: + f.write(cron_line) + def backup_first_start(): if not FIRST_BACKUP.exists(): logging.info("Creating first start backup...") @@ -34,17 +48,9 @@ def run_initial_update(): logging.error(f"Initial update script failed: {e}") sys.exit(1) -def setup_cron(): - cron_expr = os.getenv("BLOCKLIST_CRON", "0 6 * * *") - cron_line = f"{cron_expr} root /usr/local/bin/update-blocklist.py\n" - cron_file = "/etc/crontabs/root" - logging.info(f"Setting cron job: {cron_line.strip()}") - with open(cron_file, "w") as f: - f.write(cron_line) - def start_cron_foreground(): logging.info("Starting cron in foreground...") - os.execvp("crond", ["crond", "-f"]) + os.execvp("cron", ["cron", "-f"]) def main(): # Check AdGuardHome.yaml exists diff --git a/update-blocklist.py b/update-blocklist.py index 79f5a60..cb25eba 100644 --- a/update-blocklist.py +++ b/update-blocklist.py @@ -1,156 +1,117 @@ #!/usr/bin/env python3 import os import sys -import shutil import logging -import re import requests from pathlib import Path logging.basicConfig( level=logging.INFO, format='[update-blocklist] %(levelname)s: %(message)s', - stream=sys.stdout + stream=sys.stdout, ) -# Config / variables ADGUARD_YAML = Path("/adguard/AdGuardHome.yaml") FIRST_BACKUP = Path("/adguard/AdGuardHome.yaml.first-start.bak") LAST_CRON_BACKUP = Path("/adguard/AdGuardHome.yaml.last-cron.bak") +TMP_YAML = ADGUARD_YAML.parent / (ADGUARD_YAML.name + ".tmp") MANUAL_IPS_FILE = Path("/adguard/manually_blocked_ips.conf") CIDR_BASE_URL = "https://raw.githubusercontent.com/vulnebify/cidre/main/output/cidr/ipv4" COUNTRIES = os.getenv("BLOCK_COUNTRIES", "") -DOCKER_API_URL = os.getenv("DOCKER_API_URL", "http://socket-proxy-adguard:2375") -CONTAINER_NAME = os.getenv("ADGUARD_CONTAINER_NAME", "adguard-home") -TMP_YAML = Path("/tmp/AdGuardHome.yaml") -TMP_DIR = Path("/tmp/cidr") -def backup_first_start(): +def backup_files(): if not FIRST_BACKUP.exists(): logging.info(f"Creating first-start backup: {FIRST_BACKUP}") - shutil.copy2(ADGUARD_YAML, FIRST_BACKUP) + FIRST_BACKUP.write_text(ADGUARD_YAML.read_text()) else: logging.info("First-start backup already exists, skipping.") -def backup_last_cron(): logging.info(f"Creating last-cron backup: {LAST_CRON_BACKUP}") - shutil.copy2(ADGUARD_YAML, LAST_CRON_BACKUP) + LAST_CRON_BACKUP.write_text(ADGUARD_YAML.read_text()) def download_cidr_lists(countries): - if not countries: - logging.error("No countries specified in BLOCK_COUNTRIES environment variable.") - sys.exit(1) - - TMP_DIR.mkdir(parents=True, exist_ok=True) - all_ips = [] - - codes = [c.strip().lower() for c in countries.split(",") if c.strip()] - for code in codes: - url = f"{CIDR_BASE_URL}/{code}.cidr" + combined_ips = [] + for code in countries: + url = f"{CIDR_BASE_URL}/{code.lower()}.cidr" logging.info(f"Downloading CIDR list for {code} from {url}") try: - r = requests.get(url, timeout=15) + r = requests.get(url, timeout=30) r.raise_for_status() - lines = r.text.strip().splitlines() - logging.info(f"Downloaded {len(lines)} CIDR entries for {code}") - all_ips.extend(lines) + ips = r.text.strip().splitlines() + logging.info(f"Downloaded {len(ips)} CIDR entries for {code}") + combined_ips.extend(ips) except Exception as e: - logging.warning(f"Failed to download {url}: {e}") - - return all_ips + logging.warning(f"Failed to download {code}: {e}") + return combined_ips def read_manual_ips(): - ips = [] if MANUAL_IPS_FILE.exists(): logging.info(f"Reading manual IPs from {MANUAL_IPS_FILE}") - try: - with MANUAL_IPS_FILE.open() as f: - for line in f: - line = line.strip() - if re.match(r'^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$', line): - ips.append(line) - else: - logging.debug(f"Ignoring invalid manual IP line: {line}") - logging.info(f"Read {len(ips)} valid manual IP entries") - except Exception as e: - logging.warning(f"Error reading manual IPs: {e}") + valid_ips = [] + with MANUAL_IPS_FILE.open() as f: + for line in f: + line = line.strip() + # Simple regex match for IPv4 or IPv4 CIDR + if line and line.count('.') == 3: + valid_ips.append(line) + logging.info(f"Added {len(valid_ips)} manual IP entries") + return valid_ips else: logging.info("Manual IPs file does not exist, skipping.") - return ips - -def format_ips_yaml_list(ips): - return [f" - {ip}\n" for ip in ips] + return [] def update_yaml_with_ips(ips): - if not ADGUARD_YAML.exists(): - logging.error(f"AdGuardHome.yaml not found at {ADGUARD_YAML}") - sys.exit(1) + # Format IPs for YAML list (4 spaces indent + dash) + formatted_ips = [f" - {ip}" for ip in ips] + + inside_disallowed = False + output_lines = [] with ADGUARD_YAML.open() as f: - lines = f.readlines() - - new_lines = [] - inside_disallowed = False - ips_inserted = False - - for line in lines: - stripped = line.rstrip("\n") - - if stripped.startswith(" disallowed_clients:"): - # Write key line without any value (no [] etc) - new_lines.append(" disallowed_clients:\n") - # Insert ips - if ips: - new_lines.extend(format_ips_yaml_list(ips)) - # mark inserted - inside_disallowed = True - ips_inserted = True - continue - - if inside_disallowed: - # skip old IP entries starting with ' - ' - if stripped.startswith(" - "): - continue + for line in f: + if line.strip().startswith("disallowed_clients:"): + # Replace existing disallowed_clients block + output_lines.append("disallowed_clients:") + output_lines.extend(formatted_ips) + inside_disallowed = True + elif inside_disallowed: + # Skip old lines under disallowed_clients (assuming indentation) + if line.startswith(" ") and not line.startswith(" -"): + # This is a new section, disallowed_clients block ended + inside_disallowed = False + output_lines.append(line.rstrip("\n")) + # Else skip line inside disallowed_clients block else: - inside_disallowed = False + output_lines.append(line.rstrip("\n")) - new_lines.append(line) - - if not ips_inserted: - # disallowed_clients not found - append at end - new_lines.append("\n disallowed_clients:\n") - if ips: - new_lines.extend(format_ips_yaml_list(ips)) + # If the file ended while still inside disallowed_clients block, append nothing more (already done) + # Write to temporary YAML in same folder (to avoid cross-device rename error) with TMP_YAML.open("w") as f: - f.writelines(new_lines) + f.write("\n".join(output_lines) + "\n") + # Atomic replace TMP_YAML.replace(ADGUARD_YAML) - logging.info(f"Updated {ADGUARD_YAML} with {len(ips)} disallowed_clients entries") - -def restart_container(): - url = f"{DOCKER_API_URL}/containers/{CONTAINER_NAME}/restart" - logging.info(f"Restarting container '{CONTAINER_NAME}' via {url}") - try: - r = requests.post(url, timeout=10) - if r.status_code == 204: - logging.info("Container restarted successfully.") - else: - logging.error(f"Failed to restart container. Status: {r.status_code} Response: {r.text}") - except Exception as e: - logging.error(f"Exception during container restart: {e}") + logging.info(f"Updated {ADGUARD_YAML} with new disallowed clients list.") def main(): - backup_first_start() - backup_last_cron() - cidr_ips = download_cidr_lists(COUNTRIES) + if not ADGUARD_YAML.exists(): + logging.error(f"{ADGUARD_YAML} not found, exiting.") + sys.exit(1) + + if not COUNTRIES: + logging.error("No countries specified in BLOCK_COUNTRIES environment variable, exiting.") + sys.exit(1) + + backup_files() + + countries_list = [c.strip() for c in COUNTRIES.split(",") if c.strip()] + cidr_ips = download_cidr_lists(countries_list) manual_ips = read_manual_ips() + combined_ips = cidr_ips + manual_ips - if not combined_ips: - logging.warning("No IPs to add to disallowed_clients. The list will be empty.") + update_yaml_with_ips(combined_ips) - restart_container() - logging.info("Blocklist update complete.") if __name__ == "__main__": main()