Metadata-Version: 2.4
Name: octodns-adguard
Version: 0.1.2
Summary: AdGuard Home DNS rewrites provider for octoDNS
License-Expression: MIT
Project-URL: Homepage, https://gitlab.com/davinkevin.fr/projects/octodns-adguard
Project-URL: Source, https://gitlab.com/davinkevin.fr/projects/octodns-adguard
Project-URL: Issues, https://gitlab.com/davinkevin.fr/projects/octodns-adguard/-/issues
Keywords: dns,octodns,dns-as-code,adguard,adguard-home
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: Name Service (DNS)
Classifier: Topic :: System :: Systems Administration
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: octodns>=1.5.0
Requires-Dist: requests>=2.26.0
Dynamic: license-file

# octodns-adguard

An [octoDNS](https://github.com/octodns/octodns) provider that syncs a YAML source of
truth to the DNS rewrites of an [AdGuard Home](https://adguard.com/adguard-home.html)
instance.

octoDNS owns the rewrites of the zones you declare, and only those. Rewrites belonging to
any other zone are never read, never changed and never deleted, so the AdGuard web UI
stays usable in parallel for everything octoDNS does not manage.

## Support Information

### Records

| Type    | Notes                                                             |
| ------- | ----------------------------------------------------------------- |
| `A`     | Multi-valued; each value becomes one AdGuard rewrite              |
| `AAAA`  | Same                                                              |
| `CNAME` | Single-valued, as AdGuard stores one answer per rewrite           |

Records are identified by the `(name, type)` pair, so an `A` and an `AAAA` on the same
name coexist without being confused for one another. Apex (`''`) and wildcard (`*`) names
are supported.

Anything else AdGuard can hold is out of scope: rewrites are not a zone file, and there is
no way to express an `NS`, `MX` or `TXT` record through them.

### Dynamic

Dynamic records are not supported. A rewrite is a single unconditional answer, with no
weighting, no geo or subnet targeting and no health checking to build a pool on, so there
is nothing for octoDNS to map a dynamic record onto.

### Limitations

- `list_zones()` is not implemented. AdGuard has no notion of a zone — the rewrite list is
  flat — so [dynamic zone configuration](https://github.com/octodns/octodns/blob/main/docs/dynamic_zone_config.md)
  is unavailable. List your zones explicitly.
- A `CNAME` at the apex is invalid in octoDNS and needs `lenient`, even though AdGuard is
  happy to store one.
- Rewrites whose answer is the special value `A` or `AAAA` are blocking exceptions, not
  records. They are skipped on read and left untouched.
- A rewrite disabled from the web UI is still reported as present, since AdGuard identifies
  an entry by its `(domain, answer)` pair alone and its add endpoint does not deduplicate.
  Its disabled state is preserved across updates.

## Installation

Requires Python 3.11 or newer and octoDNS 1.5 or newer.

### Command line

```bash
uv add octodns-adguard
```

`pip install octodns-adguard` works the same way, and a `requirements.txt` entry is just
`octodns-adguard`.

### Pinning

Pinning a version is recommended to avoid unplanned upgrades:

```bash
uv add "octodns-adguard==0.1.2"
```

### From Git

To run ahead of a release, install the branch instead — pin a SHA, since `main` moves:

```bash
uv add "git+https://gitlab.com/davinkevin.fr/projects/octodns-adguard.git@<sha>"
```

You do not have to install anything if you only want to run a sync: see
[running from the container image](#running-from-the-container-image).

## Configuration

```yaml
providers:
  adguard:
    class: octodns_adguard.AdGuardProvider
    url: env/ADGUARD_URL
    username: env/ADGUARD_USERNAME
    password: env/ADGUARD_PASSWORD
```

| Option        | Required | Default | Description                                          |
| ------------- | -------- | ------- | ---------------------------------------------------- |
| `url`         | yes      |         | Base URL of the instance, e.g. `http://10.0.0.2:3000` |
| `username`    | yes      |         | Admin user; AdGuard has no service account           |
| `password`    | yes      |         | Admin password, sent as HTTP basic auth              |
| `default_ttl` | no       | `3600`  | See below                                            |
| `timeout`     | no       | `5`     | Per-request timeout, in seconds                      |

### About `default_ttl`

AdGuard stores no TTL, but octoDNS requires one on every record. `default_ttl` is the
synthetic value reported by `populate`. You do not have to match it in your YAML: a change
that only moves the TTL is filtered out of the plan, because it is not something this
provider could ever apply.

## Bootstrapping from a live instance

Point `octodns-dump` at the provider to turn what AdGuard already holds into YAML, rather
than transcribing it by hand:

```bash
octodns-dump --config-file config/production.yaml \
             --output-dir config/ \
             example.com. adguard
```

Review the generated `config/example.com.yaml`, wire it in as the source for that zone,
then check that octoDNS agrees with reality before handing it the keys:

```bash
octodns-sync --config-file config/production.yaml   # should report no change
```

## Running from the container image

Every version tag publishes an image built on the upstream octoDNS one, with this provider
already installed. Running a sync is then an image reference and a mounted config, nothing
else:

```bash
docker run --rm -v "$PWD/config:/config" \
  registry.gitlab.com/davinkevin.fr/projects/octodns-adguard:v0.1.0 \
  octodns-sync --config-file /config/production.yaml --doit
```

`octodns-dump` is called the same way. What stays outside the image is your `config.yaml`
and your zone YAML: they belong to your repository, mounted or cloned by whatever runs the
sync.

There is no `latest` tag, deliberately: an image reference should always name a version, so
that a cluster never moves without a commit to blame. The image is built for `linux/amd64`,
and the project being public it pulls without credentials — a Kubernetes CronJob needs no
`imagePullSecret`.

The image runs as uid `65534` (`nobody`), so it is accepted by a pod that sets
`runAsNonRoot: true`. A sync needs nothing writable, but `octodns-dump --output-dir` does:
the directory you mount for it has to be writable by that uid. In exchange, the files it
writes come out owned by you rather than by root, which on a Linux host is what you want.

## Development

Toolchain, tests, build scans and the release process are described in
[CONTRIBUTING.md](CONTRIBUTING.md). In short: `mise install && uv sync`, then `uv run pytest`.
