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:
+113
@@ -0,0 +1,113 @@
|
||||
# Dynamic DNS: the router tells its provider about the uplink's address, at
|
||||
# boot and as soon as the address changes (the timer is set to an hour, so
|
||||
# only the watcher can explain the second update).
|
||||
#
|
||||
# upstream (ISP: DHCP, a dyndns2 update server) -- router
|
||||
{ self, pkgs }:
|
||||
let
|
||||
common = import ./common.nix;
|
||||
in
|
||||
pkgs.testers.runNixOSTest {
|
||||
name = "router-ddns";
|
||||
|
||||
defaults.imports = [ common.tools ];
|
||||
interactive.defaults = common.interactiveDefaults;
|
||||
|
||||
nodes = {
|
||||
upstream =
|
||||
{ lib, ... }:
|
||||
{
|
||||
virtualisation.vlans = [ 1 ];
|
||||
networking.useNetworkd = true;
|
||||
networking.useDHCP = false;
|
||||
networking.firewall.enable = false;
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ];
|
||||
systemd.network.networks."10-wan" = {
|
||||
matchConfig.Name = "eth1";
|
||||
address = [ "203.0.113.1/24" ];
|
||||
};
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
settings = {
|
||||
interface = "eth1";
|
||||
bind-interfaces = true;
|
||||
dhcp-range = "203.0.113.100,203.0.113.100,255.255.255.0,1h";
|
||||
dhcp-option = [ "option:router,203.0.113.1" ];
|
||||
};
|
||||
};
|
||||
# What a dyndns2 provider answers; the access log is the test's record.
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts.default = {
|
||||
default = true;
|
||||
locations."/nic/update".return = ''200 "good $arg_myip"'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
router =
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.router ];
|
||||
virtualisation.vlans = [
|
||||
1
|
||||
2
|
||||
];
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ];
|
||||
networking.interfaces.eth2.ipv4.addresses = lib.mkForce [ ];
|
||||
|
||||
router = {
|
||||
enable = true;
|
||||
wan.interface = "eth1";
|
||||
lans.main = {
|
||||
interface = "eth2";
|
||||
ipv4.address = "192.168.10.1";
|
||||
dhcp.pool = {
|
||||
start = "192.168.10.100";
|
||||
end = "192.168.10.199";
|
||||
};
|
||||
};
|
||||
ddns = {
|
||||
enable = true;
|
||||
protocol = "dyndns2";
|
||||
server = "203.0.113.1";
|
||||
ssl = false;
|
||||
username = "router";
|
||||
passwordFile = "/run/ddns-secret";
|
||||
domains = [
|
||||
"home.example.test"
|
||||
"vpn.example.test"
|
||||
];
|
||||
interval = "1h";
|
||||
};
|
||||
};
|
||||
|
||||
# The secret is a file on the router, as the web UI leaves it.
|
||||
systemd.tmpfiles.rules = [ "f+ /run/ddns-secret 0600 root root - hunter2" ];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
upstream.wait_for_unit("nginx.service")
|
||||
router.wait_for_unit("network-online.target")
|
||||
router.wait_until_succeeds("ip -4 addr show dev eth1 | grep -q 203.0.113.100")
|
||||
router.wait_for_unit("router-ddns-watch.service")
|
||||
|
||||
def updated(addr):
|
||||
return f"grep -q 'myip={addr}' /var/log/nginx/access.log"
|
||||
|
||||
with subtest("the first address is published, for every name"):
|
||||
upstream.wait_until_succeeds(updated("203.0.113.100"), timeout=90)
|
||||
out = upstream.succeed("cat /var/log/nginx/access.log")
|
||||
for name in ("home.example.test", "vpn.example.test"):
|
||||
assert f"hostname={name}" in out or name in out, out
|
||||
# The login arrives as basic auth, not in the URL, and the secret stays off the command line.
|
||||
router.fail("grep -r hunter2 /etc/ddclient* /nix/store/*ddclient.conf 2>/dev/null")
|
||||
|
||||
with subtest("a new address is published at once, not at the next timer"):
|
||||
router.succeed("ip addr add 203.0.113.101/24 dev eth1")
|
||||
router.succeed("ip addr del 203.0.113.100/24 dev eth1")
|
||||
upstream.wait_until_succeeds(updated("203.0.113.101"), timeout=60)
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user