client/local, ipn/localapi, all: add CertDomains and DNSConfig accessors

Add two narrow LocalAPI accessors so callers don't have to subscribe to
the IPN bus and pull a full *netmap.NetworkMap just to read DNS-shaped
fields:

  - GET /localapi/v0/cert-domains returns DNS.CertDomains.
  - GET /localapi/v0/dns-config returns the full tailcfg.DNSConfig.

Migrate in-tree callers off the netmap-on-the-bus pattern:

  - kube/certs.waitForCertDomain still wakes on the IPN bus but now
    queries CertDomains via LocalClient.CertDomains rather than
    reading n.NetMap.DNS.CertDomains. The kube LocalClient interface
    and FakeLocalClient gain a CertDomains method.
  - cmd/tailscale dns status calls LocalClient.DNSConfig directly
    instead of opening a NotifyInitialNetMap watcher.
  - cmd/tailscale configure kubeconfig switches from a netmap watcher
    + serviceDNSRecordFromNetMap to LocalClient.DNSConfig +
    serviceDNSRecordFromDNSConfig.

This is part of a series moving callers away from depending on the
netmap traveling on the IPN bus, so the bus payload can shrink in a
later change.

Updates #12542

Change-Id: Ie10204e141d085fbac183b4cfe497226b670ad6c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-30 19:34:20 +00:00
committed by Brad Fitzpatrick
parent 822299642b
commit 9f343fdc0c
8 changed files with 105 additions and 59 deletions
+37
View File
@@ -72,9 +72,11 @@ var handler = map[string]LocalAPIHandler{
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
// without a trailing slash:
"cert-domains": (*Handler).serveCertDomains,
"check-prefs": (*Handler).serveCheckPrefs,
"check-so-mark-in-use": (*Handler).serveCheckSOMarkInUse,
"derpmap": (*Handler).serveDERPMap,
"dns-config": (*Handler).serveDNSConfig,
"goroutines": (*Handler).serveGoroutines,
"login-interactive": (*Handler).serveLoginInteractive,
"logout": (*Handler).serveLogout,
@@ -1073,6 +1075,41 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
e.Encode(h.b.DERPMap())
}
// serveCertDomains returns the list of DNS.CertDomains from the current
// netmap, or an empty list if no netmap has been received yet.
// The returned list is sorted in ascending order.
func (h *Handler) serveCertDomains(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "cert-domains access denied", http.StatusForbidden)
return
}
var domains []string
if nm := h.b.NetMapNoPeers(); nm != nil {
domains = slices.Clone(nm.DNS.CertDomains)
slices.Sort(domains)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(domains)
}
// serveDNSConfig returns the [tailcfg.DNSConfig] from the current netmap.
// It returns 503 if no netmap has been received yet.
func (h *Handler) serveDNSConfig(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "dns-config access denied", http.StatusForbidden)
return
}
nm := h.b.NetMapNoPeers()
if nm == nil {
http.Error(w, "no netmap", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(nm.DNS)
}
// serveSetExpirySooner sets the expiry date on the current machine, specified
// by an `expiry` unix timestamp as POST or query param.
func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {