From 5efb0a8bcad2ad7ba2f587fc5dbd74fc16eba17a Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 10 Jan 2021 12:03:01 -0800 Subject: [PATCH] cmd/tailscale: change formatting of "tailscale status" * show DNS name over hostname, removing domain's common MagicDNS suffix. only show hostname if there's no DNS name. but still show shared devices' MagicDNS FQDN. * remove nerdy low-level details by default: endpoints, DERP relay, public key. They're available in JSON mode still for those who need them. * only show endpoint or DERP relay when it's active with the goal of making debugging easier. (so it's easier for users to understand what's happening) The asterisks are gone. * remove Tx/Rx numbers by default for idle peers; only show them when there's traffic. * include peers' owner login names * add CLI option to not show peers (matching --self=true, --peers= also defaults to true) * sort by DNS/host name, not public key * reorder columns --- cmd/tailscale/cli/status.go | 100 +++++++++++++++++++++++--------- cmd/tailscale/depaware.txt | 1 + cmd/tailscaled/depaware.txt | 1 + control/controlclient/netmap.go | 26 ++++++++- ipn/ipnstate/ipnstate.go | 13 ++++- ipn/local.go | 25 ++------ util/dnsname/dnsname.go | 19 ++++++ util/dnsname/dnsname_test.go | 28 +++++++++ wgengine/magicsock/magicsock.go | 5 +- wgengine/tsdns/tsdns.go | 12 +--- wgengine/tsdns/tsdns_test.go | 21 ------- 11 files changed, 167 insertions(+), 84 deletions(-) create mode 100644 util/dnsname/dnsname.go create mode 100644 util/dnsname/dnsname_test.go diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go index 782c4d8ef..52e30478e 100644 --- a/cmd/tailscale/cli/status.go +++ b/cmd/tailscale/cli/status.go @@ -14,6 +14,8 @@ import ( "net" "net/http" "os" + "sort" + "strings" "time" "github.com/peterbourgon/ff/v2/ffcli" @@ -21,6 +23,7 @@ import ( "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/net/interfaces" + "tailscale.com/util/dnsname" ) var statusCmd = &ffcli.Command{ @@ -34,6 +37,7 @@ var statusCmd = &ffcli.Command{ fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status") fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)") fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine") + fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers") fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic") fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode") return fs @@ -47,6 +51,7 @@ var statusArgs struct { browser bool // in web mode, whether to open browser active bool // in CLI mode, filter output to only peers with active sessions self bool // in CLI mode, show status of local machine + peers bool // in CLI mode, show status of peer machines } func runStatus(ctx context.Context, args []string) error { @@ -136,47 +141,54 @@ func runStatus(ctx context.Context, args []string) error { f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) } printPS := func(ps *ipnstate.PeerStatus) { active := peerActive(ps) - f("%s %-7s %-15s %-18s tx=%8d rx=%8d ", - ps.PublicKey.ShortString(), - ps.OS, + f("%-15s %-20s %-12s %-7s ", ps.TailAddr, - ps.SimpleHostName(), - ps.TxBytes, - ps.RxBytes, + dnsOrQuoteHostname(st, ps), + ownerLogin(st, ps), + ps.OS, ) relay := ps.Relay - if active && relay != "" && ps.CurAddr == "" { - relay = "*" + relay + "*" - } else { - relay = " " + relay - } - f("%-6s", relay) - for i, addr := range ps.Addrs { - if i != 0 { - f(", ") - } - if addr == ps.CurAddr { - f("*%s*", addr) + anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0 + if !active { + if anyTraffic { + f("idle") } else { - f("%s", addr) + f("-") + } + } else { + f("active; ") + if relay != "" && ps.CurAddr == "" { + f("relay %q", relay) + } else if ps.CurAddr != "" { + f("direct %s", ps.CurAddr) } } + if anyTraffic { + f(", tx %d rx %d", ps.TxBytes, ps.RxBytes) + } f("\n") } if statusArgs.self && st.Self != nil { printPS(st.Self) } - for _, peer := range st.Peers() { - ps := st.Peer[peer] - if ps.ShareeNode { - continue + if statusArgs.peers { + var peers []*ipnstate.PeerStatus + for _, peer := range st.Peers() { + ps := st.Peer[peer] + if ps.ShareeNode { + continue + } + peers = append(peers, ps) } - active := peerActive(ps) - if statusArgs.active && !active { - continue + sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) }) + for _, ps := range peers { + active := peerActive(ps) + if statusArgs.active && !active { + continue + } + printPS(ps) } - printPS(ps) } os.Stdout.Write(buf.Bytes()) return nil @@ -188,3 +200,37 @@ func runStatus(ctx context.Context, args []string) error { func peerActive(ps *ipnstate.PeerStatus) bool { return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute } + +func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string { + if i := strings.Index(ps.DNSName, "."); i != -1 && dnsname.HasSuffix(ps.DNSName, st.MagicDNSSuffix) { + return ps.DNSName[:i] + } + if ps.DNSName != "" { + return ps.DNSName + } + return fmt.Sprintf("- (%q)", ps.SimpleHostName()) +} + +func sortKey(ps *ipnstate.PeerStatus) string { + if ps.DNSName != "" { + return ps.DNSName + } + if ps.HostName != "" { + return ps.HostName + } + return ps.TailAddr +} + +func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string { + if ps.UserID.IsZero() { + return "-" + } + u, ok := st.User[ps.UserID] + if !ok { + return fmt.Sprint(ps.UserID) + } + if i := strings.Index(u.LoginName, "@"); i != -1 { + return u.LoginName[:i+1] + } + return u.LoginName +} diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 620838b17..4edd14a02 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -74,6 +74,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/types/strbuilder from tailscale.com/net/packet tailscale.com/types/structs from tailscale.com/control/controlclient+ tailscale.com/types/wgkey from tailscale.com/control/controlclient+ + tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+ LW tailscale.com/util/endian from tailscale.com/net/netns+ tailscale.com/util/lineread from tailscale.com/control/controlclient+ tailscale.com/util/systemd from tailscale.com/control/controlclient+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 1e2c8612b..b0f26cf51 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -82,6 +82,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/strbuilder from tailscale.com/net/packet tailscale.com/types/structs from tailscale.com/control/controlclient+ tailscale.com/types/wgkey from tailscale.com/control/controlclient+ + tailscale.com/util/dnsname from tailscale.com/control/controlclient+ LW tailscale.com/util/endian from tailscale.com/net/netns+ tailscale.com/util/lineread from tailscale.com/control/controlclient+ tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver diff --git a/control/controlclient/netmap.go b/control/controlclient/netmap.go index c9d053449..ab4caa83c 100644 --- a/control/controlclient/netmap.go +++ b/control/controlclient/netmap.go @@ -18,6 +18,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/wgkey" + "tailscale.com/util/dnsname" "tailscale.com/wgengine/filter" ) @@ -56,7 +57,30 @@ type NetworkMap struct { // TODO(crawshaw): Capabilities []tailcfg.Capability } -func (nm NetworkMap) String() string { +// MagicDNSSuffix returns the domain's MagicDNS suffix, or empty if none. +// If non-empty, it will neither start nor end with a period. +func (nm *NetworkMap) MagicDNSSuffix() string { + searchPathUsedAsDNSSuffix := func(suffix string) bool { + if dnsname.HasSuffix(nm.Name, suffix) { + return true + } + for _, p := range nm.Peers { + if dnsname.HasSuffix(p.Name, suffix) { + return true + } + } + return false + } + + for _, d := range nm.DNS.Domains { + if searchPathUsedAsDNSSuffix(d) { + return strings.Trim(d, ".") + } + } + return "" +} + +func (nm *NetworkMap) String() string { return nm.Concise() } diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 06559ce44..65454a54f 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -25,9 +25,10 @@ import ( // Status represents the entire state of the IPN network. type Status struct { - BackendState string - TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node - Self *PeerStatus + BackendState string + TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node + Self *PeerStatus + MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots) Peer map[key.Public]*PeerStatus User map[tailcfg.UserID]tailcfg.UserProfile @@ -103,6 +104,12 @@ func (sb *StatusBuilder) SetBackendState(v string) { sb.st.BackendState = v } +func (sb *StatusBuilder) SetMagicDNSSuffix(v string) { + sb.mu.Lock() + defer sb.mu.Unlock() + sb.st.MagicDNSSuffix = v +} + func (sb *StatusBuilder) Status() *Status { sb.mu.Lock() defer sb.mu.Unlock() diff --git a/ipn/local.go b/ipn/local.go index a47938859..b3abdf9d1 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -201,6 +201,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) { // TODO: hostinfo, and its networkinfo // TODO: EngineStatus copy (and deprecate it?) if b.netMap != nil { + sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix()) for id, up := range b.netMap.UserProfiles { sb.AddUser(id, up) } @@ -1232,28 +1233,10 @@ func (b *LocalBackend) authReconfig() { // magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS. // Each entry has a trailing period. func magicDNSRootDomains(nm *controlclient.NetworkMap) []string { - searchPathUsedAsDNSSuffix := func(suffix string) bool { - if tsdns.NameHasSuffix(nm.Name, suffix) { - return true - } - for _, p := range nm.Peers { - if tsdns.NameHasSuffix(p.Name, suffix) { - return true - } - } - return false - } - - var ret []string - for _, d := range nm.DNS.Domains { - if searchPathUsedAsDNSSuffix(d) { - if !strings.HasSuffix(d, ".") { - d += "." - } - ret = append(ret, d) - } + if v := nm.MagicDNSSuffix(); v != "" { + return []string{strings.Trim(v, ".") + "."} } - return ret + return nil } // routerConfig produces a router.Config from a wireguard config and IPN prefs. diff --git a/util/dnsname/dnsname.go b/util/dnsname/dnsname.go new file mode 100644 index 000000000..1488272a4 --- /dev/null +++ b/util/dnsname/dnsname.go @@ -0,0 +1,19 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package dnsname contains string functions for working with DNS names. +package dnsname + +import "strings" + +// HasSuffix reports whether the provided DNS name ends with the +// component(s) in suffix, ignoring any trailing dots. +// +// If suffix is the empty string, HasSuffix always reports false. +func HasSuffix(name, suffix string) bool { + name = strings.TrimSuffix(name, ".") + suffix = strings.TrimSuffix(suffix, ".") + nameBase := strings.TrimSuffix(name, suffix) + return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".") +} diff --git a/util/dnsname/dnsname_test.go b/util/dnsname/dnsname_test.go new file mode 100644 index 000000000..da4e51384 --- /dev/null +++ b/util/dnsname/dnsname_test.go @@ -0,0 +1,28 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dnsname + +import "testing" + +func TestHasSuffix(t *testing.T) { + tests := []struct { + name, suffix string + want bool + }{ + {"foo.com", "com", true}, + {"foo.com.", "com", true}, + {"foo.com.", "com.", true}, + + {"", "", false}, + {"foo.com.", "", false}, + {"foo.com.", "o.com", false}, + } + for _, tt := range tests { + got := HasSuffix(tt.name, tt.suffix) + if got != tt.want { + t.Errorf("HasSuffix(%q, %q) = %v; want %v", tt.name, tt.suffix, got, tt.want) + } + } +} diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 7a4c06ac1..d7e995747 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -2710,11 +2710,14 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { ss := &ipnstate.PeerStatus{ PublicKey: c.privateKey.Public(), Addrs: c.lastEndpoints, + OS: version.OS(), } if c.netMap != nil { ss.HostName = c.netMap.Hostinfo.Hostname - ss.OS = version.OS() ss.DNSName = c.netMap.Name + ss.UserID = c.netMap.User + } else { + ss.HostName, _ = os.Hostname() } if c.derpMap != nil { derpRegion, ok := c.derpMap.Regions[c.myDerp] diff --git a/wgengine/tsdns/tsdns.go b/wgengine/tsdns/tsdns.go index 652f137e1..c14503003 100644 --- a/wgengine/tsdns/tsdns.go +++ b/wgengine/tsdns/tsdns.go @@ -17,6 +17,7 @@ import ( dns "golang.org/x/net/dns/dnsmessage" "inet.af/netaddr" "tailscale.com/types/logger" + "tailscale.com/util/dnsname" ) // maxResponseBytes is the maximum size of a response from a Resolver. @@ -195,7 +196,7 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e anyHasSuffix := false for _, suffix := range dnsMap.rootDomains { - if NameHasSuffix(domain, suffix) { + if dnsname.HasSuffix(domain, suffix) { anyHasSuffix = true break } @@ -616,12 +617,3 @@ func (r *Resolver) respond(query []byte) ([]byte, error) { return marshalResponse(resp) } - -// NameHasSuffix reports whether the provided DNS name ends with the -// component(s) in suffix, ignoring any trailing dots. -func NameHasSuffix(name, suffix string) bool { - name = strings.TrimSuffix(name, ".") - suffix = strings.TrimSuffix(suffix, ".") - nameBase := strings.TrimSuffix(name, suffix) - return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".") -} diff --git a/wgengine/tsdns/tsdns_test.go b/wgengine/tsdns/tsdns_test.go index 8e6573b97..7e28b9efb 100644 --- a/wgengine/tsdns/tsdns_test.go +++ b/wgengine/tsdns/tsdns_test.go @@ -797,24 +797,3 @@ func TestMarshalResponseFormatError(t *testing.T) { } t.Logf("response: %q", v) } - -func TestNameHasSuffix(t *testing.T) { - tests := []struct { - name, suffix string - want bool - }{ - {"foo.com", "com", true}, - {"foo.com.", "com", true}, - {"foo.com.", "com.", true}, - - {"", "", false}, - {"foo.com.", "", false}, - {"foo.com.", "o.com", false}, - } - for _, tt := range tests { - got := NameHasSuffix(tt.name, tt.suffix) - if got != tt.want { - t.Errorf("NameHasSuffix(%q, %q) = %v; want %v", tt.name, tt.suffix, got, tt.want) - } - } -}