Add DNS accounts, Let's Encrypt, password change, reboot, HTTP redirect

- router.dnsProviders: named accounts at DNS providers, used by dynamic
  DNS and certificates (ddclient and lego provider lists in
  pkgs/router-webui/providers.json). Schema 2 migrates router.ddns.
- router.acme: a Let's Encrypt certificate for the web UI via
  security.acme (DNS-01, resolver 1.1.1.1), served and reloaded by the
  web UI; Certificate page.
- Web UI: password change (web UI and root), reboot button, HTTP to HTTPS
  redirect (router.webui.httpPort), grouped menu.
- Latest kernel (linuxPackages_latest); 7.3 and 7.3.0 are the same kernel
  for the reboot hint.
- SPECS.md: wanted later (REST API, GraphQL, MCP).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-10-30 19:27:49 +01:00
co-authored by Claude Sonnet 5.5
parent a32bf42777
commit caf2c6b26d
33 changed files with 1985 additions and 224 deletions
+143 -5
View File
@@ -313,10 +313,14 @@ let
ddnsValid = {
router = {
lans.main = lan { };
dnsProviders.cf = {
type = "cloudflare";
secretFile = "/var/lib/router/dns-secrets/cf";
};
ddns = {
enable = true;
provider = "cf";
domains = [ "home.example.org" ];
passwordFile = "/var/lib/router/ddns-secret";
};
};
expect = [ ];
@@ -324,20 +328,39 @@ let
ddnsSecretInTheStore = {
router = {
lans.main = lan { };
dnsProviders.cf = {
type = "cloudflare";
secretFile = "/nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-secret";
};
ddns = {
enable = true;
provider = "cf";
domains = [ "home.example.org" ];
passwordFile = "/nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-secret";
};
};
expect = [ "outside the Nix store" ];
};
ddnsWithoutNames = {
ddnsWithoutProvider = {
router = {
lans.main = lan { };
ddns = {
enable = true;
passwordFile = "/var/lib/router/ddns-secret";
provider = "nothing";
domains = [ "home.example.org" ];
};
};
expect = [ "must name a connection in router.dnsProviders" ];
};
ddnsWithoutNames = {
router = {
lans.main = lan { };
dnsProviders.cf = {
type = "cloudflare";
secretFile = "/var/lib/router/dns-secrets/cf";
};
ddns = {
enable = true;
provider = "cf";
};
};
expect = [ "no domains to update" ];
@@ -345,15 +368,130 @@ let
ddnsWithoutAnyFamily = {
router = {
lans.main = lan { };
dnsProviders.cf = {
type = "cloudflare";
secretFile = "/var/lib/router/dns-secrets/cf";
};
ddns = {
enable = true;
provider = "cf";
domains = [ "home.example.org" ];
passwordFile = "/var/lib/router/ddns-secret";
ipv4 = false;
};
};
expect = [ "neither IPv4 nor IPv6" ];
};
acmeValid = {
router = {
lans.main = lan { };
dnsProviders.cf = {
type = "cloudflare";
secretFile = "/var/lib/router/dns-secrets/cf";
};
acme = {
enable = true;
provider = "cf";
domains = [
"router.example.org"
"*.home.example.org"
];
email = "me@example.org";
acceptTerms = true;
};
};
expect = [ ];
};
acmeProviderCannotDoDNS01 = {
router = {
lans.main = lan { };
dnsProviders.dyn = {
type = "dyndns2";
secretFile = "/var/lib/router/dns-secrets/dyn";
};
acme = {
enable = true;
provider = "dyn";
domains = [ "router.example.org" ];
email = "me@example.org";
acceptTerms = true;
};
};
expect = [ "lego has no DNS provider dyndns2" ];
};
acmeEnvironmentProviderValid = {
router = {
lans.main = lan { };
dnsProviders.aws = {
type = "route53";
environmentFile = "/var/lib/router/dns-secrets/aws.env";
};
acme = {
enable = true;
provider = "aws";
domains = [ "router.example.org" ];
email = "me@example.org";
acceptTerms = true;
};
};
expect = [ ];
};
acmeEnvironmentProviderWithoutFile = {
router = {
lans.main = lan { };
dnsProviders.aws.type = "route53";
acme = {
enable = true;
provider = "aws";
domains = [ "router.example.org" ];
email = "me@example.org";
acceptTerms = true;
};
};
expect = [ "needs environmentFile for certificates" ];
};
ddnsProviderUnknownToDdclient = {
router = {
lans.main = lan { };
dnsProviders.aws = {
type = "route53";
secretFile = "/var/lib/router/dns-secrets/aws";
};
ddns = {
enable = true;
provider = "aws";
domains = [ "home.example.org" ];
};
};
expect = [ "ddclient has no protocol route53" ];
};
acmeTermsNotAccepted = {
router = {
lans.main = lan { };
dnsProviders.cf = {
type = "cloudflare";
secretFile = "/var/lib/router/dns-secrets/cf";
};
acme = {
enable = true;
provider = "cf";
domains = [ "router.example.org" ];
email = "me@example.org";
};
};
expect = [ "subscriber agreement must be accepted" ];
};
acmeWithoutProvider = {
router = {
lans.main = lan { };
acme = {
enable = true;
domains = [ "router.example.org" ];
email = "me@example.org";
acceptTerms = true;
};
};
expect = [ "must name a connection in router.dnsProviders" ];
};
ruleSourcePortsWithoutProtocol = {
router = {
lans = {