Clarify managed hostnames as CNAME targets

This commit is contained in:
StasonJatham committed 2026-05-20 19:00:21 +02:00
1 parent 3ce992ae9e
commit 840d3281d1
3 files changed
+34 -10

No files matched your search

+21 -2
View File
@@ -2,7 +2,7 @@
**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, and supports verified custom domains with DNS TXT ownership checks. 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.
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, RouterPulse is built for that use case.
@@ -12,6 +12,7 @@ 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. - **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.
- **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. - **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. - **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.
- **Real DNS publishing** through **Cloudflare** or **RFC 2136** with A and AAAA support. - **Real DNS publishing** through **Cloudflare** or **RFC 2136** with A and AAAA support.
@@ -37,6 +38,7 @@ RouterPulse lets you host your own managed DynDNS service:
3. RouterPulse shows the exact router settings for the DynDNS form. 3. RouterPulse 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. RouterPulse 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. That means users always have a current hostname for VPN, remote access, self-hosted apps, home servers, NAS devices, and lab networks.
@@ -46,6 +48,7 @@ That means users always have a current hostname for VPN, remote access, self-hos
- Free DDNS service for friends, customers, a community, or a home lab. - Free DDNS service for friends, customers, a community, or a home lab.
- Dynamic DNS for IPv4 and IPv6 home internet connections. - Dynamic DNS for IPv4 and IPv6 home internet connections.
- Custom domain DDNS with DNS TXT verification. - 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. - Lightweight Cloudflare DDNS provider with a web UI.
- RFC 2136 / TSIG DynDNS frontend for BIND, Knot, PowerDNS, or compatible DNS servers. - RFC 2136 / TSIG DynDNS frontend for BIND, Knot, PowerDNS, or compatible DNS servers.
@@ -56,6 +59,7 @@ That means users always have a current hostname for VPN, remote access, self-hos
| Router update URLs | FRITZ!Box custom provider URL, `/u/<slug>`, and `/nic/update` compatibility | | Router update URLs | FRITZ!Box custom provider URL, `/u/<slug>`, and `/nic/update` compatibility |
| DNS records | A and AAAA | | DNS records | A and AAAA |
| DNS providers | Cloudflare API and RFC 2136 dynamic DNS | | 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 | | Custom domains | TXT challenge verification before hostname creation |
| Persistence | SQLite with WAL mode | | Persistence | SQLite with WAL mode |
| API | FastAPI, OpenAPI, Swagger UI, ReDoc | | API | FastAPI, OpenAPI, Swagger UI, ReDoc |
@@ -144,6 +148,7 @@ Create a hostname in the web UI. RouterPulse shows the exact values for:
- `Update-URL` - `Update-URL`
- `Domainnamen` - `Domainnamen`
- `CNAME target`
- `Benutzername` - `Benutzername`
- `Kennwort` - `Kennwort`
@@ -161,9 +166,23 @@ https://ddns.example.net/nic/update?hostname=<domain>&myip=<ipaddr>&myipv6=<ip6a
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.
## 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:
```dns
home.example.com. CNAME a1b2c3d4.ddns.example.net.
```
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.
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.
## Custom Domain Flow ## Custom Domain Flow
Custom domains are intentionally simple: This flow is only needed when RouterPulse 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. 1. Enter the domain.
2. Save the private claim link. 2. Save the private claim link.
+10 -8
View File
@@ -649,10 +649,10 @@ def _render_public_home(
<div class="container text-center d-grid justify-items-center gap-3"> <div class="container text-center d-grid justify-items-center gap-3">
<p class="eyebrow">FRITZ!Box compatible</p> <p class="eyebrow">FRITZ!Box compatible</p>
<h1>DynDNS without signups.</h1> <h1>DynDNS without signups.</h1>
<p class="lead">Create a secure update endpoint under {suffix}. Provider hostnames are anonymous; custom domains require DNS proof.</p> <p class="lead">Create a secure hostname under {suffix}. Use it directly, or point your own subdomain at it with a CNAME.</p>
<div class="d-flex gap-2 flex-wrap justify-content-center mt-2"> <div class="d-flex gap-2 flex-wrap justify-content-center mt-2">
<a class="btn btn-primary rounded-pill" href="#create">Get a hostname</a> <a class="btn btn-primary rounded-pill" href="#create">Get a hostname</a>
<a class="btn btn-outline-primary rounded-pill" href="#custom-domain">Verify a domain</a> <a class="btn btn-outline-primary rounded-pill" href="#custom-domain">Publish inside my domain</a>
</div> </div>
</div> </div>
</section> </section>
@@ -663,8 +663,8 @@ def _render_public_home(
<div class="row g-4 align-items-start"> <div class="row g-4 align-items-start">
<div class="col-lg-7"> <div class="col-lg-7">
<p class="eyebrow">Free DynDNS hostname</p> <p class="eyebrow">Free DynDNS hostname</p>
<h2>Create a router update URL</h2> <h2>Create your DynDNS target</h2>
<p class="section-copy">Generate a random hostname, paste the update URL into your router, and keep the management link private.</p> <p class="section-copy">Generate a random hostname, paste the update URL into your router, and keep the private status page. Your own DNS name can point to this hostname with a CNAME.</p>
</div> </div>
<form method="post" action="/magic" class="col-lg-5 card card-body gap-3" aria-label="Create a generated DynDNS hostname"> <form method="post" action="/magic" class="col-lg-5 card card-body gap-3" aria-label="Create a generated DynDNS hostname">
<label>Router username <label>Router username
@@ -773,9 +773,9 @@ def _custom_domain_flow(service: DdnsService, challenge: dict[str, str | None] |
<div class="container"> <div class="container">
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row"> <div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
<div> <div>
<p class="eyebrow">Custom domain</p> <p class="eyebrow">Direct custom-domain publishing</p>
<h2>Verify DNS ownership</h2> <h2>Verify DNS ownership</h2>
<p class="section-copy">We only issue router credentials after the DNS record proves you control the domain.</p> <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>
</div> </div>
</div> </div>
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
@@ -812,10 +812,11 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
<div class="container"> <div class="container">
<p class="eyebrow">Credentials generated</p> <p class="eyebrow">Credentials generated</p>
<h2>{html.escape(hostname)}</h2> <h2>{html.escape(hostname)}</h2>
<p class="section-copy">Save this now. The password and management link are only shown on this screen.</p> <p class="section-copy">Save this now. The password and private status page are only shown on this screen. To use your own DNS name, create a CNAME from that name to this hostname.</p>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
{_copy_row("Update-URL:", update_url)} {_copy_row("Update-URL:", update_url)}
{_copy_row("Domainnamen:", hostname)} {_copy_row("Domainnamen:", hostname)}
{_copy_row("CNAME target:", hostname)}
{_copy_row("Benutzername:", username)} {_copy_row("Benutzername:", username)}
{_copy_row("Kennwort:", password)} {_copy_row("Kennwort:", password)}
{_copy_row("Private status page:", management_url)} {_copy_row("Private status page:", management_url)}
@@ -898,11 +899,12 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage
<div class="col-lg-6"> <div class="col-lg-6">
<p class="eyebrow">Router settings</p> <p class="eyebrow">Router settings</p>
<h2>FRITZ!Box fields</h2> <h2>FRITZ!Box fields</h2>
<p class="section-copy">Use these values in the FRITZ!Box custom DynDNS provider fields.</p> <p class="section-copy">Use these values in the FRITZ!Box custom DynDNS provider fields. If you use your own DNS name, point it at this hostname with a CNAME.</p>
</div> </div>
<div class="col-lg-6 d-grid gap-2"> <div class="col-lg-6 d-grid gap-2">
{_copy_row("Update-URL:", update_url)} {_copy_row("Update-URL:", update_url)}
{_copy_row("Domainnamen:", hostname)} {_copy_row("Domainnamen:", hostname)}
{_copy_row("CNAME target:", hostname)}
{_copy_row("Benutzername:", username)} {_copy_row("Benutzername:", username)}
{_copy_row("Kennwort:", "unchanged; only shown when generated")} {_copy_row("Kennwort:", "unchanged; only shown when generated")}
</div> </div>
+3
View File
@@ -289,6 +289,7 @@ def test_self_service_managed_hostname_generates_random_account(tmp_path: Path)
assert response.status_code == 200 assert response.status_code == 200
assert "Update-URL:" in response.text assert "Update-URL:" in response.text
assert "CNAME target:" in response.text
assert ".ddns.example.net" in response.text assert ".ddns.example.net" in response.text
assert "/u/" in response.text assert "/u/" in response.text
assert "/m/" in response.text assert "/m/" in response.text
@@ -309,6 +310,7 @@ def test_magic_hostname_without_login_generates_update_and_management_links(tmp_
assert response.status_code == 200 assert response.status_code == 200
assert "Update-URL:" in response.text assert "Update-URL:" in response.text
assert "CNAME target:" in response.text
assert "Private status page:" in response.text assert "Private status page:" in response.text
assert ".ddns.example.net" in response.text assert ".ddns.example.net" in response.text
@@ -323,6 +325,7 @@ def test_magic_management_link_can_delete_hostname(tmp_path: Path) -> None:
management = client.get(f"/m/{account['management_slug']}") management = client.get(f"/m/{account['management_slug']}")
assert management.status_code == 200 assert management.status_code == 200
assert "home.ddns.example.net" in management.text assert "home.ddns.example.net" in management.text
assert "CNAME target:" in management.text
assert "Router updates" in management.text assert "Router updates" in management.text
deleted = client.post(f"/m/{account['management_slug']}/delete") deleted = client.post(f"/m/{account['management_slug']}/delete")