Rebrand app to KarlDNS
No files matched your search
@@ -1,10 +1,10 @@
|
||||
DDNS_ADMIN_PASSWORD=change-me-long-random-admin-password
|
||||
DDNS_PUBLIC_BASE_URL=https://ddns.example.net
|
||||
DDNS_HOSTNAME_SUFFIX=ddns.example.net
|
||||
DDNS_PUBLIC_BASE_URL=https://karldns.de
|
||||
DDNS_HOSTNAME_SUFFIX=karldns.de
|
||||
DDNS_DATABASE=/data/ddns.sqlite3
|
||||
DDNS_TRUSTED_HOSTS=ddns.example.net
|
||||
DDNS_TRUSTED_HOSTS=karldns.de
|
||||
DDNS_DNS_PROVIDER=cloudflare
|
||||
DDNS_DNS_ZONES=ddns.example.net
|
||||
DDNS_DNS_ZONES=karldns.de
|
||||
DDNS_REQUIRE_DNS_PROVIDER=1
|
||||
DDNS_CLOUDFLARE_API_TOKEN=change-me-cloudflare-token
|
||||
DDNS_CLOUDFLARE_ZONE_ID=change-me-cloudflare-zone-id
|
||||
|
||||
@@ -10,3 +10,4 @@ __pycache__/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
tmp/
|
||||
@@ -1,4 +1,4 @@
|
||||
ddns.example.net {
|
||||
karldns.de {
|
||||
encode zstd gzip
|
||||
reverse_proxy ddns:8080
|
||||
}
|
||||
@@ -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.**
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
## Why RouterPulse?
|
||||
## Why KarlDNS?
|
||||
|
||||
- **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.
|
||||
@@ -31,13 +31,13 @@ If you want a lightweight DuckDNS / Dynu / No-IP style service that you control
|
||||
|
||||
## 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.
|
||||
2. They generate a random hostname like `a1b2c3d4.ddns.example.net`, or verify their own domain with a TXT record.
|
||||
3. RouterPulse shows the exact router settings for the DynDNS form.
|
||||
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.
|
||||
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. 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.
|
||||
|
||||
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
|
||||
export DDNS_ADMIN_PASSWORD='replace-with-a-long-random-value'
|
||||
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'
|
||||
|
||||
router-dyndns serve --host 127.0.0.1 --port 8080
|
||||
karldns serve --host 127.0.0.1 --port 8080
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
export DDNS_PUBLIC_BASE_URL='https://ddns.example.net'
|
||||
export DDNS_HOSTNAME_SUFFIX='ddns.example.net'
|
||||
export DDNS_PUBLIC_BASE_URL='https://karldns.de'
|
||||
export DDNS_HOSTNAME_SUFFIX='karldns.de'
|
||||
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_DNS_ZONES='ddns.example.net'
|
||||
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
|
||||
@@ -127,14 +127,14 @@ 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 RouterPulse should update.
|
||||
Use a least-privilege Cloudflare API token limited to the zone KarlDNS should update.
|
||||
|
||||
### RFC 2136 DNS Backend
|
||||
|
||||
```bash
|
||||
export DDNS_DNS_PROVIDER='rfc2136'
|
||||
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_SECRET='replace-with-tsig-secret'
|
||||
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`.
|
||||
|
||||
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`
|
||||
- `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:
|
||||
|
||||
```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:
|
||||
|
||||
```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.
|
||||
@@ -178,30 +178,30 @@ Every generated hostname can be updated with curl, cron, a shell script, Home As
|
||||
|
||||
```bash
|
||||
# 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
|
||||
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
|
||||
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:
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -209,7 +209,7 @@ The generated hostname is not a secret. Treat it like any public DNS name. The u
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -219,7 +219,7 @@ Custom-domain publishing is intentionally simple:
|
||||
4. Press **I added it, check DNS**.
|
||||
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
|
||||
|
||||
@@ -238,7 +238,7 @@ The JSON API is available under `/api/v1`.
|
||||
Example:
|
||||
|
||||
```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' \
|
||||
-d '{"username":"home-router"}'
|
||||
```
|
||||
@@ -260,7 +260,7 @@ docker compose up -d --build
|
||||
- Keep `.env` out of git.
|
||||
- Use a long random value for `DDNS_ADMIN_PASSWORD`.
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -282,12 +282,12 @@ Security and cleanup defaults:
|
||||
SQLite backup example:
|
||||
|
||||
```bash
|
||||
sqlite3 /data/ddns.sqlite3 ".backup '/backups/routerpulse.sqlite3'"
|
||||
sqlite3 /data/ddns.sqlite3 ".backup '/backups/karldns.sqlite3'"
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 576 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 522 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 442 KiB |
@@ -3,7 +3,7 @@ requires = ["hatchling>=1.25"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "router-dyndns"
|
||||
name = "karldns"
|
||||
version = "0.1.0"
|
||||
description = "A small self-hosted DynDNS service for FRITZ!Box and compatible routers."
|
||||
readme = "README.md"
|
||||
@@ -39,6 +39,7 @@ dev = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
karldns = "router_dyndns.cli:main"
|
||||
router-dyndns = "router_dyndns.cli:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
|
||||
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 361 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 477 KiB |
@@ -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 |
@@ -6,7 +6,7 @@ from .ddns import DdnsSettings, make_app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(prog="router-dyndns")
|
||||
parser = argparse.ArgumentParser(prog="karldns")
|
||||
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.")
|
||||
|
||||
@@ -229,7 +229,7 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
|
||||
await cleanup_task
|
||||
|
||||
app = FastAPI(
|
||||
title="router_dyndns DDNS",
|
||||
title="KarlDNS",
|
||||
summary="A small self-hosted DynDNS provider for FRITZ!Box routers.",
|
||||
description=(
|
||||
"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(
|
||||
status_code=401,
|
||||
detail="admin authentication required",
|
||||
headers={"WWW-Authenticate": 'Basic realm="router_dyndns-ddns"'},
|
||||
headers={"WWW-Authenticate": 'Basic realm="karldns"'},
|
||||
)
|
||||
|
||||
def apply_update(
|
||||
@@ -521,10 +521,6 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
|
||||
def favicon() -> FileResponse:
|
||||
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)
|
||||
def logo_png() -> FileResponse:
|
||||
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 ""
|
||||
challenge_html = _custom_domain_flow(service, challenge, message)
|
||||
return _page(
|
||||
"router-dyndns",
|
||||
"KarlDNS",
|
||||
f"""
|
||||
<main id="main">
|
||||
{_top_nav()}
|
||||
<section class="hero-band">
|
||||
<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>
|
||||
<h1>The simplest way to keep a home IP updated.</h1>
|
||||
<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><ipaddr></code> and calls it when your public IP changes.</p>
|
||||
<div class="terminal-card mt-4" aria-hidden="true">
|
||||
<div class="terminal-dots"><span></span><span></span><span></span></div>
|
||||
<code>$ curl -sS 'https://ddns.example.net/u/<secret>?myip=203.0.113.10'</code>
|
||||
<code>$ curl -sS 'https://karldns.de/u/<secret>?myip=203.0.113.10'</code>
|
||||
<code>good 203.0.113.10</code>
|
||||
</div>
|
||||
</div>
|
||||
@@ -698,9 +694,9 @@ def _render_public_home(
|
||||
<div class="terminal-card text-start mt-4">
|
||||
<div class="terminal-dots"><span></span><span></span><span></span></div>
|
||||
<code># explicit IPv4</code>
|
||||
<code>curl -sS 'https://ddns.example.net/u/<secret>?myip=203.0.113.10'</code>
|
||||
<code>curl -sS 'https://karldns.de/u/<secret>?myip=203.0.113.10'</code>
|
||||
<code># source IP fallback</code>
|
||||
<code>curl -sS 'https://ddns.example.net/u/<secret>'</code>
|
||||
<code>curl -sS 'https://karldns.de/u/<secret>'</code>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<nav class="navbar navbar-expand-sm sticky-top navbar-blur border-bottom" aria-label="Main navigation">
|
||||
<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">
|
||||
{links}
|
||||
{account_html}
|
||||
@@ -806,7 +802,7 @@ def _custom_domain_flow(service: DdnsService, challenge: dict[str, str | None] |
|
||||
<div>
|
||||
<p class="eyebrow">Direct custom-domain publishing</p>
|
||||
<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 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>
|
||||
<h3>Manual updates</h3>
|
||||
<p class="section-copy">FRITZ!Box replaces <code><ipaddr></code> and <code><ip6addr></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><ipaddr></code> and <code><ip6addr></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>
|
||||
<pre class="manual-code mb-0"><code>{safe_commands}</code></pre>
|
||||
{_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">
|
||||
<title>{html.escape(title)}</title>
|
||||
<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">
|
||||
<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">
|
||||
|
||||
@@ -16,7 +16,7 @@ class DdnsSettings(BaseModel):
|
||||
public_base_url: str = Field(default="http://localhost:8080")
|
||||
trusted_hosts: set[str] = Field(default_factory=set)
|
||||
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)
|
||||
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)
|
||||
@@ -85,7 +85,7 @@ class DdnsSettings(BaseModel):
|
||||
public_base_url=os.getenv("DDNS_PUBLIC_BASE_URL", "http://localhost:8080"),
|
||||
trusted_hosts=trusted_hosts,
|
||||
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),
|
||||
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),
|
||||
|
||||
@@ -160,7 +160,7 @@ class DdnsService:
|
||||
return None
|
||||
|
||||
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}"
|
||||
|
||||
def hostname_is_publishable(self, hostname: str) -> bool:
|
||||
|
||||