1 Commits
Author SHA1 Message Date
sneak f20b7802b1 watcher: follow a watched name's CNAME for port and TLS checks (closes #203)
check / check (push) Failing after 2m24s
When a watched name's nameservers answer with a CNAME and no address,
the DNS check follows every CNAME target they gave with
ResolveIPAddresses and saves the addresses found for all of them in the
hostname state as cnameAddresses, so nameservers that disagree on the
target do not change them from check to check. The port and TLS checks
use them. A change in them is notified as a CNAME address change, also
from or to none. A state file without the field loads them as not known
(nil), so its first check sends nothing for them. When a target cannot
be followed, or none of the name's nameservers answered, the last
check's addresses are kept. The domain check now runs the hostname
check for the apex instead of a copy of it.

Model: opus-5-5
2026-10-02 02:12:18 +00:00
+12 -6
View File
@@ -63,7 +63,7 @@ func TestCNAMEThatCannotBeFollowedKeepsPrevious(t *testing.T) {
) )
current := hostnameState(map[string]map[string][]string{ current := hostnameState(map[string]map[string][]string{
nsA: {"CNAME": {"target.example.invalid."}}, nsA: cnameTo("target.example.invalid."),
}) })
prev := &state.HostnameState{CNAMEAddresses: []string{oldIP}} prev := &state.HostnameState{CNAMEAddresses: []string{oldIP}}
@@ -123,8 +123,8 @@ func TestCNAMEAddressesOfEveryTarget(t *testing.T) {
t.Parallel() t.Parallel()
found := followLive(t, map[string]map[string][]string{ found := followLive(t, map[string]map[string][]string{
nsA: {"CNAME": {"one.one.one.one."}}, nsA: cnameTo("one.one.one.one."),
nsB: {"CNAME": {"dns.google."}}, nsB: cnameTo("dns.google."),
}) })
for _, ip := range []string{"1.1.1.1", "8.8.8.8"} { for _, ip := range []string{"1.1.1.1", "8.8.8.8"} {
@@ -141,7 +141,7 @@ func TestCNAMEChainEndingInNoAddressSavesEmptyList(t *testing.T) {
t.Parallel() t.Parallel()
found := followLive(t, map[string]map[string][]string{ found := followLive(t, map[string]map[string][]string{
nsA: {"CNAME": {"this-surely-does-not-exist-xyz.google.com."}}, nsA: cnameTo("this-surely-does-not-exist-xyz.google.com."),
}) })
if found == nil || len(found) != 0 { if found == nil || len(found) != 0 {
@@ -160,7 +160,7 @@ func TestCNAMEBesideAnAddressNotFollowed(t *testing.T) {
current := hostnameState(map[string]map[string][]string{ current := hostnameState(map[string]map[string][]string{
nsA: {"A": {ip1}}, nsA: {"A": {ip1}},
nsB: {"CNAME": {"target.example.org."}}, nsB: cnameTo("target.example.org."),
}) })
w.ResolveCNAMEAddresses(t.Context(), host, current, nil) w.ResolveCNAMEAddresses(t.Context(), host, current, nil)
@@ -194,12 +194,18 @@ func TestCNAMEWhoseNameserversAllFailedKeepsPrevious(t *testing.T) {
} }
} }
// cnameTo builds the records of a nameserver that answered with a CNAME
// to target and no address.
func cnameTo(target string) map[string][]string {
return map[string][]string{"CNAME": {target}}
}
// cnameState builds the state a check leaves behind for a name whose // cnameState builds the state a check leaves behind for a name whose
// nameserver answered with a CNAME and no address, when following the // nameserver answered with a CNAME and no address, when following the
// CNAME found these addresses, which may be none. // CNAME found these addresses, which may be none.
func cnameState(addresses ...string) *state.HostnameState { func cnameState(addresses ...string) *state.HostnameState {
hs := hostnameState(map[string]map[string][]string{ hs := hostnameState(map[string]map[string][]string{
nsA: {"CNAME": {"target.example.org."}}, nsA: cnameTo("target.example.org."),
}) })
hs.CNAMEAddresses = append([]string{}, addresses...) hs.CNAMEAddresses = append([]string{}, addresses...)