Simplify DDNS flow and polish UI

This commit is contained in:
StasonJatham committed 2026-05-21 14:18:39 +02:00
1 parent 5beee0e5c6
commit 5a90bd9d87
42 files changed
+587 -1418

No files matched your search

+14 -36
View File
@@ -2,7 +2,7 @@
**Self-hosted Dynamic DNS (DynDNS / DDNS) for FRITZ!Box, OpenWrt, pfSense, OPNsense, UniFi, MikroTik, Linux cron jobs, and any router that can call a custom DDNS update URL.**
KarlDNS is the friendly name for `karldns`: a small FastAPI Dynamic DNS provider you can run on your own VPS. It gives users accountless private update URLs, FRITZ!Box-ready DynDNS fields, public A/AAAA records through Cloudflare or RFC 2136, generated hostnames that work as CNAME targets, and verified custom domains with DNS TXT ownership checks.
KarlDNS is the friendly name for `karldns`: a small FastAPI Dynamic DNS provider you can run on your own VPS. It gives users accountless private update URLs, FRITZ!Box-ready DynDNS fields, public A/AAAA records through Cloudflare or RFC 2136, and generated hostnames that also work as CNAME targets for user-owned domains.
If you want a lightweight DuckDNS / Dynu / No-IP style service that you control yourself, KarlDNS is built for that use case.
@@ -13,8 +13,8 @@ If you want a lightweight DuckDNS / Dynu / No-IP style service that you control
- **Self-hosted Dynamic DNS provider** for home labs, small ISPs, communities, and private infrastructure.
- **FRITZ!Box compatible**: copy the generated `Update-URL`, `Domainnamen`, `Benutzername`, and `Kennwort` straight into the German FRITZ!Box DynDNS form.
- **CNAME-friendly hostnames**: users can use the generated hostname directly, or point `home.example.com` to it with a CNAME.
- **No registration required**: provider hostnames and custom-domain setup both use private magic links.
- **Custom domain support**: users enter a domain, save the private claim link, add a DNS TXT record, then press a button to verify ownership before credentials are issued.
- **No registration required**: generated hostnames use private magic links, with no accounts or login flow.
- **CNAME alias support**: users create one KarlDNS hostname, then point `home.example.com` to it at their own DNS provider.
- **Real DNS publishing** through **Cloudflare** or **RFC 2136** with A and AAAA support.
- **Small-server friendly**: FastAPI, SQLite WAL, simple Docker deployment, and no heavy background platform.
- **API first**: OpenAPI, Swagger UI, ReDoc, and JSON endpoints under `/api/v1`.
@@ -25,10 +25,6 @@ If you want a lightweight DuckDNS / Dynu / No-IP style service that you control
![Generated FRITZ!Box DynDNS credentials](docs/screenshots/generated-credentials.png)
### Verify a Custom Domain
![DNS TXT verification flow for custom domains](docs/screenshots/domain-verification.png)
## Logo And Social Preview
KarlDNS ships web-ready brand assets so self-hosted instances and shared links look clean out of the box:
@@ -47,7 +43,7 @@ KarlDNS ships web-ready brand assets so self-hosted instances and shared links l
KarlDNS lets you host your own managed DynDNS service:
1. A user opens your KarlDNS URL.
2. They generate a random hostname like `a1b2c3d4.karldns.de`, or verify their own domain with a TXT record.
2. They generate a random hostname like `a1b2c3d4.karldns.de`.
3. KarlDNS shows the exact router settings for the DynDNS form.
4. The router periodically calls the update URL with its current public IPv4/IPv6 address.
5. KarlDNS publishes the matching DNS records through your configured DNS backend.
@@ -60,7 +56,6 @@ That means users always have a current hostname for VPN, remote access, self-hos
- Self-hosted DynDNS for FRITZ!Box routers.
- Free DDNS service for friends, customers, a community, or a home lab.
- Dynamic DNS for IPv4 and IPv6 home internet connections.
- Custom domain DDNS with DNS TXT verification.
- CNAME target hostnames for users who want `home.example.com` without sharing DNS API access.
- Lightweight Cloudflare DDNS provider with a web UI.
- RFC 2136 / TSIG DynDNS frontend for BIND, Knot, PowerDNS, or compatible DNS servers.
@@ -73,10 +68,10 @@ That means users always have a current hostname for VPN, remote access, self-hos
| DNS records | A and AAAA |
| DNS providers | Cloudflare API and RFC 2136 dynamic DNS |
| Generated hostnames | Random provider-owned names that can be used directly or as CNAME targets |
| Custom domains | TXT challenge verification before hostname creation |
| User-owned DNS names | CNAME only. KarlDNS publishes the generated provider-owned hostname; the user's DNS provider serves the alias |
| Persistence | SQLite with WAL mode |
| API | FastAPI, OpenAPI, Swagger UI, ReDoc |
| Auth model | No user accounts. Private bearer links for hostnames and custom-domain claims |
| Auth model | No user accounts. Private bearer links for hostnames and dashboards |
| Operations | Rate limiting, admin audit views, update logs, cleanup jobs, Docker, Caddy example |
## Quick Start
@@ -109,7 +104,6 @@ Open:
- ReDoc: `http://localhost:8080/redoc`
Admin auth uses HTTP Basic auth. The username can be any value; the password is `DDNS_ADMIN_PASSWORD`.
`DDNS_SHARED_SECRET`, when configured for legacy updater clients, is not accepted for admin routes.
The public OpenAPI schema documents the developer API under `/api/v1`. Admin/operator endpoints are intentionally omitted from Swagger UI and ReDoc.
@@ -146,7 +140,7 @@ export DDNS_RFC2136_KEY_SECRET='replace-with-tsig-secret'
export DDNS_TTL=60
```
`DDNS_DNS_ZONES` is the allowlist of DNS zones this service may publish. A custom hostname must be inside one of these zones.
`DDNS_DNS_ZONES` is the allowlist of DNS zones this service may publish. For the public KarlDNS model this is normally only your provider zone, such as `karldns.de`.
## FRITZ!Box DynDNS Setup
@@ -175,7 +169,7 @@ This URL is enough for routers that only expose a single update URL field and do
The compatibility endpoint is also available:
```text
https://karldns.de/nic/update?hostname=<domain>&myip=<ipaddr>&myipv6=<ip6addr>&username=<username>&password=<pass>
https://karldns.de/nic/update?hostname=<generated-hostname>&myip=<ipaddr>&myipv6=<ip6addr>&username=<username>&password=<pass>
```
Prefer `/u/<slug>` for managed service use because the router request cannot change the hostname.
@@ -198,7 +192,7 @@ curl -sS 'https://karldns.de/u/<random-update-slug>'
The `/nic/update` compatibility endpoint also works for clients that expect hostname and credentials in the URL:
```bash
curl -sS 'https://karldns.de/nic/update?hostname=home.example.net&username=router&password=<password>&myip=203.0.113.10'
curl -sS 'https://karldns.de/nic/update?hostname=a1b2c3d4.karldns.de&username=router&password=<password>&myip=203.0.113.10'
```
### Linux Cron Installer
@@ -282,7 +276,7 @@ curl -fsSL https://karldns.de/install.sh | sudo sh -s -- --ipv6 off
## Use Your Own Subdomain With CNAME
Most users do not need domain verification. Create a KarlDNS hostname first, then create a CNAME at your DNS provider:
To use your own domain, create a KarlDNS hostname first, then create a CNAME at your DNS provider:
```dns
home.example.com. CNAME a1b2c3d4.karldns.de.
@@ -292,21 +286,9 @@ Put the KarlDNS `Update-URL`, `Domainnamen`, `Benutzername`, and `Kennwort` into
Use a subdomain such as `home.example.com`. Root/apex domains like `example.com` usually cannot be plain CNAME records unless your DNS provider supports CNAME flattening or ALIAS/ANAME records.
The generated hostname is not a secret. Treat it like any public DNS name. The update URL slug, router password, private dashboard URL, and domain claim link are the secrets. Dashboard forms use CSRF tokens, and dashboard password unlock cookies last 24 hours by default unless the user chooses the 30-day stay-unlocked option.
KarlDNS does not verify, claim, or publish records inside user-owned domains. Your DNS provider keeps serving `home.example.com`; KarlDNS only maintains the generated `*.karldns.de` target.
## Custom Domain Flow
This flow is only needed when KarlDNS should publish A/AAAA records directly inside a user-owned domain instead of using a CNAME target.
Custom-domain publishing is intentionally simple:
1. Enter the domain.
2. Save the private claim link.
3. Add the generated TXT record at your DNS provider.
4. Press **I added it, check DNS**.
5. Create router credentials after the TXT record verifies.
KarlDNS stores the domain claim in SQLite and only creates hostnames below verified, publishable zones. Claims created from a hostname dashboard are bound to that dashboard owner, so another dashboard cannot verify or use them. The private claim link is the bearer credential for returning later after DNS propagation.
The generated hostname is not a secret. Treat it like any public DNS name. The update URL slug, router password, private dashboard URL, and full credentialed update URL are the secrets. Dashboard forms use CSRF tokens, and dashboard password unlock cookies last 24 hours by default unless the user chooses the 30-day stay-unlocked option.
## HTTP API
@@ -317,9 +299,6 @@ The JSON API is available under `/api/v1`.
| `POST /api/v1/hostnames/magic` | Create an anonymous provider-owned hostname. |
| `GET /api/v1/management/{management_slug}` | Inspect a hostname by private management link. |
| `DELETE /api/v1/management/{management_slug}` | Delete a hostname and its DNS records. |
| `POST /api/v1/domains/challenges` | Create a TXT challenge and private claim secret. |
| `POST /api/v1/domains/verify` | Verify a TXT challenge with the claim secret. |
| `POST /api/v1/hostnames/custom` | Create a custom hostname below a verified domain with the claim secret. |
| `GET /api/v1/updates/{update_slug}` | JSON update endpoint for routers or automation. |
Example:
@@ -374,7 +353,6 @@ Security and cleanup defaults:
| `DDNS_DASHBOARD_COOKIE_SECRET` | `DDNS_ADMIN_PASSWORD` fallback | HMAC signing secret for dashboard unlock cookies. Use a separate 32+ character random value in production. |
| `DDNS_REDACTED_ACCESS_LOG` | `0` | Enable KarlDNS request logs with bearer paths and update query strings redacted. |
| `DDNS_TRUSTED_HOSTS` | from `DDNS_PUBLIC_BASE_URL` | Host header allowlist. Add every public hostname that should serve the app. |
| `DDNS_CLEANUP_CHALLENGE_HOURS` | `72` | Delete unverified DNS TXT claims after this many hours. |
| `DDNS_CLEANUP_UNUSED_ACCOUNT_HOURS` | `1080` | Delete generated hostnames that never received a router update after this many hours. |
| `DDNS_CLEANUP_EVENT_HISTORY_HOURS` | `4320` | Delete old raw update/audit events after this many hours. Rollups and recent events remain visible. |
| `DDNS_CLEANUP_INTERVAL_SECONDS` | `3600` | Store the cleanup task cadence in SQLite and schedule the next cleanup run at this interval. |
@@ -403,7 +381,7 @@ Backend layout:
| `router_dyndns/dependencies.py` | Shared FastAPI dependencies for app state, admin auth, update auth, and router update responses. |
| `router_dyndns/schemas.py` | Pydantic request and response models used by the JSON API. |
| `router_dyndns/web/` | HTML rendering, form parsing, and security-header/body-limit support code. |
| `router_dyndns/ddns_service.py` | Domain logic for account creation, verification, and IP updates. |
| `router_dyndns/ddns_service.py` | Domain logic for account creation and IP updates. |
| `router_dyndns/store/` | SQLite persistence, rate limits, durable scheduled tasks, cleanup, and event history. |
| `router_dyndns/ddns_store.py` | Compatibility export for `DdnsStore`. |
| `router_dyndns/dns/` | Cloudflare and RFC 2136 DNS publishing. |
@@ -421,7 +399,7 @@ Self-hosted DynDNS, self hosted DDNS, Dynamic DNS server, FRITZ!Box DynDNS, FRIT
## Security
The repository intentionally contains only placeholders in `.env.example` and documentation. Do not commit a real `.env`, DNS token, TSIG secret, admin password, database, private status page URL, domain claim URL, or router-generated update URL. Generated hostnames and CNAME targets are public identifiers, not authentication secrets.
The repository intentionally contains only placeholders in `.env.example` and documentation. Do not commit a real `.env`, DNS token, TSIG secret, admin password, database, private dashboard URL, or router-generated update URL. Generated hostnames and CNAME targets are public identifiers, not authentication secrets.
KarlDNS does not need secret-bearing query strings in application logs. The bundled server command leaves Uvicorn access logs disabled by default; configure reverse-proxy logs the same way or redact full request URLs.