From a593df3bb49dceae86cd96f7baab74b99c3728a1 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 29 Oct 2026 14:31:36 +0100 Subject: [PATCH 1/2] fix(dns): add A for ntp.srv.renner.uno (legacy IPv4 VIP) ntp.srv.renner.uno carries only the AAAA fd40::123, so IPv4-only clients behind ntp.renner.uno get no address. The keepalived IPv4 VIP 10.2.40.4 answers NTP; IoT and Server share the Internal zone, where UDP 123 is allowed. Co-Authored-By: Claude Opus 5.5 --- opentofu/dns_renner_uno.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/opentofu/dns_renner_uno.tf b/opentofu/dns_renner_uno.tf index 9acc1e12..81daf2d5 100644 --- a/opentofu/dns_renner_uno.tf +++ b/opentofu/dns_renner_uno.tf @@ -25,6 +25,10 @@ locals { # Talos K8s control-plane VIP (etcd-elected, see kubernetes/talos/patches/k8s-{1,2,3}.yaml) "k8s" = { type = "A", value = "10.2.40.60" } + # NTP legacy IPv4 VIP (keepalived_ntp_vip, ansible/roles/keepalived-ntp). + # IPv4-only clients (Shelly Gen1) cannot use the AAAA. + "ntp.srv" = { type = "A", value = "10.2.40.4" } + # AAAA Records # UniFi auto-creates only the A record for the UDM's own hostname # (udm -> 192.168.8.1) and forwards AAAA queries upstream, where the -- 2.54.0 From 1efbfd1424a8c5f51324e625625515119d173846 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 29 Oct 2026 14:38:04 +0100 Subject: [PATCH 2/2] fix(ntp): move the IPv6 service address to fd02:40::123 fd40::/64 is the legacy VLAN 40 ULA; fd02:40::/64 is current. edge-1 takes RAs on enp1s0.40, so the prefix is on-link there. The AAAA for ntp.srv.renner.uno moves into OpenTofu next to the A. Records gain an optional name so one host can carry both types without re-keying the existing ones. Co-Authored-By: Claude Opus 5.5 --- ansible/inventory/group_vars/ntp_cluster.yml | 2 +- ansible/inventory/pve_hosts.yml | 2 +- ansible/plays/install-ntp-ha.yml | 2 +- ansible/roles/chrony-ntp-server/defaults/main.yml | 2 +- ansible/roles/keepalived-ntp/tasks/main.yml | 6 +++--- opentofu/dns_renner_uno.tf | 10 ++++++---- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/ansible/inventory/group_vars/ntp_cluster.yml b/ansible/inventory/group_vars/ntp_cluster.yml index 756fa498..8dcdc14d 100644 --- a/ansible/inventory/group_vars/ntp_cluster.yml +++ b/ansible/inventory/group_vars/ntp_cluster.yml @@ -8,7 +8,7 @@ # two plays over this group -- withdraw the static assignments fleet-wide, then # hand the address to keepalived -- and both need the same value. Duplicating a # service address across plays is exactly the sort of thing that drifts. -ntp_vip6: "fd40::123/128" +ntp_vip6: "fd02:40::123/128" # Link-local address carried as the FIRST virtual_ipaddress of the IPv6 VRRP # instance. RFC 5798 gives an IPv6 virtual router a link-local primary, and diff --git a/ansible/inventory/pve_hosts.yml b/ansible/inventory/pve_hosts.yml index 0c67e913..9fe9c028 100644 --- a/ansible/inventory/pve_hosts.yml +++ b/ansible/inventory/pve_hosts.yml @@ -55,7 +55,7 @@ heating_can # THIS IS NO LONGER HIGHLY AVAILABLE. The group was jupiter (MASTER, prio 100), # saturn (90), neptune (80) and edge-1 (BACKUP, 70); the first three are gone. # A single VRRP member takes the address regardless of its configured state, so -# fd40::123 is still served — verified answering — but by one thin client with +# the service address is still served, but by one thin client with # no failover behind it. install-ntp-ha.yml still carries the four-host # ntp_servers priority map, which now describes hosts that do not exist. # diff --git a/ansible/plays/install-ntp-ha.yml b/ansible/plays/install-ntp-ha.yml index 619bc295..c742af64 100644 --- a/ansible/plays/install-ntp-ha.yml +++ b/ansible/plays/install-ntp-ha.yml @@ -62,7 +62,7 @@ # One node at a time. The keepalived role withdraws the statically assigned # IPv6 service address and restarts keepalived so the address is re-taken # under VRRP control; doing that across every NTP server at once would leave - # the whole fleet with nobody holding fd40::123 until the elections settle. + # the whole fleet with nobody holding the service address until the elections settle. # Serialised, the remaining members keep answering throughout. serial: 1 diff --git a/ansible/roles/chrony-ntp-server/defaults/main.yml b/ansible/roles/chrony-ntp-server/defaults/main.yml index 80dcfd2c..4b5e367c 100644 --- a/ansible/roles/chrony-ntp-server/defaults/main.yml +++ b/ansible/roles/chrony-ntp-server/defaults/main.yml @@ -4,7 +4,7 @@ # Networks allowed to use this server as NTP source. # # BOTH families are required. IPv6 is the primary address family here -- the -# service address ntp.srv.renner.uno resolves to fd40::123 -- so an IPv4-only +# service address ntp.srv.renner.uno resolves to fd02:40::123 -- so an IPv4-only # allow list refuses every client that uses the documented name. # # The hosts carried `allow fd00::/8` for a long time without it ever being in diff --git a/ansible/roles/keepalived-ntp/tasks/main.yml b/ansible/roles/keepalived-ntp/tasks/main.yml index ff88dba3..ef28f781 100644 --- a/ansible/roles/keepalived-ntp/tasks/main.yml +++ b/ansible/roles/keepalived-ntp/tasks/main.yml @@ -99,7 +99,7 @@ # quietly proceed either. install-edge-host.yml is a separate play with no # ordering relationship to this one, so nothing guarantees it has been re-run # to drop the [Address] block. If it has not, edge-1 would end up holding -# fd40::123 statically AND running a VRRP instance for the same address: +# the service address statically AND running a VRRP instance for the same address: # keepalived would believe it had withdrawn the address while networkd kept it # assigned, and the host would go on answering NTP with a clock the health # check had already rejected. @@ -107,9 +107,9 @@ # That is the precise failure of 2026-08-04, on the host it happened to, so it # fails fast rather than being left to inspection. # Matched on the bare address, not the full address/prefixlen, and anchored -# with \b so it cannot also hit a longer address sharing the prefix (fd40::1234). +# with \b so it cannot also hit a longer address sharing the prefix (fd02:40::1234). # -# Searching for the literal "fd40::123/128" would fail open: a hand-written +# Searching for the literal "fd02:40::123/128" would fail open: a hand-written # [Address] using any other mask -- /64, say -- would not match, the assert # below would pass, and keepalived would be handed an address networkd is # still assigning. Since this check is detect-only, with no delete afterwards diff --git a/opentofu/dns_renner_uno.tf b/opentofu/dns_renner_uno.tf index 2ba2cb98..d7e83fa8 100644 --- a/opentofu/dns_renner_uno.tf +++ b/opentofu/dns_renner_uno.tf @@ -25,9 +25,11 @@ locals { # Talos K8s control-plane VIP (etcd-elected, see kubernetes/talos/patches/k8s-{1,2,3}.yaml) "k8s" = { type = "A", value = "10.2.40.60" } - # NTP legacy IPv4 VIP (keepalived_ntp_vip, ansible/roles/keepalived-ntp). - # IPv4-only clients (Shelly Gen1) cannot use the AAAA. - "ntp.srv" = { type = "A", value = "10.2.40.4" } + # NTP VIPs held by keepalived (ansible/roles/keepalived-ntp): IPv4 + # keepalived_ntp_vip, IPv6 ntp_vip6 (group_vars/ntp_cluster.yml). + # IPv4-only clients (Shelly Gen1) need the A. + "ntp.srv" = { type = "A", value = "10.2.40.4" } + "ntp.srv/AAAA" = { name = "ntp.srv", type = "AAAA", value = "fd02:40::123" } # AAAA Records # UniFi auto-creates only the A record for the UDM's own hostname @@ -47,7 +49,7 @@ locals { resource "unifi_dns_record" "renner_uno" { for_each = local.renner_uno_records - name = each.key == "@" ? "renner.uno" : "${each.key}.renner.uno" + name = each.key == "@" ? "renner.uno" : "${try(each.value.name, each.key)}.renner.uno" record_type = each.value.type value = each.value.value enabled = true -- 2.54.0