Rebrand app to KarlDNS

This commit is contained in:
StasonJatham committed 2026-05-20 20:03:25 +02:00
1 parent be72fa60ac
commit 2403a40800
17 files changed
+58 -96

No files matched your search

+4 -4
View File
@@ -1,10 +1,10 @@
DDNS_ADMIN_PASSWORD=change-me-long-random-admin-password DDNS_ADMIN_PASSWORD=change-me-long-random-admin-password
DDNS_PUBLIC_BASE_URL=https://ddns.example.net DDNS_PUBLIC_BASE_URL=https://karldns.de
DDNS_HOSTNAME_SUFFIX=ddns.example.net DDNS_HOSTNAME_SUFFIX=karldns.de
DDNS_DATABASE=/data/ddns.sqlite3 DDNS_DATABASE=/data/ddns.sqlite3
DDNS_TRUSTED_HOSTS=ddns.example.net DDNS_TRUSTED_HOSTS=karldns.de
DDNS_DNS_PROVIDER=cloudflare DDNS_DNS_PROVIDER=cloudflare
DDNS_DNS_ZONES=ddns.example.net DDNS_DNS_ZONES=karldns.de
DDNS_REQUIRE_DNS_PROVIDER=1 DDNS_REQUIRE_DNS_PROVIDER=1
DDNS_CLOUDFLARE_API_TOKEN=change-me-cloudflare-token DDNS_CLOUDFLARE_API_TOKEN=change-me-cloudflare-token
DDNS_CLOUDFLARE_ZONE_ID=change-me-cloudflare-zone-id DDNS_CLOUDFLARE_ZONE_ID=change-me-cloudflare-zone-id
+1
View File
@@ -10,3 +10,4 @@ __pycache__/
dist/ dist/
build/ build/
*.egg-info/ *.egg-info/
tmp/
+1 -1
View File
@@ -1,4 +1,4 @@
ddns.example.net { karldns.de {
encode zstd gzip encode zstd gzip
reverse_proxy ddns:8080 reverse_proxy ddns:8080
} }
+36 -36
View File
@@ -1,14 +1,14 @@
# RouterPulse # KarlDNS
**Self-hosted DynDNS for FRITZ!Box, OpenWrt, pfSense, OPNsense, UniFi, MikroTik, and any router that can call a custom DDNS update URL.** **Self-hosted DynDNS for FRITZ!Box, OpenWrt, pfSense, OPNsense, UniFi, MikroTik, and any router that can call a custom DDNS update URL.**
RouterPulse is the friendly name for `router-dyndns`: a small FastAPI Dynamic DNS provider you can run on your own VPS. It gives users FRITZ!Box-ready DynDNS credentials, keeps public A/AAAA records updated through Cloudflare or RFC 2136, provides generated hostnames that work as CNAME targets, and supports 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 FRITZ!Box-ready DynDNS credentials, keeps public A/AAAA records updated through Cloudflare or RFC 2136, provides generated hostnames that work as CNAME targets, and supports verified custom domains with DNS TXT ownership checks.
If you want a lightweight DuckDNS / Dynu / No-IP style service that you control yourself, RouterPulse is built for that use case. If you want a lightweight DuckDNS / Dynu / No-IP style service that you control yourself, KarlDNS is built for that use case.
![RouterPulse self-hosted DynDNS home screen](docs/screenshots/home.png) ![KarlDNS self-hosted DynDNS home screen](docs/screenshots/home.png)
## Why RouterPulse? ## Why KarlDNS?
- **Self-hosted Dynamic DNS provider** for home labs, small ISPs, communities, and private infrastructure. - **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. - **FRITZ!Box compatible**: copy the generated `Update-URL`, `Domainnamen`, `Benutzername`, and `Kennwort` straight into the German FRITZ!Box DynDNS form.
@@ -31,13 +31,13 @@ If you want a lightweight DuckDNS / Dynu / No-IP style service that you control
## What It Does ## What It Does
RouterPulse lets you host your own managed DynDNS service: KarlDNS lets you host your own managed DynDNS service:
1. A user opens your RouterPulse URL. 1. A user opens your KarlDNS URL.
2. They generate a random hostname like `a1b2c3d4.ddns.example.net`, or verify their own domain with a TXT record. 2. They generate a random hostname like `a1b2c3d4.karldns.de`, or verify their own domain with a TXT record.
3. RouterPulse shows the exact router settings for the DynDNS form. 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. 4. The router periodically calls the update URL with its current public IPv4/IPv6 address.
5. RouterPulse publishes the matching DNS records through your configured DNS backend. 5. KarlDNS publishes the matching DNS records through your configured DNS backend.
6. If the user wants their own DNS name, they can create a CNAME to the generated hostname. 6. If the user wants their own DNS name, they can create a CNAME to the generated hostname.
That means users always have a current hostname for VPN, remote access, self-hosted apps, home servers, NAS devices, and lab networks. That means users always have a current hostname for VPN, remote access, self-hosted apps, home servers, NAS devices, and lab networks.
@@ -81,10 +81,10 @@ Run locally:
```bash ```bash
export DDNS_ADMIN_PASSWORD='replace-with-a-long-random-value' export DDNS_ADMIN_PASSWORD='replace-with-a-long-random-value'
export DDNS_PUBLIC_BASE_URL='http://localhost:8080' export DDNS_PUBLIC_BASE_URL='http://localhost:8080'
export DDNS_HOSTNAME_SUFFIX='ddns.example.net' export DDNS_HOSTNAME_SUFFIX='karldns.de'
export DDNS_DATABASE='./ddns.sqlite3' export DDNS_DATABASE='./ddns.sqlite3'
router-dyndns serve --host 127.0.0.1 --port 8080 karldns serve --host 127.0.0.1 --port 8080
``` ```
Open: Open:
@@ -103,12 +103,12 @@ 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: For an internet-facing DynDNS service, run behind HTTPS and require a real DNS backend:
```bash ```bash
export DDNS_PUBLIC_BASE_URL='https://ddns.example.net' export DDNS_PUBLIC_BASE_URL='https://karldns.de'
export DDNS_HOSTNAME_SUFFIX='ddns.example.net' export DDNS_HOSTNAME_SUFFIX='karldns.de'
export DDNS_DATABASE='/data/ddns.sqlite3' export DDNS_DATABASE='/data/ddns.sqlite3'
export DDNS_TRUSTED_HOSTS='ddns.example.net' export DDNS_TRUSTED_HOSTS='karldns.de'
export DDNS_REQUIRE_DNS_PROVIDER=1 export DDNS_REQUIRE_DNS_PROVIDER=1
export DDNS_DNS_ZONES='ddns.example.net' export DDNS_DNS_ZONES='karldns.de'
export DDNS_TRUSTED_PROXY_IPS='127.0.0.1,::1' export DDNS_TRUSTED_PROXY_IPS='127.0.0.1,::1'
export DDNS_RATE_LIMIT_PER_MINUTE=60 export DDNS_RATE_LIMIT_PER_MINUTE=60
export DDNS_ADMIN_RATE_LIMIT_PER_MINUTE=20 export DDNS_ADMIN_RATE_LIMIT_PER_MINUTE=20
@@ -127,14 +127,14 @@ export DDNS_CLOUDFLARE_ZONE_ID='replace-with-cloudflare-zone-id'
export DDNS_TTL=60 export DDNS_TTL=60
``` ```
Use a least-privilege Cloudflare API token limited to the zone RouterPulse should update. Use a least-privilege Cloudflare API token limited to the zone KarlDNS should update.
### RFC 2136 DNS Backend ### RFC 2136 DNS Backend
```bash ```bash
export DDNS_DNS_PROVIDER='rfc2136' export DDNS_DNS_PROVIDER='rfc2136'
export DDNS_RFC2136_SERVER='127.0.0.1' export DDNS_RFC2136_SERVER='127.0.0.1'
export DDNS_RFC2136_ZONE='ddns.example.net' export DDNS_RFC2136_ZONE='karldns.de'
export DDNS_RFC2136_KEY_NAME='ddns-key' export DDNS_RFC2136_KEY_NAME='ddns-key'
export DDNS_RFC2136_KEY_SECRET='replace-with-tsig-secret' export DDNS_RFC2136_KEY_SECRET='replace-with-tsig-secret'
export DDNS_TTL=60 export DDNS_TTL=60
@@ -146,7 +146,7 @@ export DDNS_TTL=60
Create a hostname in the web UI. For the normal accountless flow, the generated `Update-URL` already contains the secret update slug. Put that URL into the router and use the generated hostname as `Domainnamen`. Create a hostname in the web UI. For the normal accountless flow, the generated `Update-URL` already contains the secret update slug. Put that URL into the router and use the generated hostname as `Domainnamen`.
RouterPulse also shows optional compatibility fields for routers that insist on separate credentials: KarlDNS also shows optional compatibility fields for routers that insist on separate credentials:
- `Update-URL` - `Update-URL`
- `Domainnamen` - `Domainnamen`
@@ -157,17 +157,17 @@ RouterPulse also shows optional compatibility fields for routers that insist on
The generated FRITZ!Box URL uses the native placeholders: The generated FRITZ!Box URL uses the native placeholders:
```text ```text
https://ddns.example.net/u/<random-update-slug>?myip=<ipaddr>&myipv6=<ip6addr> https://karldns.de/u/<random-update-slug>?myip=<ipaddr>&myipv6=<ip6addr>
``` ```
FRITZ!Box replaces `<ipaddr>` and `<ip6addr>` with the current WAN IPv4/IPv6 address and sends an HTTP GET request to that URL whenever it detects an address change. RouterPulse returns DynDNS-style responses such as `good 203.0.113.10`, `nochg 203.0.113.10`, `badauth`, or `nohost`. FRITZ!Box replaces `<ipaddr>` and `<ip6addr>` with the current WAN IPv4/IPv6 address and sends an HTTP GET request to that URL whenever it detects an address change. KarlDNS returns DynDNS-style responses such as `good 203.0.113.10`, `nochg 203.0.113.10`, `badauth`, or `nohost`.
This URL is enough for routers that only expose a single update URL field and do not send separate credentials. RouterPulse also shows a `One-box update URL` that embeds `hostname`, `username`, and `password` as query parameters for clients that require credential-style DynDNS URLs but do not expose separate credential fields. Treat both URLs as secrets. This URL is enough for routers that only expose a single update URL field and do not send separate credentials. KarlDNS also shows a `One-box update URL` that embeds `hostname`, `username`, and `password` as query parameters for clients that require credential-style DynDNS URLs but do not expose separate credential fields. Treat both URLs as secrets.
The compatibility endpoint is also available: The compatibility endpoint is also available:
```text ```text
https://ddns.example.net/nic/update?hostname=<domain>&myip=<ipaddr>&myipv6=<ip6addr>&username=<username>&password=<pass> https://karldns.de/nic/update?hostname=<domain>&myip=<ipaddr>&myipv6=<ip6addr>&username=<username>&password=<pass>
``` ```
Prefer `/u/<slug>` for managed service use because the router request cannot change the hostname. Prefer `/u/<slug>` for managed service use because the router request cannot change the hostname.
@@ -178,30 +178,30 @@ Every generated hostname can be updated with curl, cron, a shell script, Home As
```bash ```bash
# Explicit IPv4 update # Explicit IPv4 update
curl -sS 'https://ddns.example.net/u/<random-update-slug>?myip=203.0.113.10' curl -sS 'https://karldns.de/u/<random-update-slug>?myip=203.0.113.10'
# Explicit IPv6 update # Explicit IPv6 update
curl -sS 'https://ddns.example.net/u/<random-update-slug>?myipv6=2001:db8::10' curl -sS 'https://karldns.de/u/<random-update-slug>?myipv6=2001:db8::10'
# Use the request source IP # Use the request source IP
curl -sS 'https://ddns.example.net/u/<random-update-slug>' 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: The `/nic/update` compatibility endpoint also works for clients that expect hostname and credentials in the URL:
```bash ```bash
curl -sS 'https://ddns.example.net/nic/update?hostname=home.example.net&username=router&password=<password>&myip=203.0.113.10' curl -sS 'https://karldns.de/nic/update?hostname=home.example.net&username=router&password=<password>&myip=203.0.113.10'
``` ```
## Use Your Own Subdomain With CNAME ## Use Your Own Subdomain With CNAME
Most users do not need domain verification. Generate a RouterPulse hostname first, then create a CNAME at your DNS provider: Most users do not need domain verification. Generate a KarlDNS hostname first, then create a CNAME at your DNS provider:
```dns ```dns
home.example.com. CNAME a1b2c3d4.ddns.example.net. home.example.com. CNAME a1b2c3d4.karldns.de.
``` ```
Put the RouterPulse `Update-URL`, `Domainnamen`, `Benutzername`, and `Kennwort` into the router. The router updates `a1b2c3d4.ddns.example.net`; `home.example.com` follows through the CNAME. Put the KarlDNS `Update-URL`, `Domainnamen`, `Benutzername`, and `Kennwort` into the router. The router updates `a1b2c3d4.karldns.de`; `home.example.com` follows through the CNAME.
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. 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.
@@ -209,7 +209,7 @@ The generated hostname is not a secret. Treat it like any public DNS name. The u
## Custom Domain Flow ## Custom Domain Flow
This flow is only needed when RouterPulse should publish A/AAAA records directly inside a user-owned domain instead of using a CNAME target. 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: Custom-domain publishing is intentionally simple:
@@ -219,7 +219,7 @@ Custom-domain publishing is intentionally simple:
4. Press **I added it, check DNS**. 4. Press **I added it, check DNS**.
5. Create router credentials after the TXT record verifies. 5. Create router credentials after the TXT record verifies.
RouterPulse 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. The private claim link is the bearer credential for returning later after DNS propagation.
## HTTP API ## HTTP API
@@ -238,7 +238,7 @@ The JSON API is available under `/api/v1`.
Example: Example:
```bash ```bash
curl -sS https://ddns.example.net/api/v1/hostnames/magic \ curl -sS https://karldns.de/api/v1/hostnames/magic \
-H 'content-type: application/json' \ -H 'content-type: application/json' \
-d '{"username":"home-router"}' -d '{"username":"home-router"}'
``` ```
@@ -260,7 +260,7 @@ docker compose up -d --build
- Keep `.env` out of git. - Keep `.env` out of git.
- Use a long random value for `DDNS_ADMIN_PASSWORD`. - Use a long random value for `DDNS_ADMIN_PASSWORD`.
- Use a least-privilege DNS API token or TSIG key. - 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 `ddns.example.net`. - Set `DDNS_TRUSTED_HOSTS` to the public hostnames your reverse proxy will forward, for example `karldns.de`.
- Configure `DDNS_TRUSTED_PROXY_IPS` only for reverse proxies that strip user-supplied forwarding headers. - 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. - 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 app periodically removes abandoned TXT challenges and generated hostnames that were never updated by a router.
@@ -282,12 +282,12 @@ Security and cleanup defaults:
SQLite backup example: SQLite backup example:
```bash ```bash
sqlite3 /data/ddns.sqlite3 ".backup '/backups/routerpulse.sqlite3'" sqlite3 /data/ddns.sqlite3 ".backup '/backups/karldns.sqlite3'"
``` ```
## Project Status ## Project Status
RouterPulse is suitable for self-hosting and private/public beta use when a real DNS backend is configured. For a larger public free DynDNS provider, add external abuse controls, monitoring, alerting, and a tested backup/restore process before launch. KarlDNS is suitable for self-hosting and private/public beta use when a real DNS backend is configured. For a larger public free DynDNS provider, add external abuse controls, monitoring, alerting, and a tested backup/restore process before launch.
## Development ## Development
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 442 KiB

+2 -1
View File
@@ -3,7 +3,7 @@ requires = ["hatchling>=1.25"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "router-dyndns" name = "karldns"
version = "0.1.0" version = "0.1.0"
description = "A small self-hosted DynDNS service for FRITZ!Box and compatible routers." description = "A small self-hosted DynDNS service for FRITZ!Box and compatible routers."
readme = "README.md" readme = "README.md"
@@ -39,6 +39,7 @@ dev = [
] ]
[project.scripts] [project.scripts]
karldns = "router_dyndns.cli:main"
router-dyndns = "router_dyndns.cli:main" router-dyndns = "router_dyndns.cli:main"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 477 KiB

-35
View File
@@ -1,35 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title">
<title id="title">router-dyndns logo</title>
<defs>
<linearGradient id="navy" x1="48" x2="208" y1="52" y2="220" gradientUnits="userSpaceOnUse">
<stop stop-color="#17395d"/>
<stop offset="1" stop-color="#071727"/>
</linearGradient>
<linearGradient id="blue" x1="44" x2="218" y1="72" y2="180" gradientUnits="userSpaceOnUse">
<stop stop-color="#14b8ff"/>
<stop offset="1" stop-color="#0066cc"/>
</linearGradient>
</defs>
<path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="30" d="M60 155a78 78 0 0 1 112-70"/>
<path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="30" d="M184 88a78 78 0 0 1 12 81"/>
<path fill="#fff" d="m198 64 33 20-32 22-5-22z"/>
<path fill="none" stroke="url(#blue)" stroke-linecap="round" stroke-linejoin="round" stroke-width="18" d="M60 155a78 78 0 0 1 112-70"/>
<path fill="none" stroke="url(#blue)" stroke-linecap="round" stroke-linejoin="round" stroke-width="18" d="M184 88a78 78 0 0 1 12 81"/>
<path fill="url(#blue)" d="m198 74 21 12-20 14-3-14z"/>
<rect x="46" y="172" width="164" height="44" rx="18" fill="#fff"/>
<rect x="56" y="181" width="144" height="26" rx="11" fill="url(#navy)"/>
<path fill="#fff" d="M80 202h18v19a9 9 0 0 1-18 0zm78 0h18v19a9 9 0 0 1-18 0z"/>
<circle cx="86" cy="194" r="4" fill="#fff"/>
<circle cx="104" cy="194" r="4" fill="#fff"/>
<circle cx="122" cy="194" r="4" fill="#fff"/>
<circle cx="128" cy="119" r="47" fill="#fff"/>
<circle cx="128" cy="119" r="37" fill="url(#navy)"/>
<path fill="none" stroke="#fff" stroke-linecap="round" stroke-width="8" d="M102 94c19 6 38 18 53 35M98 132c22 8 46 6 64-12M122 83c-10 22-10 49 0 72M149 88c8 20 8 44-2 63"/>
<circle cx="108" cy="105" r="10" fill="url(#navy)" stroke="#fff" stroke-width="7"/>
<circle cx="148" cy="128" r="10" fill="url(#navy)" stroke="#fff" stroke-width="7"/>
<circle cx="120" cy="148" r="9" fill="url(#navy)" stroke="#fff" stroke-width="7"/>
<path fill="none" stroke="#fff" stroke-linecap="round" stroke-width="24" d="M91 65a53 53 0 0 1 74 0"/>
<path fill="none" stroke="#fff" stroke-linecap="round" stroke-width="24" d="M109 86a28 28 0 0 1 38 0"/>
<path fill="none" stroke="url(#navy)" stroke-linecap="round" stroke-width="12" d="M91 65a53 53 0 0 1 74 0"/>
<path fill="none" stroke="url(#navy)" stroke-linecap="round" stroke-width="12" d="M109 86a28 28 0 0 1 38 0"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

+1 -1
View File
@@ -6,7 +6,7 @@ from .ddns import DdnsSettings, make_app
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(prog="router-dyndns") parser = argparse.ArgumentParser(prog="karldns")
subparsers = parser.add_subparsers(dest="command", required=True) subparsers = parser.add_subparsers(dest="command", required=True)
render_url = subparsers.add_parser("render-fritz-url", help="Render a FRITZ!Box custom DynDNS update URL.") render_url = subparsers.add_parser("render-fritz-url", help="Render a FRITZ!Box custom DynDNS update URL.")
+10 -15
View File
@@ -229,7 +229,7 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
await cleanup_task await cleanup_task
app = FastAPI( app = FastAPI(
title="router_dyndns DDNS", title="KarlDNS",
summary="A small self-hosted DynDNS provider for FRITZ!Box routers.", summary="A small self-hosted DynDNS provider for FRITZ!Box routers.",
description=( description=(
"Generate cryptographically random DynDNS endpoints, verify custom domains with DNS TXT " "Generate cryptographically random DynDNS endpoints, verify custom domains with DNS TXT "
@@ -301,7 +301,7 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
raise HTTPException( raise HTTPException(
status_code=401, status_code=401,
detail="admin authentication required", detail="admin authentication required",
headers={"WWW-Authenticate": 'Basic realm="router_dyndns-ddns"'}, headers={"WWW-Authenticate": 'Basic realm="karldns"'},
) )
def apply_update( def apply_update(
@@ -521,10 +521,6 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
def favicon() -> FileResponse: def favicon() -> FileResponse:
return FileResponse(ASSET_DIR / "favicon.ico", media_type="image/x-icon") return FileResponse(ASSET_DIR / "favicon.ico", media_type="image/x-icon")
@app.get("/logo.svg", include_in_schema=False)
def logo_svg() -> FileResponse:
return FileResponse(ASSET_DIR / "logo.svg", media_type="image/svg+xml")
@app.get("/logo.png", include_in_schema=False) @app.get("/logo.png", include_in_schema=False)
def logo_png() -> FileResponse: def logo_png() -> FileResponse:
return FileResponse(ASSET_DIR / "logo.png", media_type="image/png") return FileResponse(ASSET_DIR / "logo.png", media_type="image/png")
@@ -647,13 +643,13 @@ def _render_public_home(
message_html = _message_band(message) if message and not challenge else "" message_html = _message_band(message) if message and not challenge else ""
challenge_html = _custom_domain_flow(service, challenge, message) challenge_html = _custom_domain_flow(service, challenge, message)
return _page( return _page(
"router-dyndns", "KarlDNS",
f""" f"""
<main id="main"> <main id="main">
{_top_nav()} {_top_nav()}
<section class="hero-band"> <section class="hero-band">
<div class="container text-center d-grid justify-items-center gap-3"> <div class="container text-center d-grid justify-items-center gap-3">
<img class="hero-logo" src="/logo.svg" alt="" width="92" height="92"> <img class="hero-logo" src="/logo.png" alt="" width="92" height="92">
<p class="eyebrow">Dynamic DNS for FRITZ!Box and any router</p> <p class="eyebrow">Dynamic DNS for FRITZ!Box and any router</p>
<h1>The simplest way to keep a home IP updated.</h1> <h1>The simplest way to keep a home IP updated.</h1>
<div class="install-pill mx-auto"> <div class="install-pill mx-auto">
@@ -677,7 +673,7 @@ def _render_public_home(
<p class="section-copy">The generated URL is the secret. FRITZ!Box replaces <code>&lt;ipaddr&gt;</code> and calls it when your public IP changes.</p> <p class="section-copy">The generated URL is the secret. FRITZ!Box replaces <code>&lt;ipaddr&gt;</code> and calls it when your public IP changes.</p>
<div class="terminal-card mt-4" aria-hidden="true"> <div class="terminal-card mt-4" aria-hidden="true">
<div class="terminal-dots"><span></span><span></span><span></span></div> <div class="terminal-dots"><span></span><span></span><span></span></div>
<code>$ curl -sS 'https://ddns.example.net/u/&lt;secret&gt;?myip=203.0.113.10'</code> <code>$ curl -sS 'https://karldns.de/u/&lt;secret&gt;?myip=203.0.113.10'</code>
<code>good 203.0.113.10</code> <code>good 203.0.113.10</code>
</div> </div>
</div> </div>
@@ -698,9 +694,9 @@ def _render_public_home(
<div class="terminal-card text-start mt-4"> <div class="terminal-card text-start mt-4">
<div class="terminal-dots"><span></span><span></span><span></span></div> <div class="terminal-dots"><span></span><span></span><span></span></div>
<code># explicit IPv4</code> <code># explicit IPv4</code>
<code>curl -sS 'https://ddns.example.net/u/&lt;secret&gt;?myip=203.0.113.10'</code> <code>curl -sS 'https://karldns.de/u/&lt;secret&gt;?myip=203.0.113.10'</code>
<code># source IP fallback</code> <code># source IP fallback</code>
<code>curl -sS 'https://ddns.example.net/u/&lt;secret&gt;'</code> <code>curl -sS 'https://karldns.de/u/&lt;secret&gt;'</code>
</div> </div>
</div> </div>
</section> </section>
@@ -727,7 +723,7 @@ def _top_nav(label: str | None = None, links: str = "") -> str:
<a class="skip-link" href="#main">Skip to content</a> <a class="skip-link" href="#main">Skip to content</a>
<nav class="navbar navbar-expand-sm sticky-top navbar-blur border-bottom" aria-label="Main navigation"> <nav class="navbar navbar-expand-sm sticky-top navbar-blur border-bottom" aria-label="Main navigation">
<div class="container"> <div class="container">
<a class="navbar-brand brand" href="/"><img src="/logo.svg" alt="" width="28" height="28">router-dyndns</a> <a class="navbar-brand brand" href="/"><img src="/logo.png" alt="" width="28" height="28">KarlDNS</a>
<div class="navbar-nav ms-auto flex-row align-items-center gap-2 gap-sm-3"> <div class="navbar-nav ms-auto flex-row align-items-center gap-2 gap-sm-3">
{links} {links}
{account_html} {account_html}
@@ -806,7 +802,7 @@ def _custom_domain_flow(service: DdnsService, challenge: dict[str, str | None] |
<div> <div>
<p class="eyebrow">Direct custom-domain publishing</p> <p class="eyebrow">Direct custom-domain publishing</p>
<h2>Verify DNS ownership</h2> <h2>Verify DNS ownership</h2>
<p class="section-copy">Use this only when RouterPulse should publish A/AAAA records directly inside your domain. If you just want your own subdomain, generate a hostname above and create a CNAME to it.</p> <p class="section-copy">Use this only when KarlDNS should publish A/AAAA records directly inside your domain. If you just want your own subdomain, generate a hostname above and create a CNAME to it.</p>
</div> </div>
</div> </div>
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
@@ -924,7 +920,7 @@ def _manual_update_panel(commands: str) -> str:
<div class="card card-body gap-3 mt-4"> <div class="card card-body gap-3 mt-4">
<div> <div>
<h3>Manual updates</h3> <h3>Manual updates</h3>
<p class="section-copy">FRITZ!Box replaces <code>&lt;ipaddr&gt;</code> and <code>&lt;ip6addr&gt;</code>, then sends a GET request to the Update-URL. Scripts and other routers can do the same with curl. If no IP parameter is sent, RouterPulse uses the request source IP.</p> <p class="section-copy">FRITZ!Box replaces <code>&lt;ipaddr&gt;</code> and <code>&lt;ip6addr&gt;</code>, then sends a GET request to the Update-URL. Scripts and other routers can do the same with curl. If no IP parameter is sent, KarlDNS uses the request source IP.</p>
</div> </div>
<pre class="manual-code mb-0"><code>{safe_commands}</code></pre> <pre class="manual-code mb-0"><code>{safe_commands}</code></pre>
{_copy_button("Copy curl examples", commands)} {_copy_button("Copy curl examples", commands)}
@@ -1316,7 +1312,6 @@ def _page(title: str, body: str) -> str:
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)}</title> <title>{html.escape(title)}</title>
<link rel="icon" href="/favicon.ico" sizes="any"> <link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/logo.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/logo.png"> <link rel="apple-touch-icon" href="/logo.png">
<script src="/app.js" defer></script> <script src="/app.js" defer></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
+2 -2
View File
@@ -16,7 +16,7 @@ class DdnsSettings(BaseModel):
public_base_url: str = Field(default="http://localhost:8080") public_base_url: str = Field(default="http://localhost:8080")
trusted_hosts: set[str] = Field(default_factory=set) trusted_hosts: set[str] = Field(default_factory=set)
hostname_suffix: str = Field(default="") hostname_suffix: str = Field(default="")
verification_prefix: str = Field(default="_router_dyndns-ddns") verification_prefix: str = Field(default="_karldns")
rate_limit_per_minute: int = Field(default=60, ge=1, le=10_000) rate_limit_per_minute: int = Field(default=60, ge=1, le=10_000)
admin_rate_limit_per_minute: int = Field(default=20, ge=1, le=10_000) admin_rate_limit_per_minute: int = Field(default=20, ge=1, le=10_000)
max_request_body_bytes: int = Field(default=16_384, ge=1024, le=1_048_576) max_request_body_bytes: int = Field(default=16_384, ge=1024, le=1_048_576)
@@ -85,7 +85,7 @@ class DdnsSettings(BaseModel):
public_base_url=os.getenv("DDNS_PUBLIC_BASE_URL", "http://localhost:8080"), public_base_url=os.getenv("DDNS_PUBLIC_BASE_URL", "http://localhost:8080"),
trusted_hosts=trusted_hosts, trusted_hosts=trusted_hosts,
hostname_suffix=os.getenv("DDNS_HOSTNAME_SUFFIX", ""), hostname_suffix=os.getenv("DDNS_HOSTNAME_SUFFIX", ""),
verification_prefix=os.getenv("DDNS_VERIFICATION_PREFIX", "_router_dyndns-ddns"), verification_prefix=os.getenv("DDNS_VERIFICATION_PREFIX", "_karldns"),
rate_limit_per_minute=_env_int("DDNS_RATE_LIMIT_PER_MINUTE", 60), rate_limit_per_minute=_env_int("DDNS_RATE_LIMIT_PER_MINUTE", 60),
admin_rate_limit_per_minute=_env_int("DDNS_ADMIN_RATE_LIMIT_PER_MINUTE", 20), admin_rate_limit_per_minute=_env_int("DDNS_ADMIN_RATE_LIMIT_PER_MINUTE", 20),
max_request_body_bytes=_env_int("DDNS_MAX_REQUEST_BODY_BYTES", 16_384), max_request_body_bytes=_env_int("DDNS_MAX_REQUEST_BODY_BYTES", 16_384),
+1 -1
View File
@@ -160,7 +160,7 @@ class DdnsService:
return None return None
def verification_name(self, domain: str) -> str: def verification_name(self, domain: str) -> str:
prefix = self.settings.verification_prefix.strip().strip(".") or "_router_dyndns-ddns" prefix = self.settings.verification_prefix.strip().strip(".") or "_karldns"
return f"{prefix}.{domain}" return f"{prefix}.{domain}"
def hostname_is_publishable(self, hostname: str) -> bool: def hostname_is_publishable(self, hostname: str) -> bool: