diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md index e69de29..b7b4319 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,9 @@ +# pihole-opnsense-sync +_Sync custom DNS entries in Pi-hole to OPNsense Unbound._ + +## Install +```shell +pip install -r requirements.txt +``` + +Then get your API auth details. diff --git a/pihole-opnsense-sync.service b/pihole-opnsense-sync.service new file mode 100644 index 0000000..03bb6cc --- /dev/null +++ b/pihole-opnsense-sync.service @@ -0,0 +1,12 @@ +[Unit] +Description=Sync Pi-hole local DNS to OPNsense Unbound + +[Service] +User=pihole +Group=pihole +ExecStart=/opt/icinga2-checks/Other/auto-acknowledge-apt.sh --api https://localhost:5665 --fail --user icingaweb2 --password XXXXX +SyslogIdentifier=pihole-opnsense-sync +Restart=always + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7a069ed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests==2.31.0 +watchdog==4.0.0 \ No newline at end of file diff --git a/syncer.py b/syncer.py new file mode 100755 index 0000000..00df610 --- /dev/null +++ b/syncer.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import argparse +import logging +import time +from datetime import datetime +from pathlib import Path + +import requests +import urllib3 +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer + +urllib3.disable_warnings() + + +class WatchdogHandler(FileSystemEventHandler): + def __init__(self, args): + self.args = args + + def on_modified(self, event): + if event.src_path == args.custom_path: + file_modified(args) + + +def file_modified(args): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + response = requests.get(args.opnsense + "/api/unbound/settings/searchHostOverride", auth=(args.api_key, args.api_secret), verify=args.insecure) + + if response.status_code == 200: + data = response.json() + current_overrides = set([(x['server'], x['domain']) for x in data.get('rows', []) if x['description'] != 'SAVE']) + current_overrides_uuid = {x['domain']: x for x in data.get('rows', [])} + pihole_custom = set([(x[0], x[1]) for x in [y.split(' ') for y in Path(args.custom_path).read_text().split('\n')] if len(x) > 1]) + + to_remove = current_overrides - pihole_custom + to_add = pihole_custom - current_overrides + + for item_ip, item_domain in to_remove: + item_data = current_overrides_uuid[item_domain] + p = requests.post(args.opnsense + f"/api/unbound/settings/delHostOverride/{item_data['uuid']}", auth=(args.api_key, args.api_secret), verify=args.insecure) + if p.status_code != 200: + raise Exception(f'Failed to delete item {item_domain}: {p.status_code} - {p.text}') + else: + logger.info(f'Deleted: {item_domain}') + + now = datetime.now() + dt_string = now.strftime("%m/%d/%Y %H:%M:%S") + + for item_ip, item_domain in to_add: + p_data = { + 'host': { + 'enabled': '1', + 'hostname': '', + 'domain': item_domain, + 'rr': 'A', + 'mxprio': '', + 'mx': '', + 'server': item_ip, + 'description': f'Synced from Pi-hole {dt_string}' + } + } + p = requests.post(args.opnsense + "/api/unbound/settings/addHostOverride", json=p_data, auth=(args.api_key, args.api_secret), verify=args.insecure) + if p.status_code != 200: + raise Exception(f'Failed to add item {item_domain}: {p.status_code} - {p.text}') + else: + logger.info(f'Added: {item_domain}') + else: + raise Exception(f"Failed to fetch host overrides: {response.status_code} - {response.text}") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Sync custom DNS entries in Pi-hole to OPNsense Unbound.') + parser.add_argument('--opnsense', required=True, help='Address of your OPNsense server. Example: https://192.168.1.1') + parser.add_argument('--custom-path', default='/etc/pihole/custom.list', help="Path to Pi-hole's custom.list. Default: /etc/pihole/custom.list") + parser.add_argument('--api-key', required=True, help='OPNsense API key.') + parser.add_argument('--api-secret', required=True, help='OPNsense API secret.') + parser.add_argument('--insecure', action='store_true', help="Don't verify SSL.") + args = parser.parse_args() + + event_handler = WatchdogHandler(args) + observer = Observer() + observer.schedule(event_handler, path='/etc/pihole/', recursive=False) + observer.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join()