Packages
defdo_ddns
0.5.0
Cloudflare DDNS updater for A/AAAA/CNAME records with proxy-aware synchronization.
Current section
Files
Jump to
Current section
Files
defdo_ddns
CHANGELOG.md
CHANGELOG.md
# 0.5.0
## β¨ Features
- **A provisioned record is managed from birth.** `POST /v1/dns/upsert` wrote to
Cloudflare and stopped there, so the monitor never knew the record existed.
The next inventory reported it as unmanaged and an operator had to *adopt* a
record the platform itself had just created β the API was manufacturing
exactly the drift adoption exists to clean up, once per provisioned tenant.
Upsert now declares the record in desired state in the same call, and the
response carries `declared: true|false` so a caller can tell.
Declaring is never fatal. The DNS record already exists by that point, so a
failure to record it is logged and reported, not turned into a failed
response for an operation that succeeded. A deployment with no desired-state
file configured is a supported configuration and simply reports
`declared: false`.
## π§Ή Internal
- `Defdo.DDNS.DesiredStateStore.declare/1` is now the single definition of what
declaring a record means. Adoption's promotion path used to carry a private
copy of the entry shape; two copies would eventually disagree about what
"already declared" means, and the same record could be stored twice or fail to
match itself on the next reconcile.
- `declare/1` starts from an empty document when the file is absent. `load/0`
deliberately refuses to do this β for a read, treating "no file" as "no
intent" would silently unmanage the whole estate β but a declaration adds a
fact rather than inferring the absence of one, and without it the first record
a fresh deployment provisions would be dropped in silence.
## π Security
- Cleared all 28 advisories `mix hex.audit` reported. Most were removed rather
than upgraded: `tesla` and a second `mint` came from `restlax` and
`cloudflare`, neither declared in `mix.exs`; `bypass` pulled
`plug_cowboy β cowboy β cowlib` β a whole second HTTP server β for tests that
never used it. `req` was pinned `~> 0.5.0`, which locked out every fix, and
now tracks `~> 0.6`.
# 0.4.3
## π Fixes
- **Adoption persists by default.** With no `DDNS_ADOPTION_PATH` configured, the
store silently no-oped: `refresh` reported records added while `list` returned
nothing, because writes went nowhere. It now defaults to
`/var/lib/defdo_ddns/adoption.json` β the same volume the record snapshot uses
β so a deployment that mounts it keeps decisions without configuring another
path.
# 0.4.2
## β¨ New Features
- **Adoption discovery over the API.** `POST /v1/adoption/refresh` (body
`{"domain": "..."}`) runs an inventory and populates the pending list. The
release image has no `mix`, so without this the pending list could never be
filled where DDNS actually runs β list/accept/reject had nothing to act on.
Read-only against Cloudflare, same auth as the other adoption endpoints.
# 0.4.1
## π Fixes
- **Desired state seeds itself on first read.** With `DDNS_DESIRED_STATE_PATH`
set but no file yet, `DesiredStateStore.load/0` now writes the file from the
environment config on the first read instead of returning
`:missing_desired_state`. The release image runs no `mix`, so there was no way
for an operator to create the file, and `accept` (over the API) would have
failed against a store that could never be initialized. Still fails loudly when
there is genuinely nothing to seed from, and still never overwrites an existing
file.
# 0.4.0
## β¨ New Features
- **Record adoption.** DDNS can now see records that exist in Cloudflare but were
never declared here, and bring them under management through an explicit
decision. The live `defdo.ninja` zone carries 33 such records against the dozen
the config declares; until now nothing reported that gap, and nothing could
converge it.
- `Reconcile.Inventory` classifies a zone's live records as managed, unmanaged
or missing. Read-only.
- `Adoption` holds undeclared records as `pending` until an operator accepts or
rejects them. Discovery is automatic; adoption never is. Rejections are
durable, so a refused record never asks to be adopted again.
- Accepting promotes the record into desired state atomically β from there the
monitor converges it. A failed promotion rolls the decision back rather than
marking a record adopted that nothing manages.
- Operator surface: `mix defdo.ddns.adoption.{refresh,list,accept,reject}`,
plus `GET/POST /v1/adoption[/:id/{accept,reject}]` behind the existing API
gate. Targets are redacted in listings.
- **File-backed desired state.** `DesiredStateStore` moves DNS intent out of
environment variables into a canonical file (`DDNS_DESIRED_STATE_PATH`). The
file is authoritative once written; env becomes a one-time seed. Disabled
unless the path is set, so adoption is opt-in per deployment. Kept distinct
from the runtime record snapshot: intent and observation are not the same
thing.
# 0.3.4
## π Fixes
- **Cloudflare edge errors no longer shut the application down.** Cloudflare
returns 520-527 failures as a plain-text or HTML page rather than the
documented JSON envelope. The client indexed into that body directly, so a
single transient `error code: 521` raised, killed the monitor, and β after the
supervisor restarted it into the same failing call and exhausted its restart
intensity β terminated the whole application, which then stayed down silently.
Zone lookups, record listing, creation and updates now validate the response
before reading it and degrade to their documented return value with a log line.
- `get_zone_id/1` no longer raises when Cloudflare returns an empty result list
for an unknown domain.
- The monitor now rescues per-domain and per-checkup, so an unexpected failure is
logged and retried on the next tick instead of terminating the process.
# Unreleased
## β¨ New Features
- Added safe runtime diagnostics for DDNS record store metadata, counts, source, and persistence flags.
- Hardened bootstrap behavior for empty-state handling and malformed snapshot/init sources.
- Documented release/TrueNAS record-store operations and adapter boundaries.
# 0.3.3
## β¨ New Features
- Added portable DDNS snapshot codec (`Defdo.DDNS.RecordSnapshot`) for canonical record normalization, JSON encode/decode, and legacy CNAME seed conversion.
- Added runtime record store APIs for release-safe export and persistence:
- `Defdo.DDNS.RecordStore.list_records/1`
- `Defdo.DDNS.RecordStore.export_snapshot/1`
- `Defdo.DDNS.RecordStore.write_snapshot/2`
- `Defdo.DDNS.RecordStore.persist/1`
- `Defdo.DDNS.RecordStore.reload/1`
- Added ETS-backed file store bootstrap precedence for DDNS records:
- snapshot file
- init file
- legacy CNAME env seed
- explicit empty state when allowed
## π Bug Fixes
- Moved DDNS record snapshot serialization out of the store adapter so future host apps can reuse the same snapshot/export primitives for database-backed adapters.
## π Documentation
- Updated README to document runtime record store behavior, snapshot/init paths, and the legacy status of `CLOUDFLARE_CNAME_RECORDS_JSON`.
# 0.3.2
## β¨ New Features
- Added multi-tenant-light API auth mode with client credentials and domain scope enforcement.
- New runtime config: `DDNS_API_CLIENTS_JSON`.
- New runtime flag: `DDNS_API_ALLOW_RUNTIME_CLIENTS` (deny-all bootstrap until runtime client injection).
- Added ETS-backed in-memory auth store (`Defdo.DDNS.API.AuthStore`) for fast per-request client lookup.
- Added public runtime API for embedded integrations:
- `Defdo.DDNS.set_api_clients/1`
- `Defdo.DDNS.clear_api_clients/0`
- `Defdo.DDNS.api_clients/0`
## π Bug Fixes
- Hardened API auth token verification with timing-safe comparison.
- Added API auth fail-fast startup validation:
- when `DDNS_API_ENABLED=true`, requires `DDNS_API_TOKEN` or `DDNS_API_CLIENTS_JSON`
- unless `DDNS_API_ALLOW_RUNTIME_CLIENTS=true`.
- Fixed DNS API params parsing to preserve explicit `proxied=false` in upsert payloads.
- Enforced `base_domain` allowlist validation in client-auth mode before DNS upsert execution.
## π Documentation
- Expanded README with:
- multi-tenant-light auth config and request headers
- `defdo_auth` integration guidance (HTTP and embedded runtime injection modes).
# 0.3.1
## π Bug Fixes
- Fixed `A/AAAA` auto-create behavior when a hostname already has non-IP records (`TXT`, `MX`, etc.):
- the monitor now creates missing `A/AAAA` by record type and only skips when a conflicting `CNAME` exists.
# 0.3.0
## β¨ New Features
- Added optional embedded HTTP API (Bandit):
- `GET /health` for liveness checks.
- `POST /v1/dns/upsert` for authenticated CNAME upserts.
- Added public dependency facade `Defdo.DDNS`:
- One-shot/manual checkup (`checkup_once/0`) and monitor-aware `checkup/0`.
- Runtime helpers for monitor lifecycle and configuration checks.
- Added monitor runtime controls:
- `DDNS_ENABLE_MONITOR` to disable background monitor startup.
- `DDNS_REFETCH_EVERY_MS` to tune check interval without code changes.
## π Bug Fixes
- Added one-shot monitor execution path (`Defdo.Cloudflare.Monitor.checkup_once/0`) so checkups can run without a running monitor process.
- Hardened monitor tests to avoid external network/API dependency during test runs.
- Fixed domain mapping normalization for root marker `@`:
- `@` is now treated as the zone root (e.g. `paridin.net`) instead of generating invalid names like `@.paridin.net`.
## π¦ Packaging & Release
- Added Apache License 2.0 file (`LICENSE.md`).
- Added GitHub Actions workflow to validate and publish tagged releases to Hex (`.github/workflows/publish_hex.yml`).
- Completed Hex package metadata and docs setup (`description`, `package`, `docs`, `ex_doc`).
## π Documentation
- Updated README with dependency/API usage and Bandit runtime configuration.
# 0.2.1
## β¨ New Features
- Added `CLOUDFLARE_PROXY_EXCLUDE` to keep selected hostnames in `DNS only` even when `CLOUDFLARE_PROXY_A_RECORDS=true`.
- Supports exact hostnames and wildcard suffix patterns (for example `*.idp-dev.defdo.ninja`).
- Added `CLOUDFLARE_PROXY_A_RECORDS` to force Cloudflare proxy mode (`proxied=true`) for `A/AAAA` records during updates and auto-creation.
- Added `CLOUDFLARE_A_RECORDS_JSON` with JSON-first parsing for A monitor targets.
- Supports object and array JSON formats for easier machine-generated config.
- Falls back to legacy `CLOUDFLARE_DOMAIN_MAPPINGS` when JSON is empty or invalid.
- Added `CLOUDFLARE_AAAA_RECORDS_JSON` for real IPv6 record synchronization.
- AAAA records now use detected public IPv6 independently from A record IPv4 updates.
- Supports object and array JSON formats, matching `CLOUDFLARE_A_RECORDS_JSON`.
- Added declarative CNAME management via `CLOUDFLARE_CNAME_RECORDS_JSON`.
- Supports `name`, `target`, optional `proxied`, `ttl`, and optional `domain` scope per record.
- Enables wildcard/alias record management using a plain-text env var (JSON string), without requiring a database.
- Added domain posture output in monitor logs:
- `[HEALTH][GREEN|YELLOW|RED]` summary per domain with SSL mode, proxied/dns-only counts, and hairpin risk.
- Added Cloudflare edge SSL mode checks (`strict/full/flexible/off`) to improve runtime diagnostics.
- Added ACM advisory warnings for deep hostnames that may require Cloudflare Advanced Certificate Manager when proxied.
- Logs now include `[CERT][ACM]` hints and recommendations.
## π Bug Fixes
- Improved proxy toggle safety when exclusions are used:
- Excluded records now resolve desired `proxied=false` during updates.
- Normalized TTL when switching from proxied to DNS-only:
- Records with Cloudflare auto TTL (`1`) are converted to a standard DNS-only TTL (`300`) to avoid invalid DNS-only state.
- Normalized logs for container dashboards: removed ANSI/emoji log output and disabled console colors to improve log readability.
- Removed Cloudflare `modified_on` timestamp from success log lines to avoid broken rendering in dashboards that misparse ISO8601 text.
- When `CLOUDFLARE_PROXY_A_RECORDS=true`, records with correct IP but `proxied=false` are now patched to enable proxy mode.
- Set explicit logger format without leading blank line (`$time [$level] $message`) to improve compatibility with log viewers.
- Relative wildcard entries in `CLOUDFLARE_DOMAIN_MAPPINGS` (e.g. `*.idp-dev`, `*.rnu`) are now expanded to the current zone (e.g. `*.idp-dev.defdo.ninja`).
- Proxy-mode updates now enforce Cloudflare Auto TTL (`ttl=1`) for proxied records, preventing update failures on some wildcard records.
- Improved Cloudflare API error handling for DNS create/update calls to log errors whenever the API returns `success=false` (including `result=nil` responses).
- Duplicate A/AAAA records for the same `name+type` are now handled safely: if one record is already in desired state, conflicting updates are skipped and a warning with record IDs is logged.
- Improved auto-create safety:
- When a hostname is declared in `CLOUDFLARE_CNAME_RECORDS_JSON`, monitor now skips auto-creating `A` records for that same name to avoid `A/CNAME` conflicts.
- Improved auto-create logic for IP records:
- Missing `A` and `AAAA` records are now created independently based on configured mappings and detected address family availability.
- Added graceful handling when a zone ID cannot be resolved before DNS record operations.
- Fixed release boot crash caused by runtime helper loading:
- Moved configuration parsing helper to compiled app code (`Defdo.ConfigHelper`) so releases do not depend on external `.exs` files at runtime.
## π Documentation
- Expanded README with:
- Cloudflare SSL/TLS mode behavior (`Flexible`, `Full`, `Full (strict)`).
- Orange-cloud vs gray-cloud behavior and hairpin implications.
- New `CLOUDFLARE_PROXY_EXCLUDE` configuration examples.
- Health posture and ACM warning log interpretation.
- Updated roadmap to reflect completed work and next priorities.
# 0.2.0
## π¨ Breaking Changes
- **Environment Variable Changes**:
- `CLOUDFLARE_DOMAIN` and `CLOUDFLARE_SUBDOMAINS` have been replaced with `CLOUDFLARE_DOMAIN_MAPPINGS`
- New format: `domain1.com:subdomain1,subdomain2;domain2.com:api,blog;domain3.com:`
- Migration required: Update your environment variables to use the new mapping format
- **Configuration Format**:
- Domain-to-subdomain mapping now uses a structured format instead of separate variables
- Root domain monitoring requires explicit empty subdomain list (e.g., `example.com:`)
## β¨ New Features
- **Multiple Domain Support**: Monitor and update DNS records for multiple domains simultaneously
- **Auto-creation**: New `AUTO_CREATE_DNS_RECORDS` option to automatically create missing DNS records
- **Promotional Comments**: Auto-created DNS records include promotional comments
- **Improved Logging**: Enhanced status messages with emojis and clearer error reporting
- **Better Error Handling**: More robust error handling and validation
## π Bug Fixes
- Fixed duplicate function definitions causing compilation warnings
- Resolved configuration parsing issues in runtime.exs
- Improved DNS record validation and creation logic
## π Documentation
- Complete README rewrite with step-by-step setup instructions
- Added troubleshooting section with common issues
- Docker Compose and UniFi Dream Machine Pro examples
- Clear migration guide for breaking changes
# 0.1.1
- Support for multiple domains
- Subdomains must be specific otherwise they are added to the monitor domain even if they didn't exists.
# 0.1.0
- Support for only one domain
- Bootstrap defdo_ddns