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:
committed by
Brad Fitzpatrick
parent
822299642b
commit
9f343fdc0c
@@ -1040,6 +1040,30 @@ func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error)
|
|||||||
return &derpMap, nil
|
return &derpMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CertDomains returns the list of domains for which the local tailscaled can
|
||||||
|
// fetch TLS certificates, equivalent to the DNS.CertDomains field of the
|
||||||
|
// current netmap. The returned list is sorted in ascending order, and is
|
||||||
|
// empty if no netmap has been received yet.
|
||||||
|
func (lc *Client) CertDomains(ctx context.Context) ([]string, error) {
|
||||||
|
body, err := lc.get200(ctx, "/localapi/v0/cert-domains")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeJSON[[]string](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSConfig returns the [tailcfg.DNSConfig] from the current netmap.
|
||||||
|
// It returns an error if no netmap has been received yet.
|
||||||
|
// It is intended for callers that need fields like ExtraRecords or CertDomains
|
||||||
|
// without pulling the rest of the netmap.
|
||||||
|
func (lc *Client) DNSConfig(ctx context.Context) (*tailcfg.DNSConfig, error) {
|
||||||
|
body, err := lc.get200(ctx, "/localapi/v0/dns-config")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeJSON[*tailcfg.DNSConfig](body)
|
||||||
|
}
|
||||||
|
|
||||||
// PingOpts contains options for the ping request.
|
// PingOpts contains options for the ping request.
|
||||||
//
|
//
|
||||||
// The zero value is valid, which means to use defaults.
|
// The zero value is valid, which means to use defaults.
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ import (
|
|||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"k8s.io/client-go/util/homedir"
|
"k8s.io/client-go/util/homedir"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
"tailscale.com/ipn"
|
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/netmap"
|
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
)
|
)
|
||||||
@@ -98,12 +96,12 @@ func runConfigureKubeconfig(ctx context.Context, args []string) error {
|
|||||||
if st.BackendState != "Running" {
|
if st.BackendState != "Running" {
|
||||||
return errors.New("Tailscale is not running")
|
return errors.New("Tailscale is not running")
|
||||||
}
|
}
|
||||||
nm, err := getNetMap(ctx)
|
dnsCfg, err := getDNSConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
targetFQDN, err := nodeOrServiceDNSNameFromArg(st, nm, hostOrFQDNOrIP)
|
targetFQDN, err := nodeOrServiceDNSNameFromArg(st, dnsCfg, hostOrFQDNOrIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -240,14 +238,14 @@ func setKubeconfigForPeer(scheme, fqdn, filePath string) error {
|
|||||||
// nodeOrServiceDNSNameFromArg returns the PeerStatus.DNSName value from a peer
|
// nodeOrServiceDNSNameFromArg returns the PeerStatus.DNSName value from a peer
|
||||||
// in st that matches the input arg which can be a base name, full DNS name, or
|
// in st that matches the input arg which can be a base name, full DNS name, or
|
||||||
// an IP. If none is found, it looks for a Tailscale Service
|
// an IP. If none is found, it looks for a Tailscale Service
|
||||||
func nodeOrServiceDNSNameFromArg(st *ipnstate.Status, nm *netmap.NetworkMap, arg string) (string, error) {
|
func nodeOrServiceDNSNameFromArg(st *ipnstate.Status, dns *tailcfg.DNSConfig, arg string) (string, error) {
|
||||||
// First check for a node DNS name.
|
// First check for a node DNS name.
|
||||||
if dnsName, ok := nodeDNSNameFromArg(st, arg); ok {
|
if dnsName, ok := nodeDNSNameFromArg(st, arg); ok {
|
||||||
return dnsName, nil
|
return dnsName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not found, check for a Tailscale Service DNS name.
|
// If not found, check for a Tailscale Service DNS name.
|
||||||
rec, ok := serviceDNSRecordFromNetMap(nm, arg)
|
rec, ok := serviceDNSRecordFromDNSConfig(dns, arg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("no peer found for %q", arg)
|
return "", fmt.Errorf("no peer found for %q", arg)
|
||||||
}
|
}
|
||||||
@@ -269,25 +267,13 @@ func nodeOrServiceDNSNameFromArg(st *ipnstate.Status, nm *netmap.NetworkMap, arg
|
|||||||
return "", fmt.Errorf("%q is in MagicDNS, but is not currently reachable on any known peer", arg)
|
return "", fmt.Errorf("%q is in MagicDNS, but is not currently reachable on any known peer", arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNetMap(ctx context.Context) (*netmap.NetworkMap, error) {
|
func getDNSConfig(ctx context.Context) (*tailcfg.DNSConfig, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
return localClient.DNSConfig(ctx)
|
||||||
watcher, err := localClient.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer watcher.Close()
|
|
||||||
|
|
||||||
n, err := watcher.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return n.NetMap, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceDNSRecordFromNetMap(nm *netmap.NetworkMap, arg string) (rec tailcfg.DNSRecord, ok bool) {
|
func serviceDNSRecordFromDNSConfig(dns *tailcfg.DNSConfig, arg string) (rec tailcfg.DNSRecord, ok bool) {
|
||||||
argIP, _ := netip.ParseAddr(arg)
|
argIP, _ := netip.ParseAddr(arg)
|
||||||
argFQDN, err := dnsname.ToFQDN(arg)
|
argFQDN, err := dnsname.ToFQDN(arg)
|
||||||
argFQDNValid := err == nil
|
argFQDNValid := err == nil
|
||||||
@@ -295,7 +281,7 @@ func serviceDNSRecordFromNetMap(nm *netmap.NetworkMap, arg string) (rec tailcfg.
|
|||||||
return rec, false
|
return rec, false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rec := range nm.DNS.ExtraRecords {
|
for _, rec := range dns.ExtraRecords {
|
||||||
if argIP.IsValid() {
|
if argIP.IsValid() {
|
||||||
recIP, _ := netip.ParseAddr(rec.Value)
|
recIP, _ := netip.ParseAddr(rec.Value)
|
||||||
if recIP == argIP {
|
if recIP == argIP {
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"tailscale.com/cmd/tailscale/cli/jsonoutput"
|
"tailscale.com/cmd/tailscale/cli/jsonoutput"
|
||||||
"tailscale.com/ipn"
|
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/netmap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var dnsStatusCmd = &ffcli.Command{
|
var dnsStatusCmd = &ffcli.Command{
|
||||||
@@ -120,11 +118,10 @@ func runDNSStatus(ctx context.Context, args []string) error {
|
|||||||
SelfDNSName: s.Self.DNSName,
|
SelfDNSName: s.Self.DNSName,
|
||||||
}
|
}
|
||||||
|
|
||||||
netMap, err := fetchNetMap()
|
dnsConfig, err := localClient.DNSConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch network map: %w", err)
|
return fmt.Errorf("failed to fetch DNS config: %w", err)
|
||||||
}
|
}
|
||||||
dnsConfig := netMap.DNS
|
|
||||||
|
|
||||||
for _, r := range dnsConfig.Resolvers {
|
for _, r := range dnsConfig.Resolvers {
|
||||||
data.Resolvers = append(data.Resolvers, makeDNSResolverInfo(r))
|
data.Resolvers = append(data.Resolvers, makeDNSResolverInfo(r))
|
||||||
@@ -357,19 +354,3 @@ func formatDNSStatusText(data *jsonoutput.DNSStatusResult, all bool) string {
|
|||||||
fmt.Fprintf(&sb, "[this is a preliminary version of this command; the output format may change in the future]\n")
|
fmt.Fprintf(&sb, "[this is a preliminary version of this command; the output format may change in the future]\n")
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNetMap() (netMap *netmap.NetworkMap, err error) {
|
|
||||||
w, err := localClient.WatchIPNBus(context.Background(), ipn.NotifyInitialNetMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer w.Close()
|
|
||||||
notify, err := w.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if notify.NetMap == nil {
|
|
||||||
return nil, fmt.Errorf("no network map yet available, please try again later")
|
|
||||||
}
|
|
||||||
return notify.NetMap, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -72,9 +72,11 @@ var handler = map[string]LocalAPIHandler{
|
|||||||
|
|
||||||
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
||||||
// without a trailing slash:
|
// without a trailing slash:
|
||||||
|
"cert-domains": (*Handler).serveCertDomains,
|
||||||
"check-prefs": (*Handler).serveCheckPrefs,
|
"check-prefs": (*Handler).serveCheckPrefs,
|
||||||
"check-so-mark-in-use": (*Handler).serveCheckSOMarkInUse,
|
"check-so-mark-in-use": (*Handler).serveCheckSOMarkInUse,
|
||||||
"derpmap": (*Handler).serveDERPMap,
|
"derpmap": (*Handler).serveDERPMap,
|
||||||
|
"dns-config": (*Handler).serveDNSConfig,
|
||||||
"goroutines": (*Handler).serveGoroutines,
|
"goroutines": (*Handler).serveGoroutines,
|
||||||
"login-interactive": (*Handler).serveLoginInteractive,
|
"login-interactive": (*Handler).serveLoginInteractive,
|
||||||
"logout": (*Handler).serveLogout,
|
"logout": (*Handler).serveLogout,
|
||||||
@@ -1073,6 +1075,41 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
|
|||||||
e.Encode(h.b.DERPMap())
|
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
|
// serveSetExpirySooner sets the expiry date on the current machine, specified
|
||||||
// by an `expiry` unix timestamp as POST or query param.
|
// by an `expiry` unix timestamp as POST or query param.
|
||||||
func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
+11
-2
@@ -173,6 +173,12 @@ func (cm *CertManager) runCertLoop(ctx context.Context, domain string) {
|
|||||||
|
|
||||||
// waitForCertDomain ensures the requested domain is in the list of allowed
|
// waitForCertDomain ensures the requested domain is in the list of allowed
|
||||||
// domains before issuing the cert for the first time.
|
// domains before issuing the cert for the first time.
|
||||||
|
// It uses the IPN bus only as a wake-up trigger and queries the current cert
|
||||||
|
// domains explicitly via [LocalClient.CertDomains].
|
||||||
|
//
|
||||||
|
// TODO(bradfitz): once Notify.SelfChange lands upstream, switch this to
|
||||||
|
// watch for SelfChange events instead of NotifyInitialNetMap, and drop the
|
||||||
|
// netmap dependency on the bus entirely.
|
||||||
func (cm *CertManager) waitForCertDomain(ctx context.Context, domain string) error {
|
func (cm *CertManager) waitForCertDomain(ctx context.Context, domain string) error {
|
||||||
w, err := cm.lc.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
|
w, err := cm.lc.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,8 +194,11 @@ func (cm *CertManager) waitForCertDomain(ctx context.Context, domain string) err
|
|||||||
if n.NetMap == nil {
|
if n.NetMap == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
domains, err := cm.lc.CertDomains(ctx)
|
||||||
if slices.Contains(n.NetMap.DNS.CertDomains, domain) {
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slices.Contains(domains, domain) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-11
@@ -201,18 +201,12 @@ func TestEnsureCertLoops(t *testing.T) {
|
|||||||
|
|
||||||
notifyChan := make(chan ipn.Notify)
|
notifyChan := make(chan ipn.Notify)
|
||||||
go func() {
|
go func() {
|
||||||
|
// Drive waitForCertDomain by sending notifications
|
||||||
|
// with empty netmaps as wake-up triggers; the cert
|
||||||
|
// manager queries CertDomains via the local
|
||||||
|
// client and not by reading the bus payload.
|
||||||
for {
|
for {
|
||||||
notifyChan <- ipn.Notify{
|
notifyChan <- ipn.Notify{NetMap: &netmap.NetworkMap{}}
|
||||||
NetMap: &netmap.NetworkMap{
|
|
||||||
DNS: tailcfg.DNSConfig{
|
|
||||||
CertDomains: []string{
|
|
||||||
"my-app.tailnetxyz.ts.net",
|
|
||||||
"my-other-app.tailnetxyz.ts.net",
|
|
||||||
"my-apiserver.tailnetxyz.ts.net",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
cm := &CertManager{
|
cm := &CertManager{
|
||||||
@@ -220,6 +214,11 @@ func TestEnsureCertLoops(t *testing.T) {
|
|||||||
FakeIPNBusWatcher: localclient.FakeIPNBusWatcher{
|
FakeIPNBusWatcher: localclient.FakeIPNBusWatcher{
|
||||||
NotifyChan: notifyChan,
|
NotifyChan: notifyChan,
|
||||||
},
|
},
|
||||||
|
CertDomainsResult: []string{
|
||||||
|
"my-app.tailnetxyz.ts.net",
|
||||||
|
"my-other-app.tailnetxyz.ts.net",
|
||||||
|
"my-apiserver.tailnetxyz.ts.net",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
logf: log.Printf,
|
logf: log.Printf,
|
||||||
certLoops: make(map[string]context.CancelFunc),
|
certLoops: make(map[string]context.CancelFunc),
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
|
|
||||||
type FakeLocalClient struct {
|
type FakeLocalClient struct {
|
||||||
FakeIPNBusWatcher
|
FakeIPNBusWatcher
|
||||||
SetServeCalled bool
|
SetServeCalled bool
|
||||||
EditPrefsCalls []*ipn.MaskedPrefs
|
EditPrefsCalls []*ipn.MaskedPrefs
|
||||||
GetPrefsResult *ipn.Prefs
|
GetPrefsResult *ipn.Prefs
|
||||||
|
CertDomainsResult []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FakeLocalClient) SetServeConfig(ctx context.Context, cfg *ipn.ServeConfig) error {
|
func (m *FakeLocalClient) SetServeConfig(ctx context.Context, cfg *ipn.ServeConfig) error {
|
||||||
@@ -45,6 +46,10 @@ func (f *FakeLocalClient) CertPair(ctx context.Context, domain string) ([]byte,
|
|||||||
return nil, nil, fmt.Errorf("CertPair not implemented")
|
return nil, nil, fmt.Errorf("CertPair not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeLocalClient) CertDomains(ctx context.Context) ([]string, error) {
|
||||||
|
return f.CertDomainsResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
type FakeIPNBusWatcher struct {
|
type FakeIPNBusWatcher struct {
|
||||||
NotifyChan chan ipn.Notify
|
NotifyChan chan ipn.Notify
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type LocalClient interface {
|
|||||||
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (IPNBusWatcher, error)
|
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (IPNBusWatcher, error)
|
||||||
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
||||||
EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
|
EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
|
||||||
|
CertDomains(ctx context.Context) ([]string, error)
|
||||||
CertIssuer
|
CertIssuer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,3 +58,7 @@ func (lc *localClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt)
|
|||||||
func (lc *localClient) CertPair(ctx context.Context, domain string) ([]byte, []byte, error) {
|
func (lc *localClient) CertPair(ctx context.Context, domain string) ([]byte, []byte, error) {
|
||||||
return lc.lc.CertPair(ctx, domain)
|
return lc.lc.CertPair(ctx, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (lc *localClient) CertDomains(ctx context.Context) ([]string, error) {
|
||||||
|
return lc.lc.CertDomains(ctx)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user