WIP: rebase for 2026-05-18 #7
@@ -607,6 +607,24 @@ func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, erro
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// GetDebugResultJSON invokes a debug action and decodes the JSON response
|
||||
// into a value of type T. It avoids the marshal/unmarshal roundtrip that
|
||||
// callers of [Client.DebugResultJSON] otherwise need to do to get a typed
|
||||
// value.
|
||||
//
|
||||
// These are development tools and subject to change or removal over time.
|
||||
func GetDebugResultJSON[T any](ctx context.Context, lc *Client, action string) (T, error) {
|
||||
var v T
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
if err := json.Unmarshal(body, &v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// QueryOptionalFeatures queries the optional features supported by the Tailscale daemon.
|
||||
func (lc *Client) QueryOptionalFeatures(ctx context.Context) (*apitype.OptionalFeatures, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-optional-features", 200, nil)
|
||||
|
||||
@@ -63,7 +63,7 @@ func visitDoctor(ctx context.Context, b *ipnlocal.LocalBackend, logf logger.Logf
|
||||
// IPs; this can interfere with our ability to connect to the Tailscale
|
||||
// controlplane.
|
||||
checks = append(checks, doctor.CheckFunc("dns-resolvers", func(_ context.Context, logf logger.Logf) error {
|
||||
nm := b.NetMap()
|
||||
nm := b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -909,7 +909,7 @@ func (b *LocalBackend) resolveCertDomain(domain string) (string, error) {
|
||||
}
|
||||
|
||||
// Read the netmap once to get both CertDomains and capabilities atomically.
|
||||
nm := b.NetMap()
|
||||
nm := b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return "", errors.New("no netmap available")
|
||||
}
|
||||
|
||||
+44
-5
@@ -701,7 +701,9 @@ func (b *LocalBackend) onHomeDERPUpdateLocked(du magicsock.HomeDERPChanged) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.writeNetmapToDiskLocked(b.NetMap()); err != nil {
|
||||
// Persist the full netmap (including up-to-date Peers) to disk for
|
||||
// fast restart.
|
||||
if err := b.writeNetmapToDiskLocked(b.NetMapWithPeers()); err != nil {
|
||||
b.logf("write netmap to cache: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1023,7 +1025,7 @@ func (b *LocalBackend) pauseOrResumeControlClientLocked() {
|
||||
return
|
||||
}
|
||||
networkUp := b.interfaceState.AnyInterfaceUp()
|
||||
pauseForNetwork := (b.state == ipn.Stopped && b.NetMap() != nil) || (!networkUp && !testenv.InTest() && !envknob.AssumeNetworkUp())
|
||||
pauseForNetwork := (b.state == ipn.Stopped && b.NetMapNoPeers() != nil) || (!networkUp && !testenv.InTest() && !envknob.AssumeNetworkUp())
|
||||
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
pauseForSyncPref := prefs.Valid() && prefs.Sync().EqualBool(false)
|
||||
@@ -4057,7 +4059,8 @@ func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer tai
|
||||
var zero tailcfg.NodeView
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
nm := b.NetMap()
|
||||
// PeerByTailscaleIP needs an up-to-date Peers slice.
|
||||
nm := b.NetMapWithPeers()
|
||||
if nm == nil {
|
||||
return zero, "", errors.New("no netmap")
|
||||
}
|
||||
@@ -4967,7 +4970,7 @@ func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Con
|
||||
}
|
||||
|
||||
func (b *LocalBackend) isLocalIP(ip netip.Addr) bool {
|
||||
nm := b.NetMap()
|
||||
nm := b.NetMapNoPeers()
|
||||
return nm != nil && views.SliceContains(nm.GetAddresses(), netip.PrefixFrom(ip, ip.BitLen()))
|
||||
}
|
||||
|
||||
@@ -5119,10 +5122,46 @@ func extractPeerAPIPorts(services []tailcfg.Service) portPair {
|
||||
|
||||
// NetMap returns the latest cached network map received from
|
||||
// controlclient, or nil if no network map was received yet.
|
||||
//
|
||||
// Deprecated: callers should declare their needs explicitly by calling
|
||||
// either [LocalBackend.NetMapNoPeers] (cheap; for code that reads
|
||||
// non-Peers fields like SelfNode, DNS, PacketFilter, capabilities) or
|
||||
// [LocalBackend.NetMapWithPeers] (currently the same; will be made to
|
||||
// return an up-to-date Peers slice in a follow-up change, at the cost of
|
||||
// O(N) work per call). NetMap will eventually be removed.
|
||||
func (b *LocalBackend) NetMap() *netmap.NetworkMap {
|
||||
return b.currentNode().NetMap()
|
||||
}
|
||||
|
||||
// NetMapNoPeers returns the latest cached network map received from
|
||||
// controlclient WITHOUT a freshly-built Peers slice.
|
||||
//
|
||||
// On a tailnet with frequent peer churn the cached netmap's Peers slice
|
||||
// can be stale relative to the live per-node-backend peers map; non-Peers
|
||||
// fields (SelfNode, DNS, PacketFilter, capabilities, ...) are always
|
||||
// current. Use this for any caller that does not need to iterate Peers,
|
||||
// since it's O(1) regardless of tailnet size.
|
||||
//
|
||||
// Returns nil if no network map has been received yet.
|
||||
func (b *LocalBackend) NetMapNoPeers() *netmap.NetworkMap {
|
||||
return b.currentNode().NetMap()
|
||||
}
|
||||
|
||||
// NetMapWithPeers returns the latest network map with the Peers slice
|
||||
// populated.
|
||||
//
|
||||
// Currently this is the same as [LocalBackend.NetMapNoPeers]: the cached
|
||||
// netmap's Peers slice may be stale relative to the live per-node-backend
|
||||
// peers map. A follow-up change will switch this method to return a
|
||||
// freshly-built netmap with up-to-date Peers, at O(N) cost per call.
|
||||
// Callers that genuinely need the up-to-date peer set should use this
|
||||
// method (and document why) so the upcoming change reaches them.
|
||||
//
|
||||
// Returns nil if no network map has been received yet.
|
||||
func (b *LocalBackend) NetMapWithPeers() *netmap.NetworkMap {
|
||||
return b.currentNode().NetMap()
|
||||
}
|
||||
|
||||
// lookupPeerByIP returns the node public key for the peer that owns the
|
||||
// given IP address. It is the fast path for [Engine.SetPeerByIPPacketFunc],
|
||||
// handling exact-IP matches against node addresses; subnet routes and exit
|
||||
@@ -6978,7 +7017,7 @@ func (b *LocalBackend) AppConnector() *appc.AppConnector {
|
||||
func (b *LocalBackend) allowExitNodeDNSProxyToServeName(name string) bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
nm := b.NetMap()
|
||||
nm := b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
nm := pln.lb.NetMap()
|
||||
nm := pln.lb.NetMapNoPeers()
|
||||
if nm == nil || !nm.SelfNode.Valid() {
|
||||
logf("peerapi: no netmap")
|
||||
c.Close()
|
||||
|
||||
@@ -276,7 +276,7 @@ func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint1
|
||||
}
|
||||
}
|
||||
|
||||
nm := b.NetMap()
|
||||
nm := b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
b.logf("netMap is nil")
|
||||
return
|
||||
@@ -333,7 +333,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
||||
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
|
||||
}
|
||||
|
||||
nm := b.NetMap()
|
||||
nm := b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return errors.New("netMap is nil")
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (b *LocalBackend) waitWebClientAuthURL(ctx context.Context, id string, src
|
||||
// one to be completed, based on the presence or absence of the
|
||||
// provided id value.
|
||||
func (b *LocalBackend) doWebClientNoiseRequest(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error) {
|
||||
nm := b.NetMap()
|
||||
nm := b.NetMapNoPeers()
|
||||
if nm == nil || !nm.SelfNode.Valid() {
|
||||
return nil, errors.New("[unexpected] no self node")
|
||||
}
|
||||
|
||||
+19
-2
@@ -9,6 +9,7 @@ import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -249,6 +250,22 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
case "clear-netmap-cache":
|
||||
h.b.ClearNetmapCache(r.Context())
|
||||
case "current-netmap":
|
||||
// Return the current netmap (with peers populated) as JSON. This
|
||||
// is a debug-only path: the netmap.NetworkMap shape is an
|
||||
// internal type and may change without notice. Production
|
||||
// callers should fetch the narrower bits they need via their
|
||||
// own LocalAPI methods instead.
|
||||
nm := h.b.NetMapWithPeers()
|
||||
if nm == nil {
|
||||
err = errors.New("no netmap")
|
||||
break
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(nm)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
case "":
|
||||
err = fmt.Errorf("missing parameter 'action'")
|
||||
default:
|
||||
@@ -284,7 +301,7 @@ func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Req
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
nm := h.b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusNotFound)
|
||||
return
|
||||
@@ -301,7 +318,7 @@ func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.R
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
nm := h.b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusNotFound)
|
||||
return
|
||||
|
||||
@@ -347,7 +347,7 @@ func (h *Handler) serveIDToken(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "id-token access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
nm := h.b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusServiceUnavailable)
|
||||
return
|
||||
@@ -417,7 +417,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Information about the current node from the netmap
|
||||
if nm := h.b.NetMap(); nm != nil {
|
||||
if nm := h.b.NetMapNoPeers(); nm != nil {
|
||||
if self := nm.SelfNode; self.Valid() {
|
||||
h.logf("user bugreport node info: nodeid=%q stableid=%q expiry=%q", self.ID(), self.StableID(), self.KeyExpiry().Format(time.RFC3339))
|
||||
}
|
||||
@@ -1476,7 +1476,7 @@ func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "missing feature", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
nm := h.b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusServiceUnavailable)
|
||||
return
|
||||
@@ -1731,7 +1731,7 @@ func (h *Handler) serveServices(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
nm := h.b.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusServiceUnavailable)
|
||||
return
|
||||
|
||||
@@ -202,7 +202,7 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err
|
||||
incubatorArgs = append(incubatorArgs, "--is-selinux-enforcing")
|
||||
}
|
||||
|
||||
nm := ss.conn.srv.lb.NetMap()
|
||||
nm := ss.conn.srv.lb.NetMapNoPeers()
|
||||
forceV1Behavior := nm.HasCap(tailcfg.NodeAttrSSHBehaviorV1) && !nm.HasCap(tailcfg.NodeAttrSSHBehaviorV2)
|
||||
if forceV1Behavior {
|
||||
incubatorArgs = append(incubatorArgs, "--force-v1-behavior")
|
||||
|
||||
@@ -92,7 +92,7 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err
|
||||
"--tty-name=", // updated in-place by startWithPTY
|
||||
}
|
||||
|
||||
nm := ss.conn.srv.lb.NetMap()
|
||||
nm := ss.conn.srv.lb.NetMapNoPeers()
|
||||
forceV1Behavior := nm.HasCap(tailcfg.NodeAttrSSHBehaviorV1) && !nm.HasCap(tailcfg.NodeAttrSSHBehaviorV2)
|
||||
if forceV1Behavior {
|
||||
incubatorArgs = append(incubatorArgs, "--force-v1-behavior")
|
||||
|
||||
@@ -76,6 +76,7 @@ const (
|
||||
type ipnLocalBackend interface {
|
||||
ShouldRunSSH() bool
|
||||
NetMap() *netmap.NetworkMap
|
||||
NetMapNoPeers() *netmap.NetworkMap
|
||||
WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||
DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||
Dialer() *tsdial.Dialer
|
||||
@@ -598,7 +599,7 @@ func (c *conn) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
|
||||
if !lb.ShouldRunSSH() {
|
||||
return nil, false
|
||||
}
|
||||
nm := lb.NetMap()
|
||||
nm := lb.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return nil, false
|
||||
}
|
||||
@@ -717,7 +718,7 @@ func (c *conn) handleSessionPostSSHAuth(s gliderssh.Session) {
|
||||
}
|
||||
|
||||
func (c *conn) expandDelegateURLLocked(actionURL string) string {
|
||||
nm := c.srv.lb.NetMap()
|
||||
nm := c.srv.lb.NetMapNoPeers()
|
||||
ci := c.info
|
||||
lu := c.localUser
|
||||
var dstNodeID string
|
||||
|
||||
@@ -910,6 +910,8 @@ func (tb *testBackend) NetMap() *netmap.NetworkMap {
|
||||
}
|
||||
}
|
||||
|
||||
func (tb *testBackend) NetMapNoPeers() *netmap.NetworkMap { return tb.NetMap() }
|
||||
|
||||
func (tb *testBackend) WhoIs(_ string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
return (&tailcfg.Node{}).View(), tailcfg.UserProfile{
|
||||
LoginName: tb.localUser + "@example.com",
|
||||
|
||||
@@ -422,6 +422,8 @@ func (ts *localState) NetMap() *netmap.NetworkMap {
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *localState) NetMapNoPeers() *netmap.NetworkMap { return ts.NetMap() }
|
||||
|
||||
func (ts *localState) WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
if proto != "tcp" {
|
||||
return tailcfg.NodeView{}, tailcfg.UserProfile{}, false
|
||||
|
||||
+2
-2
@@ -668,7 +668,7 @@ func (s *Server) doInit() {
|
||||
// Server.
|
||||
// If the server is not running, it returns nil.
|
||||
func (s *Server) CertDomains() []string {
|
||||
nm := s.lb.NetMap()
|
||||
nm := s.lb.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -679,7 +679,7 @@ func (s *Server) CertDomains() []string {
|
||||
// has not yet joined a tailnet or is otherwise unaware of its own IP addresses,
|
||||
// the returned ip4, ip6 will be !netip.IsValid().
|
||||
func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
|
||||
nm := s.lb.NetMap()
|
||||
nm := s.lb.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user