Archived
Clarify managed hostnames as CNAME targets
This commit is contained in:
1 parent
3ce992ae9e
commit
840d3281d1
3 files changed
+34
-10
No files matched your search
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in new issue
Block a user