cmd/tailscale: make "up", "status" warn if routes and --accept-routes off

Example output:

    # Health check:
    #     - Some peers are advertising routes but --accept-routes is false

Also, move "tailscale status" health checks to the bottom, where they
won't be lost in large netmaps.

Updates #2053
Updates #6266

Change-Id: I5ae76a0cd69a452ce70063875cd7d974bfeb8f1a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/5681/merge
Brad Fitzpatrick 2 years ago committed by Brad Fitzpatrick
parent 66b4a363bd
commit 08e110ebc5

@ -16,6 +16,7 @@ import (
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tstest" "tailscale.com/tstest"
@ -1143,3 +1144,15 @@ func TestCleanUpArgs(t *testing.T) {
c.Assert(got, qt.DeepEquals, tt.want) c.Assert(got, qt.DeepEquals, tt.want)
} }
} }
func TestUpWorthWarning(t *testing.T) {
if !upWorthyWarning(healthmsg.WarnAcceptRoutesOff) {
t.Errorf("WarnAcceptRoutesOff of %q should be worth warning", healthmsg.WarnAcceptRoutesOff)
}
if !upWorthyWarning(healthmsg.TailscaleSSHOnBut + "some problem") {
t.Errorf("want true for SSH problems")
}
if upWorthyWarning("not in map poll") {
t.Errorf("want false for other misc errors")
}
}

@ -128,18 +128,21 @@ func runStatus(ctx context.Context, args []string) error {
return err return err
} }
// print health check information prior to checking LocalBackend state as printHealth := func() {
// it may provide an explanation to the user if we choose to exit early
if len(st.Health) > 0 {
printf("# Health check:\n") printf("# Health check:\n")
for _, m := range st.Health { for _, m := range st.Health {
printf("# - %s\n", m) printf("# - %s\n", m)
} }
outln()
} }
description, ok := isRunningOrStarting(st) description, ok := isRunningOrStarting(st)
if !ok { if !ok {
// print health check information if we're in a weird state, as it might
// provide context about why we're in that weird state.
if len(st.Health) > 0 && (st.BackendState == ipn.Starting.String() || st.BackendState == ipn.NoState.String()) {
printHealth()
outln()
}
outln(description) outln(description)
os.Exit(1) os.Exit(1)
} }
@ -214,6 +217,10 @@ func runStatus(ctx context.Context, args []string) error {
} }
} }
Stdout.Write(buf.Bytes()) Stdout.Write(buf.Bytes())
if len(st.Health) > 0 {
outln()
printHealth()
}
return nil return nil
} }

@ -25,6 +25,7 @@ import (
shellquote "github.com/kballard/go-shellquote" shellquote "github.com/kballard/go-shellquote"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode" qrcode "github.com/skip2/go-qrcode"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
@ -495,7 +496,7 @@ func runUp(ctx context.Context, args []string) (retErr error) {
defer func() { defer func() {
if retErr == nil { if retErr == nil {
checkSSHUpWarnings(ctx) checkUpWarnings(ctx)
} }
}() }()
@ -678,25 +679,39 @@ func runUp(ctx context.Context, args []string) (retErr error) {
} }
} }
func checkSSHUpWarnings(ctx context.Context) { // upWorthWarning reports whether the health check message s is worth warning
if !upArgs.runSSH { // about during "tailscale up". Many of the health checks are noisy or confusing
return // or very ephemeral and happen especially briefly at startup.
} //
st, err := localClient.Status(ctx) // TODO(bradfitz): change the server to send typed warnings with metadata about
// the health check, rather than just a string.
func upWorthyWarning(s string) bool {
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
strings.Contains(s, healthmsg.WarnAcceptRoutesOff)
}
func checkUpWarnings(ctx context.Context) {
st, err := localClient.StatusWithoutPeers(ctx)
if err != nil { if err != nil {
// Ignore. Don't spam more. // Ignore. Don't spam more.
return return
} }
if len(st.Health) == 0 { var warn []string
for _, w := range st.Health {
if upWorthyWarning(w) {
warn = append(warn, w)
}
}
if len(warn) == 0 {
return return
} }
if len(st.Health) == 1 && strings.Contains(st.Health[0], "SSH") { if len(warn) == 1 {
printf("%s\n", st.Health[0]) printf("%s\n", warn[0])
return return
} }
printf("# Health check:\n") printf("# Health check warnings:\n")
for _, m := range st.Health { for _, m := range warn {
printf(" - %s\n", m) printf("# - %s\n", m)
} }
} }
@ -1040,3 +1055,15 @@ func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netip.Addr) {
} }
return return
} }
func anyPeerAdvertisingRoutes(st *ipnstate.Status) bool {
for _, ps := range st.Peer {
if ps.PrimaryRoutes == nil {
continue
}
if ps.PrimaryRoutes.Len() > 0 {
return true
}
}
return false
}

@ -51,6 +51,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/derp/derphttp from tailscale.com/net/netcheck tailscale.com/derp/derphttp from tailscale.com/net/netcheck
tailscale.com/disco from tailscale.com/derp tailscale.com/disco from tailscale.com/derp
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+ tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
tailscale.com/hostinfo from tailscale.com/net/interfaces+ tailscale.com/hostinfo from tailscale.com/net/interfaces+
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+ tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+ tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+

@ -194,6 +194,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/envknob from tailscale.com/control/controlclient+ tailscale.com/envknob from tailscale.com/control/controlclient+
tailscale.com/health from tailscale.com/control/controlclient+ tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
tailscale.com/hostinfo from tailscale.com/control/controlclient+ tailscale.com/hostinfo from tailscale.com/control/controlclient+
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+ tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+

@ -0,0 +1,14 @@
// Copyright (c) 2022 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 healthmsg contains some constants for health messages.
//
// It's a leaf so both the server and CLI can depend on it without bringing too
// much in to the CLI binary.
package healthmsg
const (
WarnAcceptRoutesOff = "Some peers are advertising routes but --accept-routes is false"
TailscaleSSHOnBut = "Tailscale SSH enabled, but " // + ... something from caller
)

@ -35,6 +35,7 @@ import (
"tailscale.com/doctor/routetable" "tailscale.com/doctor/routetable"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health" "tailscale.com/health"
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo" "tailscale.com/hostinfo"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
@ -585,19 +586,23 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
s.CurrentTailnet.MagicDNSSuffix = b.netMap.MagicDNSSuffix() s.CurrentTailnet.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
s.CurrentTailnet.MagicDNSEnabled = b.netMap.DNS.Proxied s.CurrentTailnet.MagicDNSEnabled = b.netMap.DNS.Proxied
s.CurrentTailnet.Name = b.netMap.Domain s.CurrentTailnet.Name = b.netMap.Domain
if prefs := b.pm.CurrentPrefs(); prefs.Valid() && !prefs.ExitNodeID().IsZero() { if prefs := b.pm.CurrentPrefs(); prefs.Valid() {
if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok { if !prefs.RouteAll() && b.netMap.AnyPeersAdvertiseRoutes() {
var online = false s.Health = append(s.Health, healthmsg.WarnAcceptRoutesOff)
if exitPeer.Online != nil { }
online = *exitPeer.Online if !prefs.ExitNodeID().IsZero() {
} if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok {
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{ var online = false
ID: prefs.ExitNodeID(), if exitPeer.Online != nil {
Online: online, online = *exitPeer.Online
TailscaleIPs: exitPeer.Addresses, }
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
ID: prefs.ExitNodeID(),
Online: online,
TailscaleIPs: exitPeer.Addresses,
}
} }
} }
} }
} }
}) })
@ -2172,12 +2177,12 @@ func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage
isAdmin := hasCapability(nm, tailcfg.CapabilityAdmin) isAdmin := hasCapability(nm, tailcfg.CapabilityAdmin)
if !isAdmin { if !isAdmin {
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access." return healthmsg.TailscaleSSHOnBut + "access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access."
} }
if !isDefault { if !isDefault {
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs to allow access." return healthmsg.TailscaleSSHOnBut + "access controls don't allow anyone to access this device. Update your tailnet's ACLs to allow access."
} }
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs at https://tailscale.com/s/ssh-policy" return healthmsg.TailscaleSSHOnBut + "access controls don't allow anyone to access this device. Update your tailnet's ACLs at https://tailscale.com/s/ssh-policy"
} }
func (b *LocalBackend) isDefaultServerLocked() bool { func (b *LocalBackend) isDefaultServerLocked() bool {

@ -84,6 +84,16 @@ type NetworkMap struct {
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
} }
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
for _, p := range nm.Peers {
if len(p.PrimaryRoutes) > 0 {
return true
}
}
return false
}
// PeerByTailscaleIP returns a peer's Node based on its Tailscale IP. // PeerByTailscaleIP returns a peer's Node based on its Tailscale IP.
// //
// If nm is nil or no peer is found, ok is false. // If nm is nil or no peer is found, ok is false.

Loading…
Cancel
Save