- 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
100 lines
3.4 KiB
Nix
100 lines
3.4 KiB
Nix
# Dynamic DNS: ddclient keeps the provider's records on the uplink's addresses.
|
|
# It runs from a timer; a watcher starts it as soon as an address of the uplink
|
|
# changes, so a new PPPoE session or lease is published within seconds.
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.router;
|
|
ddns = cfg.ddns;
|
|
# The connection (an empty one when it is missing: the assertion says so).
|
|
providers = builtins.fromJSON (builtins.readFile ../../pkgs/router-webui/providers.json);
|
|
has = ddns.provider != null && cfg.dnsProviders ? ${ddns.provider};
|
|
prov =
|
|
cfg.dnsProviders.${if ddns.provider == null then "" else ddns.provider} or {
|
|
type = "none";
|
|
username = "token";
|
|
server = null;
|
|
secretFile = null;
|
|
};
|
|
|
|
# Where the addresses are: IPv4 on the PPP device, IPv6 on the physical uplink
|
|
# (or the PPP device when IPv6 runs over it).
|
|
dev4 = cfg.wan.ipv4.interface;
|
|
dev6 = if cfg.wan.ipv6.enable && cfg.wan.ipv6.transport == "ppp" then dev4 else cfg.wan.interface;
|
|
devices = lib.unique (lib.optional ddns.ipv4 dev4 ++ lib.optional ddns.ipv6 dev6);
|
|
|
|
watch = pkgs.writeShellApplication {
|
|
name = "router-ddns-watch";
|
|
runtimeInputs = [
|
|
pkgs.iproute2
|
|
pkgs.gnugrep
|
|
pkgs.systemd
|
|
];
|
|
text = ''
|
|
# `ip monitor` also reports the lines of devices that do not exist yet.
|
|
ip -o monitor address \
|
|
| grep --line-buffered ${lib.concatMapStringsSep " " (d: "-e ' ${d} '") devices} \
|
|
| while read -r _; do
|
|
sleep 5 # an address change comes as several events
|
|
systemctl start --no-block ddclient.service
|
|
done
|
|
'';
|
|
};
|
|
in
|
|
{
|
|
config = lib.mkIf (cfg.enable && ddns.enable) {
|
|
assertions = [
|
|
{
|
|
assertion = ddns.domains != [ ];
|
|
message = "router.ddns: no domains to update.";
|
|
}
|
|
{
|
|
assertion = ddns.ipv4 || ddns.ipv6;
|
|
message = "router.ddns: neither IPv4 nor IPv6 is enabled.";
|
|
}
|
|
{
|
|
assertion = has;
|
|
message = "router.ddns.provider must name a connection in router.dnsProviders.";
|
|
}
|
|
{
|
|
assertion = !has || lib.elem prov.type providers.ddns;
|
|
message = "router.ddns: ddclient has no protocol ${prov.type}.";
|
|
}
|
|
{
|
|
assertion = !has || (prov.secretFile != null && !lib.hasPrefix "/nix/store" prov.secretFile);
|
|
message = "The secretFile of router.dnsProviders.${toString ddns.provider} must name a file outside the Nix store (the secret must not be copied into it).";
|
|
}
|
|
];
|
|
|
|
services.ddclient = {
|
|
enable = true;
|
|
inherit (ddns) domains ssl interval;
|
|
protocol = prov.type;
|
|
inherit (prov) username;
|
|
passwordFile = prov.secretFile;
|
|
server = lib.optionalString (prov.server != null) prov.server;
|
|
zone = lib.optionalString (ddns.zone != null) ddns.zone;
|
|
usev4 = lib.optionalString ddns.ipv4 "ifv4, ifv4=${dev4}";
|
|
usev6 = lib.optionalString ddns.ipv6 "ifv6, ifv6=${dev6}";
|
|
# ddclient only sends a changed address; its default of 30 s between
|
|
# updates would hold back a new one that follows closely on another
|
|
# (a PPP session that drops and comes back).
|
|
extraConfig = "min-interval=1s";
|
|
};
|
|
|
|
systemd.services.router-ddns-watch = {
|
|
description = "Update dynamic DNS when the uplink's address changes";
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
ExecStart = lib.getExe watch;
|
|
Restart = "always";
|
|
RestartSec = 5;
|
|
};
|
|
};
|
|
};
|
|
}
|