first commit

This commit is contained in:
Adam Toy 2021-01-31 12:46:48 -05:00
commit 2bc3aa12f0
8 changed files with 276 additions and 0 deletions

35
.github/workflows/docker-publish.yaml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Docker
on:
push:
branches:
- master
env:
IMAGE_NAME: atoy3731/adguard-sync
jobs:
push:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
# Build: Build the Docker image with a temporary tag.
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
# Login: Log into Docker Hub using Github secrets.
- name: Log into Docker
env:
DOCKER_USER: ${{ secrets.DOCKER_USER }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: echo "$DOCKER_PASSWORD" | docker login -u $DOCKER_USER --password-stdin
# Push: Retag the Docker image and push it to Docker Hub.
- name: Push image to DockerHub
run: |
VERSION=$(cat VERSION)
docker tag $IMAGE_NAME $IMAGE_NAME:$VERSION
docker push $IMAGE_NAME:$VERSION

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
venv/
*.pyc
.idea/
*.iml

33
README.md Normal file
View file

@ -0,0 +1,33 @@
# AdGuard Sync
This project will sync entries between two Primary and Secondary AdGuard Home instances using the API.
This is useful if you're dependent on local DNS and want to ensure relative High Availability.
### How to Run
AdGuard Sync is packaged as a Docker image and can be ran anywhere with access to your instances. You can update the `docker-compose.yaml` file with your values based on the following:
| Variable | Required | Description | Default |
|---|---|---|---|
| ADGUARD_PRIMARY | Yes | Primary base URL for the primary AdGuard instance (Ie. http://dns01.example.com) | N/A |
| ADGUARD_SECONDARY | Yes | Secondary base URL for the primary AdGuard instance (Ie. http://dns02.example.com) | N/A |
| ADGUARD_USER | Yes | Username to log into your AdGuard instances. | N/A |
| ADGUARD_PASS | Yes | Password to log into your AdGuard instances. | N/A |
| SECONDARY_ADGUARD_USER | No | Username to log into your secondary AdGuard instance. Only necessary if credentials are different between primary and secondary | Value of 'ADGUARD_USER' |
| SECONDARY_ADGUARD_PASS | No | Password to log into your secondary AdGuard instance. Only necessary if credentials are different between primary and secondary | Value of 'ADGUARD_PASSWORD' |
| REFRESH_INTERVAL_SECS | No | Frequency in seconds to refresh entries. | 60 |
Once you've updated the file and ensure you have `docker` and `docker-compose` installed, run the following in the root directory:
```bash
docker-compose up -d
```
You can check on the status of your newly running pod with:
```bash
docker-compose logs
```
**NOTE:** The container is set to automatically restart when the docker daemon restarts.

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.0

15
build/Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM alpine:3.13
RUN apk update && \
apk add python3 curl && \
curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python3 && \
apk del curl
COPY requirements.txt /tmp/requirements.txt
RUN pip3 install -r /tmp/requirements.txt && \
rm -f /tmp/requirements.txt
COPY app.py /opt/app.py
CMD [ '/usr/bin/python3', '/opt/app.py' ]

168
build/app.py Normal file
View file

@ -0,0 +1,168 @@
import requests
import os
import json
import time
ADGUARD_PRIMARY = os.environ['ADGUARD_PRIMARY']
ADGUARD_SECONDARY = os.environ['ADGUARD_SECONDARY']
ADGUARD_USER = os.environ['ADGUARD_USER']
ADGUARD_PASS = os.environ['ADGUARD_PASS']
# Optional, use if your secondary AdGuard has different credentials
SECONDARY_ADGUARD_USER = os.environ.get('SECONDARY_ADGUARD_USER', ADGUARD_USER)
SECONDARY_ADGUARD_PASS = os.environ.get('SECONDARY_ADGUARD_PASS', ADGUARD_PASS)
REFRESH_INTERVAL_SECS = int(os.environ.get('REFRESH_INTERVAL_SECS', '30'))
class UnauthenticatedError(Exception):
pass
def get_login_cookie(url, user, passwd):
"""
Logs into AdGuard URL using username/password and returns a valid session cookie.
:param url: Base URL of AdGuard
:param user: Username of AdGuard
:param passwd: Password of AdGuard
:return: Session token
"""
creds = {
'name': user,
'password': passwd
}
response = requests.post('{}/control/login'.format(url), data=json.dumps(creds))
return response.cookies['agh_session']
def get_entries(url, cookie):
"""
Retrieves all existing entries from AdGuard.
:param url: Base AdGuard URL
:param cookie: Session token
:return: Dict of Entries
"""
cookies = {
'agh_session': cookie
}
response = requests.get('{}/control/rewrite/list'.format(url), cookies=cookies)
if response.status_code == 403:
raise UnauthenticatedError
entry_array = json.loads(response.text)
entry_dict = {}
for e in entry_array:
entry_dict[e['domain']] = e['answer']
return entry_dict
def update_entries(url, cookie, sync_entries):
"""
Update entries from your primary to secondary AdGuard.
ADD: Will add the entry with the domain pointing to IP.
UPDATE: Will update existing entry to point the domain to the new IP.
DEL: Will delete the existing entry from secondary AdGuard.
:param url: URL of the Secondary AdGuard
:param cookie: Secondary AdGuard Auth Cookie.
:param sync_entries: Array of entries to be sync.
:return: None
"""
cookies = {
'agh_session': cookie
}
for entry in sync_entries:
if entry['action'] == 'ADD':
print(" - Adding entry ({} => {})".format(entry['domain'], entry['answer']))
data = {
'domain': entry['domain'],
'answer': entry['answer']
}
response = requests.post('{}/control/rewrite/add'.format(url), cookies=cookies, data=json.dumps(data))
if response.status_code == 403:
raise UnauthenticatedError
elif entry['action'] == 'DEL':
print(" - Deleting entry ({} => {})".format(entry['domain'], entry['answer']))
data = {
'domain': entry['domain'],
'answer': entry['answer']
}
response = requests.post('{}/control/rewrite/delete'.format(url), cookies=cookies, data=json.dumps(data))
if response.status_code == 403:
raise UnauthenticatedError
elif entry['action'] == 'UPDATE':
print(" - Updating entry ({} => {})".format(entry['domain'], entry['new_answer']))
old_data = {
'domain': entry['domain'],
'answer': entry['old_answer']
}
new_data = {
'domain': entry['domain'],
'answer': entry['new_answer']
}
response = requests.post('{}/control/rewrite/delete'.format(url), cookies=cookies, data=json.dumps(old_data))
if response.status_code == 403:
raise UnauthenticatedError
response = requests.post('{}/control/rewrite/delete'.format(url), cookies=cookies, data=json.dumps(new_data))
if response.status_code == 403:
raise UnauthenticatedError
if __name__ == '__main__':
print("Running Adguard Sync for '{}' => '{}'..".format(ADGUARD_PRIMARY, ADGUARD_SECONDARY))
# Get initial login cookie
primary_cookie = get_login_cookie(ADGUARD_PRIMARY, ADGUARD_USER, ADGUARD_PASS)
secondary_cookie = get_login_cookie(ADGUARD_SECONDARY, SECONDARY_ADGUARD_USER, SECONDARY_ADGUARD_PASS)
while True:
try:
primary_entries = get_entries(ADGUARD_PRIMARY, primary_cookie)
secondary_entries = get_entries(ADGUARD_SECONDARY, secondary_cookie)
sync_entries = []
for pk, pv in primary_entries.items():
if pk not in secondary_entries:
sync_entries.append({
'action': 'ADD',
'domain': pk,
'answer': pv
})
elif secondary_entries[pk] != pv:
sync_entries.append({
'action': 'UPDATE',
'domain': pk,
'old_answer': secondary_entries[pk],
'new_answer': pv
})
for sk, sv in secondary_entries.items():
if sk not in primary_entries:
sync_entries.append({
'action': 'DEL',
'domain': sk,
'answer': sv
})
update_entries(ADGUARD_SECONDARY, secondary_cookie, sync_entries)
except UnauthenticatedError:
primary_cookie = get_login_cookie(ADGUARD_PRIMARY, ADGUARD_USER, ADGUARD_PASS)
secondary_cookie = get_login_cookie(ADGUARD_SECONDARY, SECONDARY_ADGUARD_USER, SECONDARY_ADGUARD_PASS)
time.sleep(REFRESH_INTERVAL_SECS)

1
build/requirements.txt Normal file
View file

@ -0,0 +1 @@
requests

19
docker-compose.yaml Normal file
View file

@ -0,0 +1,19 @@
version: "3"
services:
adguard-sync:
image: atoy3731/adguard-sync:1.0
container_name: adguard-sync
restart: always
environment:
# Required variables
- ADGUARD_PRIMARY=http://dns01.example.com
- ADGUARD_SECONDARY=http://dns02.example.com
- ADGUARD_USER=admin
- ADGUARD_PASS=password
# Optional variables
# - SECONDARY_ADGUARD_USER=other_admin
# - SECONDARY_ADGUARD_PASS=other_password
# - REFRESH_INTERVAL_SECS=30