From 2ba426802fea017ac33ee402474e6245608c56a6 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Jun 2026 02:30:45 +0000 Subject: [PATCH] ipn/ipnlocal: fix 'tailscale status --peers=false' missing user profile Fixes #19894 Change-Id: I310504987170e0742480c8a02706eb0dbf4ec3dc Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local.go | 9 +++++++++ ipn/ipnlocal/local_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 12c4cdd4d..b8ca9815c 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1514,6 +1514,15 @@ func (b *LocalBackend) updateStatusLocked(sb *ipnstate.StatusBuilder) { // TODO: hostinfo, and its networkinfo // TODO: EngineStatus copy (and deprecate it?) + // Always add the self user's profile, even when peers are omitted, so that + // callers can resolve the self node's owner to a login name. + // See https://github.com/tailscale/tailscale/issues/19894. + if nm != nil { + if up, ok := nm.UserProfiles[nm.User()]; ok { + sb.AddUser(nm.User(), up) + } + } + if sb.WantPeers { b.populatePeerStatusLocked(sb) } diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 390996bd7..237ed6d4e 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -2025,6 +2025,44 @@ func TestStatusPeerCapabilities(t *testing.T) { } } +// TestStatusWithoutPeersSelfUserProfile verifies that the self user's +// UserProfile is reported in Status.User even when peers are omitted, so that +// callers like `tailscale status --peers=false` can resolve the self node's +// owner to a login name rather than a bare user ID. +// Regression test for https://github.com/tailscale/tailscale/issues/19894. +func TestStatusWithoutPeersSelfUserProfile(t *testing.T) { + b := newTestLocalBackend(t) + const selfUID = tailcfg.UserID(42) + const loginName = "alice@example.com" + b.setNetMapLocked(&netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + MachineAuthorized: true, + Addresses: ipps("100.101.101.101"), + User: selfUID, + }).View(), + UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{ + selfUID: (&tailcfg.UserProfile{ + ID: selfUID, + LoginName: loginName, + }).View(), + }, + }) + st := b.StatusWithoutPeers() + if st.Self == nil { + t.Fatal("Status.Self is nil") + } + if got, want := st.Self.UserID, selfUID; got != want { + t.Errorf("Status.Self.UserID = %v; want %v", got, want) + } + up, ok := st.User[selfUID] + if !ok { + t.Fatalf("Status.User missing entry for self UserID %v; got %v", selfUID, st.User) + } + if got, want := up.LoginName, loginName; got != want { + t.Errorf("Status.User[%v].LoginName = %q; want %q", selfUID, got, want) + } +} + // legacyBackend was the interface between Tailscale frontends // (e.g. cmd/tailscale, iOS/MacOS/Windows GUIs) and the tailscale // backend (e.g. cmd/tailscaled) running on the same machine.