mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
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
This commit is contained in:
parent
c09d5a9e28
commit
5efb0a8bca
@ -14,6 +14,8 @@
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
@ -21,6 +23,7 @@
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
@ -34,6 +37,7 @@
|
||||
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 @@
|
||||
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,30 +141,30 @@ 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")
|
||||
}
|
||||
@ -167,16 +172,23 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
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
|
||||
}
|
||||
|
@ -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+
|
||||
|
@ -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
|
||||
|
@ -18,6 +18,7 @@
|
||||
"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()
|
||||
}
|
||||
|
||||
|
@ -25,9 +25,10 @@
|
||||
|
||||
// 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()
|
||||
|
25
ipn/local.go
25
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
|
||||
if v := nm.MagicDNSSuffix(); v != "" {
|
||||
return []string{strings.Trim(v, ".") + "."}
|
||||
}
|
||||
|
||||
var ret []string
|
||||
for _, d := range nm.DNS.Domains {
|
||||
if searchPathUsedAsDNSSuffix(d) {
|
||||
if !strings.HasSuffix(d, ".") {
|
||||
d += "."
|
||||
}
|
||||
ret = append(ret, d)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
return nil
|
||||
}
|
||||
|
||||
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
|
||||
|
19
util/dnsname/dnsname.go
Normal file
19
util/dnsname/dnsname.go
Normal file
@ -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, ".")
|
||||
}
|
28
util/dnsname/dnsname_test.go
Normal file
28
util/dnsname/dnsname_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
@ -17,6 +17,7 @@
|
||||
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, ".")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user