diff --git a/Dockerfile b/Dockerfile index 05db38b..28bf560 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,30 @@ -FROM alpine:latest +FROM python:3.11-slim -RUN apk add --no-cache curl bash busybox tzdata +# Install required utilities +RUN apt-get update && apt-get install -y \ + curl \ + cron \ + tzdata \ + && rm -rf /var/lib/apt/lists/* -COPY update-blocklist.sh /usr/local/bin/update-blocklist.sh -COPY entrypoint.sh /entrypoint.sh +# Install python dependencies +RUN pip install --no-cache-dir requests -RUN chmod +x /usr/local/bin/update-blocklist.sh /entrypoint.sh +# Create crontabs directory (if needed) +RUN mkdir -p /etc/crontabs -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +# Copy scripts +COPY update-blocklist.py /usr/local/bin/update-blocklist.py +COPY entrypoint.py /usr/local/bin/entrypoint.py + +# Make scripts executable +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) +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set entrypoint +ENTRYPOINT ["/usr/local/bin/entrypoint.py"] diff --git a/README.md b/README.md index 8345e13..c56434b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - Updates the disallowed_clients section in the AdGuard Home config. - Configurable update frequency via cron expression environment variable. - Automatically restarts the AdGuard Home container after updates via Docker socket proxy. +- Backup `AdguardHome.yaml` at first startup, then create a second backup at each update. ## Environment Variables @@ -73,6 +74,6 @@ 5. **Check logs to verify updates** ```bash - docker-compose logs -f + docker compose logs -f ``` diff --git a/docker-compose.yml b/docker-compose.yml index f56559c..1594fa8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ services: adguard-cidre: build: . container_name: adguard-cidre + restart: unless-stopped environment: - TZ=Europe/Paris # change to your timezone - BLOCK_COUNTRIES=cn,ru,ir # choose countries listed IP to block. Full lists here https://github.com/vulnebify/cidre/tree/main/output/cidr/ipv4 diff --git a/entrypoint.py b/entrypoint.py new file mode 100644 index 0000000..2dd856d --- /dev/null +++ b/entrypoint.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import logging +from pathlib import Path + +logging.basicConfig( + level=logging.INFO, + format='[entrypoint] %(message)s', + stream=sys.stdout +) + +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...") + FIRST_BACKUP.write_text(ADGUARD_YAML.read_text()) + else: + logging.info("First start backup already exists.") + +def run_initial_update(): + logging.info("Running initial update-blocklist.py script...") + try: + subprocess.run( + ["/usr/local/bin/update-blocklist.py"], + check=True, + stdout=sys.stdout, + stderr=sys.stderr, + ) + except subprocess.CalledProcessError as e: + logging.error(f"Initial update script failed: {e}") + sys.exit(1) + +def start_cron_foreground(): + logging.info("Starting cron in foreground...") + os.execvp("cron", ["cron", "-f"]) + +def main(): + # Check AdGuardHome.yaml exists + if not ADGUARD_YAML.exists(): + logging.error(f"{ADGUARD_YAML} not found. Exiting.") + sys.exit(1) + + backup_first_start() + run_initial_update() + setup_cron() + start_cron_foreground() + +if __name__ == "__main__": + main() diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index a4a93fa..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -set -e - -if [ -n "$TZ" ]; then - if [ -f "/usr/share/zoneinfo/$TZ" ]; then - cp "/usr/share/zoneinfo/$TZ" /etc/localtime - echo "$TZ" > /etc/timezone - fi -fi - -CRON_EXPR="${BLOCKLIST_CRON:-"0 6 * * *"}" -echo "$CRON_EXPR /usr/local/bin/update-blocklist.sh" > /etc/crontabs/root - -exec crond -f -c /etc/crontabs \ No newline at end of file diff --git a/update-blocklist.py b/update-blocklist.py new file mode 100644 index 0000000..f9eee6d --- /dev/null +++ b/update-blocklist.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +import os +import sys +import logging +import requests +from pathlib import Path + +logging.basicConfig( + level=logging.INFO, + format='[update-blocklist] %(levelname)s: %(message)s', + stream=sys.stdout, +) + +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", "") + +def backup_files(): + if not FIRST_BACKUP.exists(): + logging.info(f"Creating first-start backup: {FIRST_BACKUP}") + FIRST_BACKUP.write_text(ADGUARD_YAML.read_text()) + else: + logging.info("First-start backup already exists, skipping.") + + logging.info(f"Creating last-cron backup: {LAST_CRON_BACKUP}") + LAST_CRON_BACKUP.write_text(ADGUARD_YAML.read_text()) + +def download_cidr_lists(countries): + 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=30) + r.raise_for_status() + 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 {code}: {e}") + return combined_ips + +def read_manual_ips(): + if MANUAL_IPS_FILE.exists(): + logging.info(f"Reading manual IPs from {MANUAL_IPS_FILE}") + valid_ips = [] + with MANUAL_IPS_FILE.open() as f: + for line in f: + line = line.strip() + # Simple check for IPv4 or IPv4 CIDR format + 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 [] + +def update_yaml_with_ips(ips): + output_lines = [] + inside_disallowed = False + disallowed_indent = "" + + with ADGUARD_YAML.open() as f: + lines = f.readlines() + + for line in lines: + stripped = line.lstrip() + indent = line[:len(line) - len(stripped)] + + if stripped.startswith("disallowed_clients:"): + # Capture the indentation of the disallowed_clients key + disallowed_indent = indent + + # Replace entire line with just 'disallowed_clients:' (remove any []) + output_lines.append(f"{disallowed_indent}disallowed_clients:") + + # Add all IPs indented 2 spaces more than disallowed_clients + formatted_ips = [f"{disallowed_indent} - {ip}" for ip in ips] + output_lines.extend(formatted_ips) + + inside_disallowed = True + continue + + if inside_disallowed: + # We skip all old lines inside disallowed_clients block. + # The block ends when we find a line with indentation + # less than or equal to disallowed_indent but not the key line itself. + # To detect end of block, compare indent length: + if len(indent) <= len(disallowed_indent) and stripped != "": + inside_disallowed = False + output_lines.append(line.rstrip("\n")) + else: + # skip this line (old disallowed_clients content) + continue + else: + output_lines.append(line.rstrip("\n")) + + # Write temp file in same directory to avoid cross-device rename errors + with TMP_YAML.open("w") as f: + f.write("\n".join(output_lines) + "\n") + + TMP_YAML.replace(ADGUARD_YAML) + logging.info(f"Updated {ADGUARD_YAML} with new disallowed clients list.") + + +def restart_adguard_container(): + docker_api_url = os.getenv("DOCKER_API_URL", "http://socket-proxy-adguard:2375") + container_name = os.getenv("ADGUARD_CONTAINER_NAME", "adguardhome") + restart_url = f"{docker_api_url}/containers/{container_name}/restart" + + logging.info(f"Restarting AdGuard container '{container_name}'...") + try: + resp = requests.post(restart_url, timeout=10) + if resp.status_code == 204: + logging.info("AdGuard container restarted successfully.") + else: + logging.error(f"Failed to restart container: {resp.status_code} {resp.text}") + except Exception as e: + logging.error(f"Error restarting container: {e}") + +def main(): + 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 + + update_yaml_with_ips(combined_ips) + + restart_adguard_container() + +if __name__ == "__main__": + main() diff --git a/update-blocklist.sh b/update-blocklist.sh deleted file mode 100644 index cdddeb1..0000000 --- a/update-blocklist.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash - -set -e - -ADGUARD_YAML="/adguard/AdGuardHome.yaml" -TMP_YAML="/tmp/AdGuardHome.yaml" -MANUAL_IPS_FILE="/adguard/manually_blocked_ips.conf" -CIDR_BASE_URL="https://raw.githubusercontent.com/vulnebify/cidre/main/output/cidr/ipv4" -COUNTRIES=${BLOCK_COUNTRIES:-""} -DOCKER_API_URL=${DOCKER_API_URL:-"http://socket-proxy-adguard:2375"} -CONTAINER_NAME=${ADGUARD_CONTAINER_NAME:-"adguard-home"} - -if [ -z "$COUNTRIES" ]; then - echo "No countries specified in BLOCK_COUNTRIES." - exit 1 -fi - -mkdir -p /tmp/cidr -> /tmp/cidr/all.txt - -IFS=',' read -ra CODES <<< "$COUNTRIES" -for CODE in "${CODES[@]}"; do - echo "Downloading CIDR list for $CODE..." - curl -sf "$CIDR_BASE_URL/${CODE,,}.cidr" -o "/tmp/cidr/${CODE}.cidr" || continue - cat "/tmp/cidr/${CODE}.cidr" >> /tmp/cidr/all.txt -done - -if [ -f "$MANUAL_IPS_FILE" ]; then - echo "Validating and adding manually blocked IPs from $MANUAL_IPS_FILE..." - grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$' "$MANUAL_IPS_FILE" >> /tmp/cidr/all.txt -fi - -# Format IPs as YAML list items -sed 's/^/ - /' /tmp/cidr/all.txt > /tmp/cidr/ips_formatted.txt - -awk ' -BEGIN { - # Read formatted IPs into array - while ((getline line < "/tmp/cidr/ips_formatted.txt") > 0) { - ips[++count] = line - } - close("/tmp/cidr/ips_formatted.txt") - inside=0 -} - -/^ disallowed_clients:/ { - print - inside=1 - next -} - -/^ [^ ]/ && inside==1 { - # Insert all IPs here - for (i=1; i<=count; i++) print ips[i] - inside=0 -} - -{ - if (!inside) print -} - -END { - # If file ended while still inside disallowed_clients section - if (inside==1) { - for (i=1; i<=count; i++) print ips[i] - } -} -' "$ADGUARD_YAML" > "$TMP_YAML" - -mv "$TMP_YAML" "$ADGUARD_YAML" - -echo "Restarting $CONTAINER_NAME container..." -curl -s -X POST "$DOCKER_API_URL/containers/$CONTAINER_NAME/restart" -o /dev/null - -echo "Done."