diff --git a/README.md b/README.md new file mode 100644 index 0000000..6678705 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Technitium DDNS + +Python utility that fetches the host's public IPv4 and IPv6 and updates `A` and `AAAA` records for a given hostname in a zone hosted on a Technitium DNS server. + +This repository contains a single service that supports two update backends: + +- **HTTP_API** — use Technitium's HTTP API (token-based) to add/overwrite records. +- **RFC2136** — use standard DNS dynamic updates (RFC2136), optionally authenticated with a TSIG key. + +The service runs in a continuous loop by default and updates every `UPDATE_INTERVAL` seconds (configurable via environment variables). + +## Quick start + +1. Create `.env` and fill in secrets and other variables. +2. Build and run: + +```bash +docker compose build +docker compose up -d +``` + +Or to run with Docker directly: + +```bash +docker build -t technitium-ddns . +docker run -d --name technitium-ddns --env-file .env technitium-ddns +``` + +## Environment variables + +You can set these in `.env`, via `docker-compose.yml`, or when running the container. + +General: +- `LOG_LEVEL` - `DEBUG`, `INFO`, `WARNING`, `ERROR` or `FATAL`. +- `LOG_FILE` - path of the log file or empty to log to stdout/stderr. +- `BACKEND` — `HTTP_API` (default) or `RFC2136`. +- `RUN_ONCE` — `1` to run a single update and exit, `0` to loop continuously (default `0`). +- `UPDATE_INTERVAL` — seconds between updates when looping (default `300`). +- `ZONE` — DNS zone to update the record into. +- `RECORD_NAME` — record name (left part) — the script updates `.`. +- `TTL` — record TTL in seconds (default `300`). +- `SKIP_IPV6` — set to `1` to skip AAAA updates. + +HTTP API backend (Technitium): + +- `TDNS_HOST` — Technitium base URL. +- `TDNS_API_TOKEN` — API token generated in Technitium web console (**required** for `HTTP_API`). + +RFC2136 backend: + +- `RFC2136_SERVER` — authoritative DNS server IP or hostname for dynamic updates (required for `RFC2136`). +- `RFC2136_PORT` — port (default `53`). +- `RFC2136_TSIG_NAME` — optional TSIG key name. +- `RFC2136_TSIG_KEY` — optional TSIG key (base64). +- `RFC2136_TSIG_ALGO` — optional TSIG algorithm (default `hmac-sha256.`). + +## Example `.env` + +```ini +# Log level and log file +LOG_LEVEL=INFO +LOG_FILE=/var/log/technitium-ddns.log + +# Choose HTTP_API or RFC2136 +BACKEND=HTTP_API + +# If RUN_ONCE=1 the container runs once and exits (good for host cron). +RUN_ONCE=0 +UPDATE_INTERVAL=300 + +ZONE=example.com +RECORD_NAME=ddns +TTL=300 +SKIP_IPV6=0 + +# HTTP API (Technitium) +TDNS_HOST=http://technitium:5380 +TDNS_API_TOKEN=YOUR_TECHNITIUM_TOKEN_HERE + +# RFC2136 (if using RFC2136 backend) +RFC2136_SERVER=192.0.2.5 +RFC2136_PORT=53 +# Optional TSIG: +RFC2136_TSIG_NAME=ddns-key +RFC2136_TSIG_KEY=base64encodedkey== +RFC2136_TSIG_ALGO=hmac-sha256. +``` + +## How it behaves + +- On each run (single-run or loop iteration) the script: + 1. Detects public IPv4 (via `api.ipify.org`, fallback to other services). + 2. Detects public IPv6 (unless `SKIP_IPV6=1`). + 3. Queries current `A` / `AAAA` values; if they already match the current public IP, update is skipped. + 4. Otherwise updates the record(s) using the selected backend. +- `HTTP_API` backend calls Technitium endpoint `/api/zones/records/add` with `overwrite=true`. +- `RFC2136` backend issues a dynamic DNS update packet and can use TSIG for authentication. + +## Running as a cron job on the host (recommended for many setups) + +If you prefer not to run a continuously-looping container, set `RUN_ONCE=1` and run the container from a host cron or systemd timer: + +Example host systemd timer unit (recommended): + +```ini +# /etc/systemd/system/tdns-updater-run.service +[Unit] +Description=Run tdns-updater once + +[Service] +Type=oneshot +WorkingDirectory=/path/to/project +ExecStart=/usr/bin/docker compose run --rm tdns-updater +``` + +```ini +# /etc/systemd/system/tdns-updater-run.timer +[Unit] +Description=Run tdns-updater every 5 minutes + +[Timer] +OnBootSec=1min +OnUnitActiveSec=5min + +[Install] +WantedBy=timers.target +``` + +## RFC2136 notes + +- Many servers expect dynamic updates to come from an allowed IP or require TSIG authentication. If using Technitium as the RFC2136 target, check its dynamic update / ACL settings and configure TSIG if required. +- The script supports TSIG via `RFC2136_TSIG_NAME` and `RFC2136_TSIG_KEY` (base64). Provide `RFC2136_TSIG_ALGO` like `hmac-sha256.` if needed. + +## TLS / self-signed certs + +If your `TDNS_HOST` is `https://` with a self-signed certificate, mount CA certificate(s) into the container and set `REQUESTS_CA_BUNDLE` or `SSL_CERT_FILE` appropriately, or use valid certificates to avoid modifying the image: + +Example `docker-compose.yml` snippet for a CA bundle file: + +```yaml + volumes: + - ./my-ca.pem:/usr/local/share/ca-certificates/my-ca.crt:ro +``` + +(then update CA store inside the image at build time, or pass `REQUESTS_CA_BUNDLE` pointing to the file.) + +## Security + +- Treat `TDNS_API_TOKEN` and `RFC2136_TSIG_KEY` as secrets. Do not commit them to VCS. +- For production, use Docker secrets, environment injection from your orchestrator, or mount a read-only file containing secrets. diff --git a/src/technitium-ddns.py b/src/technitium-ddns.py index da07e88..64d9d3c 100644 --- a/src/technitium-ddns.py +++ b/src/technitium-ddns.py @@ -157,7 +157,6 @@ def rfc2136_update(record_type: str, ip: str) -> Tuple[bool, Any]: def query_current_records(record_type: str) -> list: try: resolver = dns.resolver - resolver.nameservers = ['192.168.178.2'] answers = resolver.resolve(FULL_DOMAIN, record_type, lifetime=5) return [r.to_text() for r in answers] except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout, dns.resolver.NoNameservers):