Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5 c47ae5a518 fix(edge-host): track the NTP anycast address that only existed on the host
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
/etc/systemd/network/10-enp1s0.40.network on edge-1 carries the internal
NTP service address:

  [Address]
  Address=fd40::123/128
  DuplicateAddressDetection=none

That stanza is not in this repository and never has been. Verified:

  git log --all -S 'fd40::123'              -> no commits
  git log --all -S 'DuplicateAddressDetection' -> no commits
  git log --all -S 'anycast'                -> no commits

The role renders that exact path with ansible.builtin.template
(roles/edge-host/tasks/main.yml:188), which overwrites wholesale, and it is
gated on edge_vlan40_enabled, which is true for this host. So the next run
of the edge-host role silently deletes the address and drops edge-1 out of
the NTP anycast service -- no error, no warning, nothing in the diff to
suggest it happened. The on-host file was written 2026-07-19 23:36:51,
about two hours after that evening's role run.

This is the modern NTP service path. ntp.srv.renner.uno resolves to
fd40::123 (v6) and to the keepalived VIP (v4, legacy). The v6 side has no
VRRP and no VIP: every server carries the same address and clients reach
whichever answers neighbour discovery first.

This commit only captures what is already live, verbatim, including the
RFC 4862 reasoning for disabling DAD. No behaviour change: applying the
role now reproduces the current on-host file instead of destroying it.

Parameterised as edge_ntp_anycast_address so it can be set per host, and
set to an empty string to omit the address entirely.

Not addressed here: nothing gates this address on clock health. networkd
assigns it at boot unconditionally, so a host whose clock is wrong answers
NTP anyway -- which is what happened on 2026-08-04 while edge-1's clock was
~16 days out. Under anycast, withdrawing the address on an unhealthy host
is the correct response and does not interrupt the service, provided more
than one host carries it. Raised separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:47:04 +02:00
2 changed files with 25 additions and 0 deletions
@@ -116,3 +116,11 @@ edge_vlan40_parent: enp1s0
edge_vlan40_id: 40
edge_vlan40_name: enp1s0.40
edge_vlan40_parent_network: 20-wired.network
# Shared IPv6 anycast address for the internal NTP service
# (ntp.srv.renner.uno). Every NTP server carries this same address; clients
# reach whichever host answers neighbour discovery first. This is the modern
# service path -- the keepalived VIP is IPv4-only and therefore legacy.
#
# Set to an empty string to omit the address entirely.
edge_ntp_anycast_address: "fd40::123/128"
@@ -33,3 +33,20 @@ UseHostname=false
[IPv6AcceptRA]
UseGateway=false
UseDNS=false
{% if edge_ntp_anycast_address %}
# Shared IPv6 anycast address for NTP. Every NTP server carries this same
# address; clients reach whichever answers neighbour discovery first.
#
# DuplicateAddressDetection=none is required, not an optimisation. RFC 4862
# section 5.4: "Duplicate Address Detection MUST NOT be performed on anycast
# addresses (note that anycast addresses cannot syntactically be distinguished
# from unicast addresses)." Because the stack cannot tell anycast from unicast,
# it has to be told -- this is how. Left at the default of "ipv6", the second
# host to boot would lose the DAD race, refuse to assign the address
# (RFC 4862 section 5.4.5) and only log it, leaving a cluster that looks
# healthy but has one silent non-member.
[Address]
Address={{ edge_ntp_anycast_address }}
DuplicateAddressDetection=none
{% endif %}