This repository has been archived on 2026-10-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
karldns.de/README.md
T

4.6 KiB

router-dyndns

A small self-hosted DynDNS service for FRITZ!Box and compatible routers.

It gives users FRITZ!Box-ready DynDNS settings without requiring a full account for provider-owned hostnames. For custom domains, users verify domain ownership with a DNS TXT challenge before creating hostnames.

Features

  • FastAPI app with /docs, /redoc, and /openapi.json.
  • Anonymous magic URLs for provider-owned hostnames.
  • Optional user sessions for custom-domain verification and hostname quotas.
  • FRITZ!Box-compatible update URLs.
  • SQLite persistence with WAL mode.
  • SQLite-backed rate limiting for public creation and update endpoints.
  • Cloudflare DNS publishing.
  • RFC 2136 DNS publishing.
  • DNS publish-before-store update behavior.
  • Per-host update locking inside one server process.
  • Custom-domain TXT verification.
  • Trusted proxy configuration for X-Forwarded-For.
  • Docker and Caddy examples.

Install

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Run quality checks:

ruff check router_dyndns tests
pytest

Run Locally

export DDNS_ADMIN_PASSWORD='long-random-admin-password'
export DDNS_SESSION_SECRET='another-long-random-secret'
export DDNS_PUBLIC_BASE_URL='https://ddns.example.net'
export DDNS_HOSTNAME_SUFFIX='ddns.example.net'
export DDNS_DATABASE='./ddns.sqlite3'
export DDNS_DNS_PROVIDER='cloudflare'
export DDNS_DNS_ZONES='ddns.example.net'
export DDNS_REQUIRE_DNS_PROVIDER=1
export DDNS_CLOUDFLARE_API_TOKEN='cloudflare-api-token'
export DDNS_CLOUDFLARE_ZONE_ID='cloudflare-zone-id'
export DDNS_TRUSTED_PROXY_IPS='127.0.0.1,::1'

router-dyndns serve --host 0.0.0.0 --port 8080

Open:

  • Customer UI: https://ddns.example.net/
  • Admin UI: https://ddns.example.net/admin
  • Swagger UI: https://ddns.example.net/docs
  • ReDoc: https://ddns.example.net/redoc

Admin auth uses HTTP Basic auth. Use any username and DDNS_ADMIN_PASSWORD as the password.

FRITZ!Box Setup

Users receive these fields:

  • Update-URL
  • Domainnamen
  • Benutzername
  • Kennwort

The generated update URL uses FRITZ!Box placeholders:

/u/<random-update-slug>?myip=<ipaddr>&myipv6=<ip6addr>

The compatibility endpoint is also available:

/nic/update?hostname=<domain>&myip=<ipaddr>&myipv6=<ip6addr>&username=<username>&password=<pass>

The /u/<slug> endpoint is safer for public service use because the user cannot alter the hostname in the router request.

HTTP API

The API is versioned under /api/v1.

Core endpoints:

  • POST /api/v1/hostnames/magic: create a provider-owned random hostname.
  • GET /api/v1/management/{management_slug}: inspect a hostname by private management link.
  • DELETE /api/v1/management/{management_slug}: delete a hostname and DNS records.
  • POST /api/v1/domains/challenges: create a TXT challenge. Requires login.
  • POST /api/v1/domains/verify: verify a TXT challenge. Requires login.
  • POST /api/v1/hostnames/custom: create a custom hostname below a verified and publishable domain. Requires login.
  • GET /api/v1/updates/{update_slug}: JSON update endpoint.

Example:

curl -sS https://ddns.example.net/api/v1/hostnames/magic \
  -H 'content-type: application/json' \
  -d '{"username":"home-router"}'

DNS Publishing

For public service use, configure a real DNS backend and set:

export DDNS_REQUIRE_DNS_PROVIDER=1

Cloudflare:

export DDNS_DNS_PROVIDER=cloudflare
export DDNS_DNS_ZONES=ddns.example.net
export DDNS_CLOUDFLARE_API_TOKEN='cloudflare-api-token'
export DDNS_CLOUDFLARE_ZONE_ID='cloudflare-zone-id'
export DDNS_TTL=60

RFC 2136:

export DDNS_DNS_PROVIDER=rfc2136
export DDNS_DNS_ZONES=ddns.example.net
export DDNS_RFC2136_SERVER=127.0.0.1
export DDNS_RFC2136_ZONE=ddns.example.net
export DDNS_RFC2136_KEY_NAME=ddns-key
export DDNS_RFC2136_KEY_SECRET='base64-tsig-secret'
export DDNS_TTL=60

DDNS_DNS_ZONES defines which DNS zones this service may publish. A user proving TXT control of a domain is not enough; the hostname must also be inside a zone your DNS backend can update.

Docker

docker compose up -d --build

Copy .env.example to .env and set real secrets first.

Operations

  • Run behind HTTPS.
  • Use long random values for DDNS_ADMIN_PASSWORD and DDNS_SESSION_SECRET.
  • Configure DDNS_TRUSTED_PROXY_IPS only for reverse proxies that strip user-supplied forwarding headers.
  • Back up the SQLite database.
  • Run one Uvicorn worker with SQLite. The per-host update lock is process-local.

SQLite backup example:

sqlite3 /data/ddns.sqlite3 ".backup '/backups/router-dyndns.sqlite3'"