Archived
Build KarlDNS self-hosted DynDNS service
This commit is contained in:
1 parent
45bc38d950
commit
ca92dc211a
65 files changed
+9200
-3570
No files matched your search
@@ -69,8 +69,8 @@ That means users always have a current hostname for VPN, remote access, self-hos
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/StasonJatham/router-dyndns.git
|
||||
cd router-dyndns
|
||||
git clone https://github.com/StasonJatham/karldns.de.git
|
||||
cd karldns.de
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e ".[dev]"
|
||||
@@ -80,6 +80,7 @@ Run locally:
|
||||
|
||||
```bash
|
||||
export DDNS_ADMIN_PASSWORD='replace-with-a-long-random-value'
|
||||
export DDNS_ADMIN_PATH='/admin'
|
||||
export DDNS_PUBLIC_BASE_URL='http://localhost:8080'
|
||||
export DDNS_HOSTNAME_SUFFIX='karldns.de'
|
||||
export DDNS_DATABASE='./ddns.sqlite3'
|
||||
@@ -95,6 +96,7 @@ 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.
|
||||
|
||||
@@ -103,34 +105,24 @@ The public OpenAPI schema documents the developer API under `/api/v1`. Admin/ope
|
||||
For an internet-facing DynDNS service, run behind HTTPS and require a real DNS backend:
|
||||
|
||||
```bash
|
||||
export DDNS_ADMIN_PASSWORD='replace-with-a-long-random-value'
|
||||
export DDNS_ADMIN_PATH='/admin'
|
||||
export DDNS_PUBLIC_BASE_URL='https://karldns.de'
|
||||
export DDNS_HOSTNAME_SUFFIX='karldns.de'
|
||||
export DDNS_DATABASE='/data/ddns.sqlite3'
|
||||
export DDNS_TRUSTED_HOSTS='karldns.de'
|
||||
export DDNS_REQUIRE_DNS_PROVIDER=1
|
||||
export DDNS_DNS_ZONES='karldns.de'
|
||||
export DDNS_TRUSTED_PROXY_IPS='127.0.0.1,::1'
|
||||
export DDNS_RATE_LIMIT_PER_MINUTE=60
|
||||
export DDNS_ADMIN_RATE_LIMIT_PER_MINUTE=20
|
||||
export DDNS_MAX_REQUEST_BODY_BYTES=16384
|
||||
export DDNS_CLEANUP_CHALLENGE_HOURS=72
|
||||
export DDNS_CLEANUP_UNUSED_ACCOUNT_HOURS=1080
|
||||
export DDNS_CLEANUP_INTERVAL_SECONDS=3600
|
||||
```
|
||||
|
||||
### Cloudflare DNS Backend
|
||||
|
||||
```bash
|
||||
export DDNS_DNS_PROVIDER='cloudflare'
|
||||
export DDNS_CLOUDFLARE_API_TOKEN='replace-with-cloudflare-token'
|
||||
export DDNS_CLOUDFLARE_ZONE_ID='replace-with-cloudflare-zone-id'
|
||||
export DDNS_TTL=60
|
||||
```
|
||||
|
||||
Use a least-privilege Cloudflare API token limited to the zone KarlDNS should update.
|
||||
That is the normal Cloudflare setup. Use a least-privilege Cloudflare API token limited to the zone KarlDNS should update. `DDNS_TRUSTED_HOSTS` is optional because KarlDNS automatically trusts the host in `DDNS_PUBLIC_BASE_URL`; set it only when the same instance should answer for additional public hostnames.
|
||||
|
||||
### RFC 2136 DNS Backend
|
||||
|
||||
Use this instead of the Cloudflare variables when your DNS server supports RFC 2136 dynamic updates:
|
||||
|
||||
```bash
|
||||
export DDNS_DNS_PROVIDER='rfc2136'
|
||||
export DDNS_RFC2136_SERVER='127.0.0.1'
|
||||
@@ -195,6 +187,85 @@ The `/nic/update` compatibility endpoint also works for clients that expect host
|
||||
curl -sS 'https://karldns.de/nic/update?hostname=home.example.net&username=router&password=<password>&myip=203.0.113.10'
|
||||
```
|
||||
|
||||
### Linux Cron Installer
|
||||
|
||||
For Linux servers, Proxmox hosts, NAS boxes, and small home-lab machines that are always on, KarlDNS ships a cron updater. It creates a hostname if you do not pass one, installs a tiny updater script, schedules it with cron, runs the first update, and prints the hostname plus private dashboard URL.
|
||||
|
||||
System-wide install:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://karldns.de/install.sh | sudo sh
|
||||
```
|
||||
|
||||
The same script is hosted in GitHub and can be used directly:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/StasonJatham/karldns.de/main/scripts/install-linux.sh | sudo sh
|
||||
```
|
||||
|
||||
User-level install without sudo:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://karldns.de/install.sh | sh -s -- --mode user
|
||||
```
|
||||
|
||||
Use an existing KarlDNS update URL:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://karldns.de/install.sh | sudo sh -s -- \
|
||||
--update-url 'https://karldns.de/u/<random-update-slug>' \
|
||||
--hostname 'home.karldns.de'
|
||||
```
|
||||
|
||||
Common options:
|
||||
|
||||
```bash
|
||||
--every 5 # cron interval in minutes, default 5
|
||||
--ipv4 auto # auto, off, or a static IPv4
|
||||
--ipv6 auto # auto, off, or a static IPv6
|
||||
--no-cron # install files only
|
||||
--no-run # skip the first update
|
||||
```
|
||||
|
||||
The updater discovers the public address through `https://karldns.de/ip` and sends it to the private update URL. If explicit discovery fails, it falls back to calling the update URL without IP parameters so KarlDNS can use the request source address. Re-running the installer is safe: it replaces the managed cron block and keeps unrelated cron entries intact.
|
||||
|
||||
Installed files in system mode:
|
||||
|
||||
```text
|
||||
/usr/local/bin/karldns-update
|
||||
/etc/karldns/updater.env
|
||||
/etc/cron.d/karldns-updater
|
||||
/var/cache/karldns/last-ip
|
||||
```
|
||||
|
||||
Installed files in user mode:
|
||||
|
||||
```text
|
||||
~/.local/bin/karldns-update
|
||||
~/.config/karldns/updater.env
|
||||
~/.cache/karldns/last-ip
|
||||
```
|
||||
|
||||
`updater.env` is written with mode `600` because it contains the private update URL and dashboard URL. The hostname itself is not secret. The update URL and dashboard URL are secrets.
|
||||
|
||||
Uninstall, keeping the private config by default:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://karldns.de/uninstall.sh | sudo sh
|
||||
```
|
||||
|
||||
Remove the private config and last-IP cache too:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://karldns.de/uninstall.sh | sudo sh -s -- --delete-config
|
||||
```
|
||||
|
||||
For Proxmox, install on the host if the host owns the public connection. If you install inside an LXC that has no IPv6 route, use `--ipv6 off` so the cron job only updates A records:
|
||||
|
||||
```bash
|
||||
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. Generate a KarlDNS hostname first, then create a CNAME at your DNS provider:
|
||||
@@ -207,7 +278,7 @@ 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 status page URL, and domain claim link are the secrets.
|
||||
The generated hostname is not a secret. Treat it like any public DNS name. The update URL slug, router password, private status page 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.
|
||||
|
||||
## Custom Domain Flow
|
||||
|
||||
@@ -221,7 +292,7 @@ Custom-domain publishing is intentionally simple:
|
||||
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. The private claim link is the bearer credential for returning later after DNS propagation.
|
||||
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.
|
||||
|
||||
## HTTP API
|
||||
|
||||
@@ -254,6 +325,12 @@ cp .env.example .env
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
The example environment is intentionally short: admin password/path, public URL, managed hostname suffix, Cloudflare token, Cloudflare zone ID, and the SQLite path. The app creates the SQLite parent directory and all tables on first run. Re-running the container or starting the app twice is idempotent; schema creation uses `CREATE TABLE IF NOT EXISTS`, and the cleanup scheduler keeps a single durable task row.
|
||||
|
||||
The image and compose file default `DDNS_DATABASE` to `/data/ddns.sqlite3`, and the compose volume persists `/data`.
|
||||
|
||||
The default container command runs one Uvicorn worker. Keep that deployment shape unless you have tested multi-worker update behavior for your traffic pattern; rate-limit counters and per-host update locks are process-local.
|
||||
|
||||
`Caddyfile.example` contains a minimal HTTPS reverse proxy example.
|
||||
|
||||
## Operations Checklist
|
||||
@@ -261,13 +338,15 @@ docker compose up -d --build
|
||||
- Run behind HTTPS.
|
||||
- Keep `.env` out of git.
|
||||
- Use a long random value for `DDNS_ADMIN_PASSWORD`.
|
||||
- Change `DDNS_ADMIN_PATH` if you do not want the operator UI at `/admin`.
|
||||
- Use a least-privilege DNS API token or TSIG key.
|
||||
- Set `DDNS_TRUSTED_HOSTS` to the public hostnames your reverse proxy will forward, for example `karldns.de`.
|
||||
- Set `DDNS_TRUSTED_HOSTS` only when the app should answer for hostnames beyond `DDNS_PUBLIC_BASE_URL`.
|
||||
- Configure `DDNS_TRUSTED_PROXY_IPS` only for reverse proxies that strip user-supplied forwarding headers.
|
||||
- Keep `/docs` and `/redoc` available for API users if you want a public developer surface. Admin routes are hidden from OpenAPI and still require Basic auth.
|
||||
- Leave cleanup enabled. The app periodically removes abandoned TXT challenges and generated hostnames that were never updated by a router.
|
||||
- Leave cleanup enabled. The cleanup scheduler is stored in SQLite with a task row, next run time, status, last error, and a database lease so only one app process runs a due cleanup job.
|
||||
- Back up `/data/ddns.sqlite3`.
|
||||
- Run one Uvicorn worker with SQLite. The per-host update lock is process-local.
|
||||
- Run one Uvicorn worker with SQLite unless you have tested your deployment under load. Scheduled cleanup is lease-protected across workers, but rate-limit state and per-host update locks are process-local.
|
||||
- Keep Uvicorn access logs disabled unless your reverse proxy redacts query strings. Update URLs, dashboard URLs, and domain claim links are bearer secrets and may appear in request paths or query strings.
|
||||
|
||||
Security and cleanup defaults:
|
||||
|
||||
@@ -276,10 +355,14 @@ Security and cleanup defaults:
|
||||
| `DDNS_RATE_LIMIT_PER_MINUTE` | `60` | Public update/API/form requests allowed per source IP and path. |
|
||||
| `DDNS_ADMIN_RATE_LIMIT_PER_MINUTE` | `20` | Admin/operator requests allowed per source IP and path. |
|
||||
| `DDNS_MAX_REQUEST_BODY_BYTES` | `16384` | Maximum accepted request body for form/API endpoints. |
|
||||
| `DDNS_ADMIN_PATH` | `/admin` | Private operator UI path. Admin routes are hidden from OpenAPI and require Basic auth. |
|
||||
| `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_INTERVAL_SECONDS` | `3600` | Run the in-app cleanup scheduler at this interval. |
|
||||
| `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. |
|
||||
| `DDNS_DASHBOARD_UNLOCK_SECONDS` | `86400` | Default dashboard password unlock cookie lifetime. |
|
||||
| `DDNS_DASHBOARD_STAY_UNLOCKED_SECONDS` | `2592000` | Dashboard unlock lifetime when the user chooses to stay unlocked on this browser. |
|
||||
|
||||
SQLite backup example:
|
||||
|
||||
@@ -293,6 +376,22 @@ KarlDNS is suitable for self-hosting and private/public beta use when a real DNS
|
||||
|
||||
## Development
|
||||
|
||||
Backend layout:
|
||||
|
||||
| Module | Purpose |
|
||||
| --- | --- |
|
||||
| `router_dyndns/app.py` | FastAPI app factory, lifespan, middleware and router registration. |
|
||||
| `router_dyndns/ddns.py` | Compatibility exports for existing imports and CLI users. |
|
||||
| `router_dyndns/routers/` | Module-level `APIRouter` files for API, public web, management, admin, assets, and DynDNS update routes. |
|
||||
| `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/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. |
|
||||
| `router_dyndns/ddns_dns.py` | Compatibility exports for DNS provider helpers. |
|
||||
|
||||
```bash
|
||||
ruff check router_dyndns tests
|
||||
pytest
|
||||
@@ -307,4 +406,6 @@ Self-hosted DynDNS, self hosted DDNS, Dynamic DNS server, FRITZ!Box DynDNS, FRIT
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
If you find a vulnerability, open a private advisory or contact the repository owner before publishing details.
|
||||
Reference in new issue
Block a user