36 Commits
Author SHA1 Message Date
Djeex b197fd5977 Merge pull request 'Fix ci release template' (#16) from fix-ci-release-template into main
CI / build-and-scan (push) Successful in 28s
Reviewed-on: #16
2026-08-23 23:24:41 +02:00
Djeex 13926f2e9f Link each commit in the release changelog to its own commit page
CI / build-and-scan (pull_request) Successful in 23s
COMMIT_LIST only rendered the bare subject line per commit, with no
way to jump to that specific commit — only the triggering commit
(Source:) had a link. Each line now reads
"- [<short-sha>](<repo>/commit/<sha>) <subject>", matching the same
link style already used for Source.
2026-08-23 23:20:45 +02:00
Djeex ae16862a11 Align release changelog with the commit-list template used on the sibling repos
CI / build-and-scan (pull_request) Successful in 26s
2026-08-23 22:57:01 +02:00
Djeex 6ac20e25e3 Merge pull request 'Run container as non-root via PUID/PGID / Move environment configuration to a .env file' (#14) from dev-v2 into main
CI / build-and-scan (push) Successful in 32s
Reviewed-on: #14
2026-08-23 15:17:00 +02:00
Djeex 620d00134c Move environment configuration to a .env file
CI / build-and-scan (pull_request) Successful in 47s
2026-08-23 15:11:22 +02:00
Djeex d13f79ed82 Bump version to 1.5.0 for non-root support 2026-08-23 14:15:20 +02:00
Djeex cf96acda92 Run container as non-root via PUID/PGID with a startup entrypoint banner and logs 2026-08-23 14:14:54 +02:00
adguard-cidre-ci d811faef45 Bump build version to 1.4.1 [skip ci] 2026-08-22 14:04:02 +00:00
Djeex 85fb4b4e79 Pin Python to a patch version and auto-generate versioned releases with changelog
CI / build-and-scan (push) Successful in 36s
2026-08-22 16:02:26 +02:00
Djeex 20ec627515 Always update the versioned image tag alongside :latest on container changes
CI / build-and-scan (push) Successful in 37s
2026-08-22 14:46:56 +02:00
Djeex f8e6888d50 Merge pull request 'Update actions/checkout action to v7' (#12) from renovate/actions-checkout-7.x into main
CI / build-and-scan (push) Successful in 29s
Reviewed-on: #12
2026-08-22 14:42:32 +02:00
Djeex f9f8506963 Update actions/checkout action to v7
CI / build-and-scan (pull_request) Successful in 25s
2026-08-22 14:39:16 +02:00
Djeex a63be55cb7 Merge pull request 'Update python Docker tag to v3.14' (#11) from renovate/python-3.x into main
CI / build-and-scan (push) Successful in 28s
Reviewed-on: #11
2026-08-22 14:31:34 +02:00
Djeex 0bd6a62eca Update python Docker tag to v3.14
CI / build-and-scan (pull_request) Successful in 33s
2026-08-22 11:04:14 +00:00
Djeex b865da38f3 Add labels to Renovate PRs based on update type and vulnerability status
CI / build-and-scan (push) Successful in 33s
2026-08-22 13:02:27 +02:00
Djeex 67b4984664 Temporarily remove Renovate schedule restriction for manual testing
CI / build-and-scan (push) Successful in 30s
2026-08-22 12:44:29 +02:00
Djeex 5eafd7c7cc Add Renovate config, versioned image publishing, and pinned prod requirements
CI / build-and-scan (push) Successful in 43s
2026-08-22 12:17:59 +02:00
Djeex a72b486b3b Fix weekly scheduling crash: schedule library uses full weekday names, not 3-letter abbreviations 2026-08-22 12:17:09 +02:00
Djeex d051d9deb7 Fix CI unit test step with a dedicated Docker build stage
CI / build-and-scan (push) Successful in 34s
2026-08-22 11:11:41 +02:00
Djeex 267d9e52e0 Add unit tests for backup_first_start and run them in CI
CI / build-and-scan (push) Failing after 40s
2026-08-22 11:09:24 +02:00
Djeex 4753d80891 CI workflow with smoke test
CI / build-and-scan (push) Successful in 48s
2026-08-22 10:52:15 +02:00
Djeex 36611ef30b Add CI workflow
CI / build-and-scan (push) Successful in 17s
2026-08-22 10:00:59 +02:00
Djeex 5272796d71 python 3.13 2025-09-19 22:39:27 +00:00
Djeex 157a0f7830 Removed french discord link 2025-07-07 14:05:24 +00:00
Djeex 13379a3419 Merge branch 'wip-python' 2025-06-06 12:40:32 +00:00
Djeex fedbe1e227 Fixed wrong repo url 2025-06-06 12:39:59 +00:00
Djeex 15ba895b04 Merge pull request 'v1.3 - Excluding list (all countries except the specified ones)' (#6) from wip-python into main
Reviewed-on: #6
2025-06-06 14:17:11 +02:00
Djeex 7676d34a39 Excluding list 2025-06-06 12:09:41 +00:00
Djeex c8451688ca Github notice 2025-06-06 11:12:15 +00:00
Djeex bb6634ffc1 Merge pull request 'New image based on python-alpine. Image size reduced by 2. A bit is a bit.' (#5) from wip-python into main
Reviewed-on: #5
2025-06-01 19:28:56 +02:00
Djeex 1e589ba91a New image based on python-alpine. Image size reduced by 2. A bit is a bit. 2025-06-01 17:27:04 +00:00
Djeex 2340aea618 Merge branch 'wip-python' 2025-06-01 11:28:15 +00:00
Djeex 5f54c87cf2 fixed yaml exemple in readme 2025-06-01 11:27:37 +00:00
Djeex e3632a84fe Merge pull request 'wip-python -> removed cron for python scheduling, and added pyyaml library.' (#4) from wip-python into main
Reviewed-on: #4
2025-06-01 13:23:22 +02:00
Djeex 125c4eaed7 fixed yaml dns entry 2025-06-01 11:11:43 +00:00
Djeex 1236970426 Replaced cron with python scheduler and using python yaml library. Updated readme. 2025-06-01 10:44:06 +00:00
15 changed files with 836 additions and 277 deletions
+22
View File
@@ -0,0 +1,22 @@
# User/group id the process runs as, matches ownership of the /adguard mount
PUID=1000
PGID=1000
# Timezone of the container
TZ=Europe/Paris
# Country codes for CIDR lists, comma separated. Prefix with ! to exclude instead of include.
# Full lists here: https://github.com/vulnebify/cidre/tree/main/output/cidr/ipv4
BLOCK_COUNTRIES=cn,ru
# Scheduling: daily or weekly
BLOCKLIST_CRON_TYPE=daily
# If weekly, choose the day: mon, tue, wed, thu, fri, sat, sun
BLOCKLIST_CRON_DAY=mon
# Time of day to run the update, 24h HH:MM format
BLOCKLIST_CRON_TIME=06:00
# Docker API URL used to restart the AdGuard container (via socket-proxy)
DOCKER_API_URL=http://socket-proxy-adguard:2375
# Name of the AdGuard Home container to restart
ADGUARD_CONTAINER_NAME=adguardhome
+185
View File
@@ -0,0 +1,185 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1"
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Build Docker image
run: |
docker build -t adguard-cidre:ci . 2>&1 | tee build.log
if grep -q "Building wheel for" build.log; then
echo "::warning::A dependency was built from source — check Python/Alpine compatibility"
fi
- name: Smoke test (syntax check)
run: |
docker run --rm --entrypoint python adguard-cidre:ci -c "
import ast
with open('blocklist_scheduler.py') as f:
source = f.read()
try:
ast.parse(source)
print('OK: syntax is valid')
except SyntaxError as e:
print(f'::error::Syntax error: {e}')
exit(1)
"
- name: Run unit tests
run: |
docker build --target test -t adguard-cidre:test .
docker run --rm adguard-cidre:test pytest -v
- name: Check deprecation warnings
run: |
docker run --rm --entrypoint python adguard-cidre:ci -W error::DeprecationWarning -c "import blocklist_scheduler" 2>&1 | tee deprecation.log || true
if grep -qi "deprecat" deprecation.log; then
echo "::warning::Deprecation warning detected, check logs"
fi
- name: Scan with Trivy (critical - blocking)
run: |
docker run --rm \
-e DOCKER_HOST=tcp://dockerhost:2375 \
--add-host=dockerhost:host-gateway \
aquasec/trivy:0.74.0 image --exit-code 1 --severity CRITICAL adguard-cidre:ci
- name: Scan with Trivy (high - informative)
run: |
docker run --rm \
-e DOCKER_HOST=tcp://dockerhost:2375 \
--add-host=dockerhost:host-gateway \
aquasec/trivy:0.74.0 image --exit-code 0 --severity HIGH adguard-cidre:ci
- name: Publish tagged image
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
BEFORE="${{ github.event.before }}"
if [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ] && git cat-file -e "$BEFORE" 2>/dev/null; then
BASE_REF="$BEFORE"
else
BASE_REF="HEAD~1"
fi
CHANGED=$(git diff --name-only "$BASE_REF" "${{ github.sha }}")
echo "Changed files:"
echo "$CHANGED"
if ! echo "$CHANGED" | grep -qE '^(Dockerfile|blocklist_scheduler\.py|VERSION)$'; then
echo "No container-relevant file changed, skipping publish."
exit 0
fi
if echo "$CHANGED" | grep -qE '^VERSION$'; then
echo "VERSION was manually edited in this push, using it as-is."
else
echo "VERSION untouched but container files changed, auto-bumping the build number (Z)."
OLD_VERSION=$(tr -d '[:space:]' < VERSION)
IFS='.' read -r MAJOR MINOR PATCH <<< "$OLD_VERSION"
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
echo "$NEW_VERSION" > VERSION
git config user.name "adguard-cidre-ci"
git config user.email "[email protected]"
git add VERSION
git commit -m "Bump build version to $NEW_VERSION [skip ci]"
# Belt and suspenders: actions/checkout can leave its own ephemeral
# credential injected as an extraheader, which would silently override
# the URL-embedded token below. persist-credentials:false on checkout
# should already prevent this, but strip it here too just in case.
git config --unset-all http.https://git.djeex.fr/.extraheader || true
git push "https://Djeex:${{ secrets.CI_PUSH_TOKEN }}@git.djeex.fr/Djeex/adguard-cidre.git" HEAD:main
fi
VERSION=$(tr -d '[:space:]' < VERSION)
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
MINOR_TAG="${MAJOR}.${MINOR}"
IMAGE=git.djeex.fr/djeex/adguard-cidre
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.djeex.fr -u Djeex --password-stdin
# Retag the already-built, already-scanned image — never rebuild for publish,
# so what ships is byte-for-byte what Trivy just scanned.
docker tag adguard-cidre:ci "$IMAGE:latest"
docker tag adguard-cidre:ci "$IMAGE:$MINOR_TAG"
docker tag adguard-cidre:ci "$IMAGE:$VERSION"
docker push "$IMAGE:latest"
docker push "$IMAGE:$MINOR_TAG"
docker push "$IMAGE:$VERSION"
TRIGGER_MSG=$(git log -1 --format=%s "${{ github.sha }}")
PR_NUM=$(echo "$TRIGGER_MSG" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)
CATEGORY="🔧 Maintenance"
CHANGE_TITLE="$TRIGGER_MSG"
if [ -n "$PR_NUM" ]; then
PR_JSON=$(curl -s -H "Authorization: token ${{ secrets.CI_PUSH_TOKEN }}" \
"https://git.djeex.fr/api/v1/repos/Djeex/adguard-cidre/pulls/$PR_NUM")
PR_TITLE=$(echo "$PR_JSON" | jq -r '.title // empty' 2>/dev/null || true)
LABELS=$(echo "$PR_JSON" | jq -r '.labels[]?.name' 2>/dev/null || true)
if [ -n "$PR_TITLE" ]; then
CHANGE_TITLE="$PR_TITLE"
fi
if echo "$LABELS" | grep -qx 'bug'; then
CATEGORY="⚠️ Hotfix"
elif echo "$LABELS" | grep -qx 'major'; then
CATEGORY="💥 Breaking change"
elif echo "$LABELS" | grep -qx 'minor'; then
CATEGORY="✨ Update"
fi
fi
REPO_URL="https://git.djeex.fr/Djeex/adguard-cidre"
COMMIT_LIST=$(git log --no-merges --format="- [%h](${REPO_URL}/commit/%H) %s" "$BASE_REF".."${{ github.sha }}")
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
SOURCE_LINE="[${SHORT_SHA}](${REPO_URL}/commit/${{ github.sha }})"
if [ -n "$PR_NUM" ]; then
SOURCE_LINE="[#${PR_NUM}](${REPO_URL}/pulls/${PR_NUM}) · ${SOURCE_LINE}"
fi
BODY=$(cat <<EOF
## Changelog
---
### ${CATEGORY}
---
${CHANGE_TITLE}
**Source:** ${SOURCE_LINE}
**Image:** \`${IMAGE}:${VERSION}\`
**Commits:**
${COMMIT_LIST}
EOF
)
JSON_PAYLOAD=$(jq -n \
--arg tag "$VERSION" \
--arg name "$VERSION" \
--arg body "$BODY" \
'{tag_name: $tag, name: $name, target_commitish: "main", body: $body}')
curl -s -o /dev/null -w "Release API response: %{http_code}\n" -X POST \
-H "Authorization: token ${{ secrets.CI_PUSH_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" \
"https://git.djeex.fr/api/v1/repos/Djeex/adguard-cidre/releases"
+2
View File
@@ -1,2 +1,4 @@
/adguard/*.log /adguard/*.log
/tmp/ /tmp/
__pycache__/
.pytest_cache/
+16 -23
View File
@@ -1,30 +1,23 @@
FROM python:3.11-slim FROM python:3.14.7-alpine AS base
# Install required utilities ENV TZ=Europe/Paris
RUN apt-get update && apt-get install -y \
curl \
cron \
tzdata \
&& rm -rf /var/lib/apt/lists/*
# Install python dependencies RUN apk add --no-cache tzdata curl su-exec \
RUN pip install --no-cache-dir requests && cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone
# Create crontabs directory (if needed) WORKDIR /app
RUN mkdir -p /etc/crontabs
# Copy scripts COPY requirements.txt .
COPY update-blocklist.py /usr/local/bin/update-blocklist.py RUN pip install --no-cache-dir -r requirements.txt
COPY entrypoint.py /usr/local/bin/entrypoint.py
# Make scripts executable COPY blocklist_scheduler.py entrypoint.sh VERSION ./
RUN chmod +x /usr/local/bin/update-blocklist.py /usr/local/bin/entrypoint.py RUN chmod +x entrypoint.sh
# Set default timezone (can be overridden with TZ env var) FROM base AS test
ENV TZ=UTC RUN pip install --no-cache-dir pytest==9.1.1
COPY tests/ tests/
COPY pytest.ini .
# Configure timezone (tzdata) FROM base
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ENTRYPOINT ["./entrypoint.sh"]
# Set entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.py"]
+47 -34
View File
@@ -1,72 +1,85 @@
<h1 align="center"> Adguard CIDRE Sync</h1> <h1 align="center"> Adguard CIDRE Sync</h1>
<div align="center">
<a href="https://discord.gg/gxffg3GA96">
<img src="https://img.shields.io/badge/JV%20hardware-rejoindre-green?style=flat-square&logo=discord&logoColor=%23fff" alt="JV Hardware">
</a>
</div>
**Adguard CIDRE Sync** - A bot to synchronize adguard clients disallow list with countries CIDR list of your choices. **Adguard CIDRE Sync** - A bot to synchronize adguard clients disallow list with countries CIDR list of your choices.
*The code is partially generated by AI* > [!NOTE]
>_The code was partially written and structured using a generative AI._
>
>_Github repo is a mirror of https://git.djeex.fr/Djeex/adguard-cidre. You'll find full package, history and release note there._
## Sommaire ## Sommaire
- [Features](#features) - [Features](#features)
- [Environment Variables](#environment-variables) - [Environment Variables](#environment-variables)
- [Volumes](#volumes)
- [File Structure](#file-structure) - [File Structure](#file-structure)
- [Installation and Usage](#nstallation-and-usage) - [Installation and Usage](#installation-and-usage)
## Features ## Features
- Automatically downloads IP CIDR blocks for specified countries to block. - Downloads CIDR lists by country from GitHub
- Supports additional manually blocked IPs from a configurable file. - (Optional) Adds manual IPs from a `manually_blocked_ips.conf` file
- Updates the disallowed_clients section in the AdGuard Home config. - Updates the `AdGuardHome.yaml` file by replacing the `disallowed_clients` list
- Configurable update frequency via cron expression environment variable. - Creates a backup of the original config (`AdGuardHome.yaml.first-start.bak`) on first run
- Automatically restarts the AdGuard Home container after updates via Docker socket proxy. - Creates a backup before each update (`AdGuardHome.yaml.last-update.bak`)
- Backup `AdguardHome.yaml` at first startup, then create a second backup at each update. - Restarts the AdGuard Home container via Docker API
- Built-in Python scheduler using the `schedule` library, configurable to run updates daily or weekly
## Environment Variables ## Environment Variables
| Variable | Description | Default |
| ------------------- | ---------------------------------------------------------- | --------------------------------- |
| `TZ` | Your Time Zone | (required) |
| `BLOCK_COUNTRIES` | Comma-separated country codes to block (e.g., `CN,RU,IR`) | (required) |
| `BLOCKLIST_CRON` | Cron expression for update frequency (e.g., `0 6 * * *`) | `0 6 * * *` (at 6:00 everydays) |
| `DOCKER_API_URL` | URL of Docker socket proxy to restart AdGuard container | `http://socket-proxy-adguard:2375` |
| `ADGUARD_CONTAINER_NAME` | Name of your adguard container | `adguardhome` |
| Variable | Description | Example | Possible Values |
|--------------------------|--------------------------------------------------------------------------|-----------------------------|---------------------------------------------|
| `PUID` | User ID the process runs as (drops root at startup) | `1000` | Any valid numeric UID |
| `PGID` | Group ID the process runs as | `1000` | Any valid numeric GID |
| `TZ` | Timezone of the container to correctly schedule updates | `Europe/Paris` | Any valid timezone (e.g., `UTC`, `America/New_York`, etc.) |
| `BLOCK_COUNTRIES` | List of country codes for CIDR lists, separated by commas. You can also define an exclude list (all countries except the specified ones) by prefixing each country code with !. Mixing inclusion and exclusion codes is not supported. | including list : `cn,ru,ir`, excluding list : `!cn,!ru,!ir` | ISO 2-letter country codes |
| `BLOCKLIST_CRON_TYPE` | Scheduling type: `daily` or `weekly` | `daily` | `daily`, `weekly` |
| `BLOCKLIST_CRON_TIME` | Time to run update in `HH:MM` 24-hour format | `06:00` | 24-hour time format |
| `BLOCKLIST_CRON_DAY` | Day of the week for weekly schedule (e.g., `mon`, `tue`, etc.) | `mon` | `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun` |
| `ADGUARD_CONTAINER_NAME` | Name of the AdGuard Home container to restart | `adguardhome` | Valid Docker container name |
| `DOCKER_API_URL` | Docker API URL (used to restart the container) | `http://socket-proxy-adguard:2375` | HTTP URL |
## Volumes
- `/path/to/adguard/confdir` : configuration directory containing `AdGuardHome.yaml` from your adguard container, and optionally `manually_blocked_ips.conf`.
## File Structure ## File Structure
- `update-blocklist.sh`: Main script to download CIDRs, merge manual IPs, update config, and restart AdGuard. - `blocklist_scheduler.py`: Script to backup, schedule, download CIDRs, merge manual IPs, update config, and restart AdGuard.
- `entrypoint.sh`: Sets up the cron job to periodically run the update script. - `Dockerfile`: Builds the lightweight python3-slim image.
- `Dockerfile`: Builds the lightweight Alpine-based image.
- `docker-compose.yml`: Example compose file to run the container. - `docker-compose.yml`: Example compose file to run the container.
- `manually_blocked_ips.conf`: (Volume mount) Add extra IPs to block manually. - (optional) `manually_blocked_ips.conf`: Add extra IPs to block manually.
## Installation and Usage ## Installation and Usage
### With our docker image ### With our provided docker image
1. **Create `docker-compose.yml` in your `adguard-cidre` folder** 1. **Create `docker-compose.yml` in your `adguard-cidre` folder**
```yaml ```yaml
--- ---
services: services:
adguard-cidre: adguard-cidre:
image: git.djeex.fr/djeex/adguard-cidre:latest image: git.djeex.fr/djeex/adguard-cidre:latest
container_name: adguard-cidre container_name: adguard-cidre
restart: unless-stopped restart: unless-stopped
environment: environment:
- PUID=1000 # user id the process runs as, matches ownership of the /adguard mount
- PGID=1000 # group id the process runs as
- TZ=Europe/Paris # change to your timezone - TZ=Europe/Paris # change to your timezone
- BLOCK_COUNTRIES=cn,ru # choose countries listed IP to block. Full lists here https://github.com/vulnebify/cidre/tree/main/output/cidr/ipv4 - BLOCK_COUNTRIES=cn,ru # choose countries listed IP to block. Full lists here https://github.com/vulnebify/cidre/tree/main/output/cidr/ipv4
- BLOCKLIST_CRON=0 6 * * * # at 6:00 every days - BLOCKLIST_CRON_TYPE=daily # daily or weekly
# if weekly, choose the day
# - BLOCKLIST_CRON_DAY=mon
- BLOCKLIST_CRON_TIME=06:00
- DOCKER_API_URL=http://socket-proxy-adguard:2375 # docker socket proxy - DOCKER_API_URL=http://socket-proxy-adguard:2375 # docker socket proxy
- ADGUARD_CONTAINER_NAME=adguardhome # adguard container name - ADGUARD_CONTAINER_NAME=adguardhome # adguard container name
volumes: volumes:
- /path/to/adguard/confdir:/adguard - /path/to/adguard/confdir:/adguard
socket-proxy: socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest image: lscr.io/linuxserver/socket-proxy:latest
container_name: socket-proxy-adguard container_name: socket-proxy-adguard
security_opt: security_opt:
@@ -84,7 +97,7 @@
2. **Modify docker-compose.yml** 2. **Modify docker-compose.yml**
- Set `BLOCK_COUNTRIES` environment variable with the countries you want to block. - Set `BLOCK_COUNTRIES` environment variable with the countries you want to block.
- Adjust `BLOCKLIST_CRON` if you want a different update frequency. - Adjust `BLOCKLIST_CRON` variables if you want a different update frequency.
- Bind mount your adguard configuration folder (wich contains `AdGuardHome.yaml`) to `/adguard` - Bind mount your adguard configuration folder (wich contains `AdGuardHome.yaml`) to `/adguard`
- (optionnally) create and edit `manually_blocked_ips.conf` file in your adguard configuration folder to add other IPs you want to block. Only valid IP or CIDR entries will be processed, for exemple : - (optionnally) create and edit `manually_blocked_ips.conf` file in your adguard configuration folder to add other IPs you want to block. Only valid IP or CIDR entries will be processed, for exemple :
@@ -112,11 +125,12 @@
git clone https://git.djeex.fr/Djeex/adguard-cidre git clone https://git.djeex.fr/Djeex/adguard-cidre
cd adguard-cidre cd adguard-cidre
``` ```
2. **Modify docker-compose.yml** 2. **Edit the `.env` file**
- Set `BLOCK_COUNTRIES` environment variable with the countries you want to block. - A `.env` file is included at the repo root with all environment variables (see [Environment Variables](#environment-variables)). Edit values there instead of `docker-compose.yml`.
- Adjust `BLOCKLIST_CRON` if you want a different update frequency. - Set `BLOCK_COUNTRIES` with the countries you want to block.
- Bind mount your adguard configuration folder (wich contains `AdGuardHome.yaml`) to `/adguard` - Adjust `BLOCKLIST_CRON_*` variables if you want a different update frequency.
- Bind mount your adguard configuration folder (wich contains `AdGuardHome.yaml`) to `/adguard` in `docker-compose.yml`.
- (optionnally) create and edit `manually_blocked_ips.conf` file in your adguard configuration folder to add other IPs you want to block. Only valid IP or CIDR entries will be processed, for exemple : - (optionnally) create and edit `manually_blocked_ips.conf` file in your adguard configuration folder to add other IPs you want to block. Only valid IP or CIDR entries will be processed, for exemple :
```bash ```bash
@@ -128,7 +142,6 @@
3. **Build and start the container** 3. **Build and start the container**
```bash ```bash
docker compose build
docker compose up -d docker compose up -d
``` ```
4. **Check logs to verify updates** 4. **Check logs to verify updates**
+1
View File
@@ -0,0 +1 @@
1.5.0
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
import os
import sys
import logging
import requests
import yaml
import schedule
import time
import re
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
stream=sys.stdout,
)
ADGUARD_YAML = Path("/adguard/AdGuardHome.yaml")
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"
COUNTRY_LIST_URL = "https://raw.githubusercontent.com/vulnebify/cidre/refs/heads/main/cidre/countries.py"
FIRST_BACKUP = ADGUARD_YAML.parent / "AdGuardHome.yaml.first-start.bak"
LAST_UPDATE_BACKUP = ADGUARD_YAML.parent / "AdGuardHome.yaml.last-update.bak"
BLOCK_COUNTRIES = os.getenv("BLOCK_COUNTRIES", "")
BLOCKLIST_CRON_TYPE = os.getenv("BLOCKLIST_CRON_TYPE", "daily").lower()
BLOCKLIST_CRON_TIME = os.getenv("BLOCKLIST_CRON_TIME", "06:00")
BLOCKLIST_CRON_DAY = os.getenv("BLOCKLIST_CRON_DAY", "mon").lower()
ADGUARD_CONTAINER_NAME = os.getenv("ADGUARD_CONTAINER_NAME", "adguardhome")
DOCKER_API_URL = os.getenv("DOCKER_API_URL", "http://socket-proxy-adguard:2375")
def backup_first_start():
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.")
def backup_last_update():
logging.info(f"Creating last update backup: {LAST_UPDATE_BACKUP}")
LAST_UPDATE_BACKUP.write_text(ADGUARD_YAML.read_text())
def fetch_all_country_codes():
try:
resp = requests.get(COUNTRY_LIST_URL, timeout=15)
resp.raise_for_status()
matches = re.findall(r'"([A-Z]{2})"', resp.text)
return set(code.lower() for code in matches)
except Exception as e:
logging.error(f"Failed to fetch available country codes: {e}")
return set()
def get_selected_countries():
if not BLOCK_COUNTRIES:
logging.error("BLOCK_COUNTRIES is not set. Skipping update.")
return []
raw_codes = [c.strip() for c in BLOCK_COUNTRIES.split(",") if c.strip()]
if not raw_codes:
logging.error("No valid country codes provided.")
return []
is_exclusion = all(c.startswith("!") for c in raw_codes)
is_inclusion = all(not c.startswith("!") for c in raw_codes)
if not (is_exclusion or is_inclusion):
logging.error("Mixed syntax in BLOCK_COUNTRIES. Use only inclusion (e.g. 'fr,de') or only exclusion (e.g. '!fr,!de').")
sys.exit(1)
available = fetch_all_country_codes()
selected = {c.lstrip("!") for c in raw_codes}
unknown = selected - available
if unknown:
logging.warning(f"Unknown country codes: {', '.join(sorted(unknown))}")
if is_exclusion:
return sorted(available - selected)
else:
return sorted(selected & available)
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()
if line and (line.count('.') == 3 or '/' in line):
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):
if not ADGUARD_YAML.exists():
logging.error(f"{ADGUARD_YAML} does not exist. Cannot update.")
return False
try:
with ADGUARD_YAML.open() as f:
data = yaml.safe_load(f)
except Exception as e:
logging.error(f"Failed to parse YAML file: {e}")
return False
if not isinstance(data, dict):
logging.error("Invalid YAML format.")
return False
data['dns']['disallowed_clients'] = ips
with TMP_YAML.open('w') as f:
yaml.safe_dump(data, f)
TMP_YAML.replace(ADGUARD_YAML)
logging.info(f"Updated {ADGUARD_YAML} with new disallowed clients list.")
return True
def restart_adguard_container():
restart_url = f"{DOCKER_API_URL}/containers/{ADGUARD_CONTAINER_NAME}/restart"
logging.info(f"Restarting AdGuard container '{ADGUARD_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 update_blocklist():
countries = get_selected_countries()
if not countries:
logging.error("No valid countries to process. Skipping update.")
return
cidr_ips = download_cidr_lists(countries)
manual_ips = read_manual_ips()
combined_ips = cidr_ips + manual_ips
backup_last_update()
success = update_yaml_with_ips(combined_ips)
if success:
restart_adguard_container()
def schedule_job():
try:
hour, minute = [int(x) for x in BLOCKLIST_CRON_TIME.split(":")]
except Exception:
logging.error(f"Invalid BLOCKLIST_CRON_TIME '{BLOCKLIST_CRON_TIME}', must be HH:MM. Defaulting to 06:00.")
hour, minute = 6, 0
if BLOCKLIST_CRON_TYPE == "daily":
schedule.every().day.at(f"{hour:02d}:{minute:02d}").do(update_blocklist)
logging.info(f"Scheduled daily update at {hour:02d}:{minute:02d}")
elif BLOCKLIST_CRON_TYPE == "weekly":
day_names = {
"mon": "monday", "tue": "tuesday", "wed": "wednesday", "thu": "thursday",
"fri": "friday", "sat": "saturday", "sun": "sunday",
}
day = BLOCKLIST_CRON_DAY[:3]
if day not in day_names:
logging.error(f"Invalid BLOCKLIST_CRON_DAY '{BLOCKLIST_CRON_DAY}', must be one of {list(day_names)}. Defaulting to Monday.")
day = "mon"
getattr(schedule.every(), day_names[day]).at(f"{hour:02d}:{minute:02d}").do(update_blocklist)
logging.info(f"Scheduled weekly update on {day_names[day].capitalize()} at {hour:02d}:{minute:02d}")
else:
logging.error(f"Invalid BLOCKLIST_CRON_TYPE '{BLOCKLIST_CRON_TYPE}', must be 'daily' or 'weekly'. Defaulting to daily.")
schedule.every().day.at(f"{hour:02d}:{minute:02d}").do(update_blocklist)
logging.info(f"Scheduled daily update at {hour:02d}:{minute:02d}")
def main():
logging.info("Starting blocklist scheduler...")
backup_first_start()
update_blocklist()
schedule_job()
while True:
schedule.run_pending()
time.sleep(10)
if __name__ == "__main__":
main()
+10 -5
View File
@@ -5,11 +5,16 @@ services:
container_name: adguard-cidre container_name: adguard-cidre
restart: unless-stopped restart: unless-stopped
environment: environment:
- TZ=Europe/Paris # change to your timezone - PUID=${PUID} # user id the process runs as, matches ownership of the /adguard mount
- BLOCK_COUNTRIES=cn,ru # choose countries listed IP to block. Full lists here https://github.com/vulnebify/cidre/tree/main/output/cidr/ipv4 - PGID=${PGID} # group id the process runs as
- BLOCKLIST_CRON=0 6 * * * # at 6:00 every days - TZ=${TZ} # change to your timezone
- DOCKER_API_URL=http://socket-proxy-adguard:2375 # docker socket proxy - BLOCK_COUNTRIES=${BLOCK_COUNTRIES} # choose countries listed IP to block. Full lists here https://github.com/vulnebify/cidre/tree/main/output/cidr/ipv4
- ADGUARD_CONTAINER_NAME=adguardhome # adguard container name - BLOCKLIST_CRON_TYPE=${BLOCKLIST_CRON_TYPE} # daily or weekly
# if weekly, choose the day
- BLOCKLIST_CRON_DAY=${BLOCKLIST_CRON_DAY}
- BLOCKLIST_CRON_TIME=${BLOCKLIST_CRON_TIME}
- DOCKER_API_URL=${DOCKER_API_URL} # docker socket proxy
- ADGUARD_CONTAINER_NAME=${ADGUARD_CONTAINER_NAME} # adguard container name
volumes: volumes:
- /path/to/adguard/confdir:/adguard - /path/to/adguard/confdir:/adguard
-67
View File
@@ -1,67 +0,0 @@
#!/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()
Executable
+105
View File
@@ -0,0 +1,105 @@
#!/bin/sh
set -e
CYAN="\033[1;36m"
NC="\033[0m"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*"; }
fail() { echo "$(date '+%Y-%m-%d %H:%M:%S') [!] $*" >&2; exit 1; }
print_banner() {
version=$(cat VERSION 2>/dev/null || echo "unknown")
title="AdGuard CIDRe - Version ${version}"
lines="Source: https://git.djeex.fr/Djeex/adguard-cidre
Mirror: https://github.com/Djeex/adguard-cidre"
width=${#title}
old_ifs=$IFS
IFS='
'
for l in $lines; do
[ ${#l} -gt "$width" ] && width=${#l}
done
IFS=$old_ifs
width=$((width + 2))
border=""
i=0
while [ "$i" -lt "$width" ]; do
border="${border}"
i=$((i + 1))
done
printf "${CYAN}╭%s╮${NC}\n" "$border"
total_pad=$((width - ${#title}))
left=$((total_pad / 2))
right=$((total_pad - left))
printf "${CYAN}${NC}%*s%s%*s${CYAN}${NC}\n" "$left" "" "$title" "$right" ""
printf "${CYAN}├%s┤${NC}\n" "$border"
IFS='
'
for l in $lines; do
printf "${CYAN}${NC} %-*s${CYAN}${NC}\n" "$((width - 1))" "$l"
done
IFS=$old_ifs
printf "${CYAN}╰%s╯${NC}\n" "$border"
}
print_banner
PUID=${PUID:-911}
PGID=${PGID:-911}
case "$PGID" in
''|*[!0-9]*) fail "PGID '$PGID' is not a valid numeric group id." ;;
esac
case "$PUID" in
''|*[!0-9]*) fail "PUID '$PUID' is not a valid numeric user id." ;;
esac
[ -d /adguard ] || fail "/adguard is not mounted — check the volume mapping in docker-compose.yml."
log "[i] Requested PUID=$PUID, PGID=$PGID"
log "[~] Checking group for GID $PGID..."
GROUP_NAME=$(getent group "$PGID" | cut -d: -f1 || true)
if [ -z "$GROUP_NAME" ]; then
log "[→] No existing group with GID $PGID, creating 'appgroup'."
addgroup -g "$PGID" appgroup || fail "Failed to create group with GID $PGID (addgroup exited $?)."
GROUP_NAME=appgroup
else
log "[i] Reusing existing group '$GROUP_NAME' (GID $PGID)."
fi
log "[✓] Group ready: $GROUP_NAME"
log "[~] Checking user for UID $PUID..."
USER_NAME=$(getent passwd "$PUID" | cut -d: -f1 || true)
if [ -z "$USER_NAME" ]; then
log "[→] No existing user with UID $PUID, creating 'appuser'."
adduser -D -u "$PUID" -G "$GROUP_NAME" appuser || fail "Failed to create user with UID $PUID (adduser exited $?)."
USER_NAME=appuser
else
log "[i] Reusing existing user '$USER_NAME' (UID $PUID)."
fi
log "[✓] User ready: $USER_NAME"
# Grant write access to the shared AdGuard config directory and to the files
# this script manages, without touching anything else AdGuardHome owns in
# there (its own db/certs/stats). AdGuardHome itself runs as root, so this is
# a one-way grant: it keeps full access regardless of what we chown here.
log "[~] Setting ownership of /adguard to $USER_NAME:$GROUP_NAME..."
chown "$USER_NAME:$GROUP_NAME" /adguard || fail "chown on /adguard failed — check that the host directory permissions allow it."
log "[✓] Ownership set on /adguard"
for f in AdGuardHome.yaml AdGuardHome.yaml.first-start.bak AdGuardHome.yaml.last-update.bak AdGuardHome.yaml.tmp; do
if [ -e "/adguard/$f" ]; then
chown "$USER_NAME:$GROUP_NAME" "/adguard/$f" || fail "chown on /adguard/$f failed."
log "[✓] chown OK: /adguard/$f"
fi
done
log "[→] Dropping privileges to $USER_NAME:$GROUP_NAME and starting blocklist_scheduler.py"
exec su-exec "$USER_NAME:$GROUP_NAME" python3 blocklist_scheduler.py "$@"
+2
View File
@@ -0,0 +1,2 @@
[pytest]
pythonpath = .
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"timezone": "Europe/Paris",
"labels": ["bot"],
"packageRules": [
{
"matchManagers": ["pip_requirements"],
"matchUpdateTypes": ["patch", "minor"],
"automerge": true
},
{
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"addLabels": ["major"]
},
{
"matchUpdateTypes": ["minor"],
"addLabels": ["minor"]
}
],
"vulnerabilityAlerts": {
"enabled": true,
"addLabels": ["bug"]
}
}
+3
View File
@@ -0,0 +1,3 @@
requests==2.34.2
pyyaml==6.0.3
schedule==1.2.2
+210
View File
@@ -0,0 +1,210 @@
import pytest
import schedule as schedule_lib
import yaml
import blocklist_scheduler as bs
class FakeResponse:
def __init__(self, text="", status_code=200, raise_exc=None):
self.text = text
self.status_code = status_code
self._raise_exc = raise_exc
def raise_for_status(self):
if self._raise_exc:
raise self._raise_exc
def test_backup_first_start_creates_backup_when_missing(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("original: config\n")
first_backup = tmp_path / "AdGuardHome.yaml.first-start.bak"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "FIRST_BACKUP", first_backup)
bs.backup_first_start()
assert first_backup.read_text() == "original: config\n"
def test_backup_first_start_does_not_overwrite_existing_backup(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("new: config\n")
first_backup = tmp_path / "AdGuardHome.yaml.first-start.bak"
first_backup.write_text("pristine: original\n")
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "FIRST_BACKUP", first_backup)
bs.backup_first_start()
assert first_backup.read_text() == "pristine: original\n"
def test_backup_first_start_raises_if_adguard_yaml_missing(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
first_backup = tmp_path / "AdGuardHome.yaml.first-start.bak"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "FIRST_BACKUP", first_backup)
with pytest.raises(FileNotFoundError):
bs.backup_first_start()
# --- update_yaml_with_ips (pyyaml) ---
def test_update_yaml_with_ips_writes_disallowed_clients(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("dns:\n bind_hosts:\n - 0.0.0.0\n")
tmp_yaml = tmp_path / "AdGuardHome.yaml.tmp"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
monkeypatch.setattr(bs, "TMP_YAML", tmp_yaml)
result = bs.update_yaml_with_ips(["1.2.3.0/24", "5.6.7.8"])
assert result is True
data = yaml.safe_load(adguard_yaml.read_text())
assert data["dns"]["disallowed_clients"] == ["1.2.3.0/24", "5.6.7.8"]
assert not tmp_yaml.exists()
def test_update_yaml_with_ips_missing_file_returns_false(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
assert bs.update_yaml_with_ips(["1.2.3.4"]) is False
def test_update_yaml_with_ips_invalid_yaml_returns_false(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("key: [unclosed\n")
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
assert bs.update_yaml_with_ips(["1.2.3.4"]) is False
def test_update_yaml_with_ips_missing_dns_key_raises(tmp_path, monkeypatch):
adguard_yaml = tmp_path / "AdGuardHome.yaml"
adguard_yaml.write_text("some_other_key: true\n")
monkeypatch.setattr(bs, "ADGUARD_YAML", adguard_yaml)
with pytest.raises(KeyError):
bs.update_yaml_with_ips(["1.2.3.4"])
# --- fetch_all_country_codes / download_cidr_lists / restart_adguard_container (requests) ---
def test_fetch_all_country_codes_parses_codes(monkeypatch):
monkeypatch.setattr(bs.requests, "get", lambda *a, **k: FakeResponse(text='COUNTRIES = ["FR", "DE", "US"]\n'))
assert bs.fetch_all_country_codes() == {"fr", "de", "us"}
def test_fetch_all_country_codes_returns_empty_set_on_error(monkeypatch):
def raise_error(*a, **k):
raise bs.requests.exceptions.ConnectionError("boom")
monkeypatch.setattr(bs.requests, "get", raise_error)
assert bs.fetch_all_country_codes() == set()
def test_download_cidr_lists_combines_successful_countries_and_skips_failures(monkeypatch):
def fake_get(url, timeout=None):
if "/fr.cidr" in url:
return FakeResponse(text="1.1.1.0/24\n1.1.2.0/24\n")
raise bs.requests.exceptions.ConnectionError("boom")
monkeypatch.setattr(bs.requests, "get", fake_get)
result = bs.download_cidr_lists(["fr", "de"])
assert result == ["1.1.1.0/24", "1.1.2.0/24"]
def test_restart_adguard_container_success_does_not_raise(monkeypatch):
monkeypatch.setattr(bs.requests, "post", lambda *a, **k: FakeResponse(status_code=204))
bs.restart_adguard_container()
def test_restart_adguard_container_error_status_does_not_raise(monkeypatch):
monkeypatch.setattr(bs.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="err"))
bs.restart_adguard_container()
def test_restart_adguard_container_network_error_does_not_raise(monkeypatch):
def raise_error(*a, **k):
raise bs.requests.exceptions.ConnectionError("boom")
monkeypatch.setattr(bs.requests, "post", raise_error)
bs.restart_adguard_container()
# --- schedule_job (schedule) ---
@pytest.fixture(autouse=True)
def clear_schedule():
yield
schedule_lib.clear()
def test_schedule_job_daily(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "daily")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TIME", "06:00")
bs.schedule_job()
assert len(schedule_lib.jobs) == 1
job = schedule_lib.jobs[0]
assert job.unit == "days"
assert str(job.at_time) == "06:00:00"
assert job.job_func.func is bs.update_blocklist
def test_schedule_job_weekly_valid_day(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "weekly")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TIME", "18:30")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_DAY", "wed")
bs.schedule_job()
job = schedule_lib.jobs[0]
assert job.unit == "weeks"
assert job.start_day == "wednesday"
assert str(job.at_time) == "18:30:00"
def test_schedule_job_weekly_invalid_day_defaults_to_monday(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "weekly")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_DAY", "xxx")
bs.schedule_job()
assert schedule_lib.jobs[0].start_day == "monday"
def test_schedule_job_invalid_time_defaults_to_six_am(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "daily")
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TIME", "not-a-time")
bs.schedule_job()
assert str(schedule_lib.jobs[0].at_time) == "06:00:00"
def test_schedule_job_invalid_type_defaults_to_daily(monkeypatch):
monkeypatch.setattr(bs, "BLOCKLIST_CRON_TYPE", "bogus")
bs.schedule_job()
assert schedule_lib.jobs[0].unit == "days"
-148
View File
@@ -1,148 +0,0 @@
#!/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()