Add dynamic DNS and show the running kernel on the overview
router.ddns keeps provider records on the uplink's addresses with ddclient, run by a timer and by a watcher that starts it when an address changes. The Dynamic DNS page edits it; the secret is a private file on the router. The overview shows the running kernel and says when the current system boots a different one. PLAN.md records why the A/B image approach was not pursued. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# 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;
|
||||
|
||||
# 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 = ddns.passwordFile != null && !lib.hasPrefix "/nix/store" ddns.passwordFile;
|
||||
message = "router.ddns.passwordFile must name a file outside the Nix store (the secret must not be copied into it).";
|
||||
}
|
||||
];
|
||||
|
||||
services.ddclient = {
|
||||
enable = true;
|
||||
inherit (ddns)
|
||||
protocol
|
||||
username
|
||||
passwordFile
|
||||
domains
|
||||
ssl
|
||||
interval
|
||||
;
|
||||
server = lib.optionalString (ddns.server != null) ddns.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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
./firewall.nix
|
||||
./dhcp.nix
|
||||
./dns.nix
|
||||
./ddns.nix
|
||||
./rollback.nix
|
||||
./power.nix
|
||||
];
|
||||
|
||||
@@ -498,6 +498,73 @@ in
|
||||
);
|
||||
};
|
||||
|
||||
ddns = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Keep DNS records at a provider pointing at the router's uplink
|
||||
addresses (ddclient). It checks every `interval` and as soon as an
|
||||
uplink address changes.
|
||||
'';
|
||||
};
|
||||
protocol = mkOption {
|
||||
type = types.str;
|
||||
default = "cloudflare";
|
||||
example = "dyndns2";
|
||||
description = "ddclient protocol of the provider, see <https://ddclient.net/protocols.html>.";
|
||||
};
|
||||
server = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "dynupdate.no-ip.com";
|
||||
description = "The provider's update server, for protocols that need one (`dyndns2`).";
|
||||
};
|
||||
username = mkOption {
|
||||
type = types.str;
|
||||
default = "token";
|
||||
description = "Login; `token` for a Cloudflare API token.";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "/var/lib/router/ddns-secret";
|
||||
description = "File with the password or API token. Never a path in the Nix store.";
|
||||
};
|
||||
zone = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "example.org";
|
||||
description = "The zone the names are in, for providers that need it (Cloudflare).";
|
||||
};
|
||||
domains = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "home.example.org" ];
|
||||
description = "Names to keep up to date.";
|
||||
};
|
||||
ipv4 = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Update A records with the IPv4 address of the uplink.";
|
||||
};
|
||||
ipv6 = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Update AAAA records with the IPv6 address of the uplink.";
|
||||
};
|
||||
ssl = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Talk to the provider over HTTPS.";
|
||||
};
|
||||
interval = mkOption {
|
||||
type = types.str;
|
||||
default = "5min";
|
||||
description = "How often to check, besides when an address changes (systemd time span).";
|
||||
};
|
||||
};
|
||||
|
||||
managementPorts = mkOption {
|
||||
type = types.listOf types.port;
|
||||
default = [ ];
|
||||
|
||||
Reference in New Issue
Block a user