mirror of
https://github.com/atoy3731/adguard-sync.git
synced 2024-09-20 06:56:01 +08:00
first commit
This commit is contained in:
commit
2bc3aa12f0
35
.github/workflows/docker-publish.yaml
vendored
Normal file
35
.github/workflows/docker-publish.yaml
vendored
Normal 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
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
venv/
|
||||
*.pyc
|
||||
.idea/
|
||||
*.iml
|
33
README.md
Normal file
33
README.md
Normal 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.
|
15
build/Dockerfile
Normal file
15
build/Dockerfile
Normal 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
168
build/app.py
Normal 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
1
build/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
requests
|
19
docker-compose.yaml
Normal file
19
docker-compose.yaml
Normal 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
|
Loading…
Reference in a new issue