Archived
Document manual DDNS update flow
This commit is contained in:
1 parent
f865bc28c7
commit
62e5724318
3 files changed
+61
No files matched your search
@@ -160,6 +160,8 @@ The generated FRITZ!Box URL uses the native placeholders:
|
|||||||
https://ddns.example.net/u/<random-update-slug>?myip=<ipaddr>&myipv6=<ip6addr>
|
https://ddns.example.net/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`.
|
||||||
|
|
||||||
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. 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.
|
||||||
|
|
||||||
The compatibility endpoint is also available:
|
The compatibility endpoint is also available:
|
||||||
@@ -170,6 +172,27 @@ 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.
|
||||||
|
|
||||||
|
## Manual Updates And Automation
|
||||||
|
|
||||||
|
Every generated hostname can be updated with curl, cron, a shell script, Home Assistant, OpenWrt, pfSense, or any client that can send an HTTP GET request:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Explicit IPv4 update
|
||||||
|
curl -sS 'https://ddns.example.net/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'
|
||||||
|
|
||||||
|
# Use the request source IP
|
||||||
|
curl -sS 'https://ddns.example.net/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'
|
||||||
|
```
|
||||||
|
|
||||||
## 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 RouterPulse hostname first, then create a CNAME at your DNS provider:
|
||||||
|
|||||||
@@ -817,6 +817,7 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
|
|||||||
management_url = service.magic_management_url(created_account)
|
management_url = service.magic_management_url(created_account)
|
||||||
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
|
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
|
||||||
fritzbox_fields = _fritzbox_fields(update_url, hostname, username, password, management_url)
|
fritzbox_fields = _fritzbox_fields(update_url, hostname, username, password, management_url)
|
||||||
|
manual_commands = _manual_update_commands(update_url)
|
||||||
return f"""
|
return f"""
|
||||||
<section class="section success-section">
|
<section class="section success-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -838,6 +839,7 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
|
|||||||
{_copy_row("Kennwort (optional):", password, "Secret")}
|
{_copy_row("Kennwort (optional):", password, "Secret")}
|
||||||
{_copy_row("Private status page:", management_url, "Secret")}
|
{_copy_row("Private status page:", management_url, "Secret")}
|
||||||
</div>
|
</div>
|
||||||
|
{_manual_update_panel(manual_commands)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
@@ -881,6 +883,31 @@ def _copy_button(label: str, value: str) -> str:
|
|||||||
return f'<button type="button" class="btn btn-outline-primary rounded-pill" data-copy="{safe_value}">{safe_label}</button>'
|
return f'<button type="button" class="btn btn-outline-primary rounded-pill" data-copy="{safe_value}">{safe_label}</button>'
|
||||||
|
|
||||||
|
|
||||||
|
def _manual_update_commands(update_url: str) -> str:
|
||||||
|
update_base = update_url.split("?", 1)[0]
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
f"curl -sS '{update_base}?myip=203.0.113.10'",
|
||||||
|
f"curl -sS '{update_base}?myipv6=2001:db8::10'",
|
||||||
|
f"curl -sS '{update_base}'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _manual_update_panel(commands: str) -> str:
|
||||||
|
safe_commands = html.escape(commands)
|
||||||
|
return f"""
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<pre class="manual-code mb-0"><code>{safe_commands}</code></pre>
|
||||||
|
{_copy_button("Copy curl examples", commands)}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _fritzbox_fields(
|
def _fritzbox_fields(
|
||||||
update_url: str,
|
update_url: str,
|
||||||
hostname: str,
|
hostname: str,
|
||||||
@@ -935,6 +962,7 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage
|
|||||||
updated = html.escape(str(account.get("updated_at") or "Never"))
|
updated = html.escape(str(account.get("updated_at") or "Never"))
|
||||||
fritzbox_fields = _fritzbox_fields(update_url, hostname, username, "unchanged; only shown when generated")
|
fritzbox_fields = _fritzbox_fields(update_url, hostname, username, "unchanged; only shown when generated")
|
||||||
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
|
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
|
||||||
|
manual_commands = _manual_update_commands(update_url)
|
||||||
history_rows = "\n".join(_user_event_row(event) for event in service.store.list_update_events_for_hostname(str(account["hostname"]), 20)) or """
|
history_rows = "\n".join(_user_event_row(event) for event in service.store.list_update_events_for_hostname(str(account["hostname"]), 20)) or """
|
||||||
<tr><td colspan="5" class="empty">No router updates yet.</td></tr>
|
<tr><td colspan="5" class="empty">No router updates yet.</td></tr>
|
||||||
"""
|
"""
|
||||||
@@ -972,6 +1000,7 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage
|
|||||||
{_copy_row("Kennwort (optional):", "unchanged; only shown when generated", "Secret")}
|
{_copy_row("Kennwort (optional):", "unchanged; only shown when generated", "Secret")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{_manual_update_panel(manual_commands)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
@@ -1363,6 +1392,7 @@ def _page(title: str, body: str) -> str:
|
|||||||
.copy-field .form-control {{ min-width: 0; }}
|
.copy-field .form-control {{ min-width: 0; }}
|
||||||
.copy-field code.form-control {{ width: 1%; flex: 1 1 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }}
|
.copy-field code.form-control {{ width: 1%; flex: 1 1 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }}
|
||||||
.copy-field code {{ min-height: 40px; margin: 0; color: var(--rp-ink); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; line-height: 1.6; background: var(--rp-surface-alt); }}
|
.copy-field code {{ min-height: 40px; margin: 0; color: var(--rp-ink); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; line-height: 1.6; background: var(--rp-surface-alt); }}
|
||||||
|
.manual-code {{ overflow-x: auto; padding: 16px; border: 1px solid var(--rp-line); border-radius: 12px; color: var(--rp-ink); background: var(--rp-surface-alt); font: 400 13px/1.6 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }}
|
||||||
.admin-table-scroll {{ max-height: 560px; overflow: auto; }}
|
.admin-table-scroll {{ max-height: 560px; overflow: auto; }}
|
||||||
.admin-table-scroll thead th {{ position: sticky; top: 0; z-index: 1; background: var(--rp-surface); }}
|
.admin-table-scroll thead th {{ position: sticky; top: 0; z-index: 1; background: var(--rp-surface); }}
|
||||||
.table-page-size {{ width: auto; min-width: 76px; }}
|
.table-page-size {{ width: auto; min-width: 76px; }}
|
||||||
|
|||||||
@@ -368,6 +368,10 @@ def test_self_service_managed_hostname_generates_random_account(tmp_path: Path)
|
|||||||
assert "Kennwort (optional):" in response.text
|
assert "Kennwort (optional):" in response.text
|
||||||
assert "Copy complete FRITZ!Box fields" in response.text
|
assert "Copy complete FRITZ!Box fields" in response.text
|
||||||
assert "Copy CNAME record" in response.text
|
assert "Copy CNAME record" in response.text
|
||||||
|
assert "Manual updates" in response.text
|
||||||
|
assert "curl -sS" in response.text
|
||||||
|
assert "203.0.113.10" in response.text
|
||||||
|
assert "2001:db8::10" in response.text
|
||||||
assert "Public:" in response.text
|
assert "Public:" in response.text
|
||||||
assert "Secret:" in response.text
|
assert "Secret:" in response.text
|
||||||
assert ".ddns.example.net" in response.text
|
assert ".ddns.example.net" in response.text
|
||||||
@@ -398,6 +402,8 @@ def test_magic_hostname_without_login_generates_update_and_management_links(tmp_
|
|||||||
assert "Private status page:" in response.text
|
assert "Private status page:" in response.text
|
||||||
assert "Copy complete FRITZ!Box fields" in response.text
|
assert "Copy complete FRITZ!Box fields" in response.text
|
||||||
assert "Copy CNAME record" in response.text
|
assert "Copy CNAME record" in response.text
|
||||||
|
assert "FRITZ!Box replaces" in response.text
|
||||||
|
assert "Copy curl examples" in response.text
|
||||||
assert response.text.count("Secret") >= 3
|
assert response.text.count("Secret") >= 3
|
||||||
assert response.text.count("Public") >= 3
|
assert response.text.count("Public") >= 3
|
||||||
assert ".ddns.example.net" in response.text
|
assert ".ddns.example.net" in response.text
|
||||||
@@ -428,6 +434,8 @@ def test_magic_management_link_can_delete_hostname(tmp_path: Path) -> None:
|
|||||||
assert "Kennwort (optional):" in management.text
|
assert "Kennwort (optional):" in management.text
|
||||||
assert "Copy complete FRITZ!Box fields" in management.text
|
assert "Copy complete FRITZ!Box fields" in management.text
|
||||||
assert "Copy CNAME record" in management.text
|
assert "Copy CNAME record" in management.text
|
||||||
|
assert "Manual updates" in management.text
|
||||||
|
assert "curl -sS" in management.text
|
||||||
assert "Public:" in management.text
|
assert "Public:" in management.text
|
||||||
assert "Secret:" in management.text
|
assert "Secret:" in management.text
|
||||||
assert "Router updates" in management.text
|
assert "Router updates" in management.text
|
||||||
|
|||||||
Reference in new issue
Block a user