From 77f8ee4024cec656db8f097b51b712f11d3d12e4 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 17:35:31 +0700 Subject: [PATCH 1/4] checker: build host FQDN from subdomain + apex at service scope --- checker/collect.go | 18 +++++++++++++++++- checker/definition.go | 6 ++++++ checker/provider_test.go | 2 +- checker/service.go | 11 ++++++----- checker/types.go | 1 + 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/checker/collect.go b/checker/collect.go index 153ab32..7e1f4b5 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -21,6 +21,7 @@ import ( "time" sdk "git.happydns.org/checker-sdk-go/checker" + happydns "git.happydns.org/happyDomain/model" "golang.org/x/net/html" ) @@ -143,7 +144,22 @@ func buildTarget(ctx context.Context, opts sdk.CheckerOptions) (Target, error) { userAgent = v } - host, ips := addressesFromServer(server) + // Origin is the FQDN where the service is mounted: svc.Domain holds the + // subdomain (relative to apex; "@" for apex), and the domain_name + // autofill carries the zone apex. + apex := "" + if v, ok := sdk.GetOption[string](opts, OptionDomainName); ok { + apex = strings.TrimSuffix(v, ".") + } + subdomain := "" + if svc, ok := sdk.GetOption[happydns.ServiceMessage](opts, OptionService); ok { + subdomain = strings.TrimSuffix(svc.Domain, ".") + } + origin := sdk.JoinRelative(subdomain, apex) + host, ips := addressesFromServer(server, origin) + if host == "" { + host = origin + } // abstract.Server only pins one A and one AAAA. Resolve the host to // pick up any additional records the authoritative DNS exposes, so // multi-IP deployments aren't silently under-probed. Failures are diff --git a/checker/definition.go b/checker/definition.go index 74b1a3f..0aeaffa 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -82,6 +82,12 @@ func (p *httpProvider) Definition() *sdk.CheckerDefinition { AutoFill: sdk.AutoFillService, Hide: true, }, + { + Id: OptionDomainName, + Label: "Parent domain name", + AutoFill: sdk.AutoFillDomainName, + Hide: true, + }, }, }, Rules: Rules(), diff --git a/checker/provider_test.go b/checker/provider_test.go index feb9f0c..86a5bf0 100644 --- a/checker/provider_test.go +++ b/checker/provider_test.go @@ -227,7 +227,7 @@ func TestAddressesFromServer(t *testing.T) { } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - host, ips := addressesFromServer(c.srv) + host, ips := addressesFromServer(c.srv, "") if host != c.wantHost { t.Errorf("host = %q, want %q", host, c.wantHost) } diff --git a/checker/service.go b/checker/service.go index f4fbb9c..0ec5c87 100644 --- a/checker/service.go +++ b/checker/service.go @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "net" - "strings" sdk "git.happydns.org/checker-sdk-go/checker" happydns "git.happydns.org/happyDomain/model" @@ -36,15 +35,17 @@ func resolveServer(opts sdk.CheckerOptions) (*abstract.Server, error) { return &server, nil } -// addressesFromServer returns the (host, ips) tuple to probe. -func addressesFromServer(server *abstract.Server) (host string, ips []string) { +// addressesFromServer returns the (host, ips) tuple to probe. origin is +// the service's parent zone; the A/AAAA Hdr.Name is relative to it as +// happyDomain encodes service owners, so we must join before using as FQDN. +func addressesFromServer(server *abstract.Server, origin string) (host string, ips []string) { if server.A != nil && len(server.A.A) > 0 { - host = strings.TrimSuffix(server.A.Hdr.Name, ".") + host = sdk.JoinRelative(server.A.Hdr.Name, origin) ips = append(ips, server.A.A.String()) } if server.AAAA != nil && len(server.AAAA.AAAA) > 0 { if host == "" { - host = strings.TrimSuffix(server.AAAA.Hdr.Name, ".") + host = sdk.JoinRelative(server.AAAA.Hdr.Name, origin) } ips = append(ips, server.AAAA.AAAA.String()) } diff --git a/checker/types.go b/checker/types.go index 2c11eac..142c653 100644 --- a/checker/types.go +++ b/checker/types.go @@ -24,6 +24,7 @@ const ObservationKeyHTTP = "http" const ( OptionService = "service" + OptionDomainName = "domain_name" OptionProbeTimeoutMs = "probeTimeoutMs" OptionMaxRedirects = "maxRedirects" OptionUserAgent = "userAgent" From 4be2bc93436c8b0fe6ab816511a5902e4c5ba0fd Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 30 Apr 2026 08:57:39 +0700 Subject: [PATCH 2/4] Update rules section --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 79ac2ad..ca92a97 100644 --- a/README.md +++ b/README.md @@ -9,26 +9,30 @@ Deep TLS / certificate analysis is intentionally **delegated to [checker-tls](https://git.happydns.org/checker-tls)** - this checker only relies on TLS for transport. -## What it checks +## Rules -| Rule | What it verifies | -| --------------------------------- | --------------------------------------------------------------------------------- | -| `http.tcp_reachable` | Port 80 accepts connections on every A/AAAA address. | -| `https.tcp_reachable` | Port 443 accepts connections on every A/AAAA address. | -| `http.https_redirect` | Plain HTTP redirects to HTTPS (warning if not). | -| `http.hsts` | `Strict-Transport-Security` is present with a sufficient `max-age`. | -| `http.csp` | `Content-Security-Policy` is set; flags `'unsafe-inline'` / `'unsafe-eval'`. | -| `http.x_frame_options` | `X-Frame-Options` or CSP `frame-ancestors` provides clickjacking protection. | -| `http.x_content_type_options` | `X-Content-Type-Options: nosniff` is set. | -| `http.x_xss_protection` | Reports the legacy `X-XSS-Protection` header (recommendation: disable). | -| `http.referrer_policy` | `Referrer-Policy` is set to a privacy-preserving value (W3C Referrer Policy). | -| `http.permissions_policy` | `Permissions-Policy` is set (W3C Permissions Policy, replaces Feature-Policy). | -| `http.coop` | `Cross-Origin-Opener-Policy` isolates the document from cross-origin windows. | -| `http.coep` | `Cross-Origin-Embedder-Policy` requires CORP/CORS opt-in for embedded resources. | -| `http.corp` | `Cross-Origin-Resource-Policy` restricts cross-origin embedding of responses. | -| `http.cookie_flags` | Every Set-Cookie has `Secure`, `HttpOnly`, and a `SameSite` attribute. | -| `http.sri` | Cross-origin `