cmd/tailscale: surface authentication errors in status.Health (#4748)

Fixes #3713

Signed-off-by: Jordan Whited <jordan@tailscale.com>
pull/4784/head
Jordan Whited 2 years ago committed by GitHub
parent c980bf01be
commit 43f9c25fd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -128,12 +128,8 @@ func runStatus(ctx context.Context, args []string) error {
return err return err
} }
description, ok := isRunningOrStarting(st) // print health check information prior to checking LocalBackend state as
if !ok { // it may provide an explanation to the user if we choose to exit early
outln(description)
os.Exit(1)
}
if len(st.Health) > 0 { if len(st.Health) > 0 {
printf("# Health check:\n") printf("# Health check:\n")
for _, m := range st.Health { for _, m := range st.Health {
@ -142,6 +138,12 @@ func runStatus(ctx context.Context, args []string) error {
outln() outln()
} }
description, ok := isRunningOrStarting(st)
if !ok {
outln(description)
os.Exit(1)
}
var buf bytes.Buffer var buf bytes.Buffer
f := func(format string, a ...any) { fmt.Fprintf(&buf, format, a...) } f := func(format string, a ...any) { fmt.Fprintf(&buf, format, a...) }
printPS := func(ps *ipnstate.PeerStatus) { printPS := func(ps *ipnstate.PeerStatus) {

@ -19,6 +19,7 @@ import (
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
shellquote "github.com/kballard/go-shellquote" shellquote "github.com/kballard/go-shellquote"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
@ -114,7 +115,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
case "windows": case "windows":
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)") upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
} }
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
registerAcceptRiskFlag(upf) registerAcceptRiskFlag(upf)
return upf return upf
} }
@ -148,6 +149,7 @@ type upArgsT struct {
hostname string hostname string
opUser string opUser string
json bool json bool
timeout time.Duration
} }
func (a upArgsT) getAuthKey() (string, error) { func (a upArgsT) getAuthKey() (string, error) {
@ -646,6 +648,12 @@ func runUp(ctx context.Context, args []string) error {
// need to prioritize reads from 'running' if it's // need to prioritize reads from 'running' if it's
// readable; its send does happen before the pump mechanism // readable; its send does happen before the pump mechanism
// shuts down. (Issue 2333) // shuts down. (Issue 2333)
var timeoutCh <-chan time.Time
if upArgs.timeout > 0 {
timeoutTimer := time.NewTimer(upArgs.timeout)
defer timeoutTimer.Stop()
timeoutCh = timeoutTimer.C
}
select { select {
case <-running: case <-running:
return nil return nil
@ -663,6 +671,8 @@ func runUp(ctx context.Context, args []string) error {
default: default:
} }
return err return err
case <-timeoutCh:
return errors.New(`timeout waiting for Tailscale service to enter a Running state; check health with "tailscale status"`)
} }
} }
@ -719,7 +729,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
// correspond to an ipn.Pref. // correspond to an ipn.Pref.
func preflessFlag(flagName string) bool { func preflessFlag(flagName string) bool {
switch flagName { switch flagName {
case "auth-key", "force-reauth", "reset", "qr", "json", "accept-risk": case "auth-key", "force-reauth", "reset", "qr", "json", "timeout", "accept-risk":
return true return true
} }
return false return false

@ -289,6 +289,7 @@ func (c *Auto) authRoutine() {
} }
if goal == nil { if goal == nil {
health.SetAuthRoutineInError(nil)
// Wait for user to Login or Logout. // Wait for user to Login or Logout.
<-ctx.Done() <-ctx.Done()
c.logf("[v1] authRoutine: context done.") c.logf("[v1] authRoutine: context done.")
@ -296,6 +297,7 @@ func (c *Auto) authRoutine() {
} }
if !goal.wantLoggedIn { if !goal.wantLoggedIn {
health.SetAuthRoutineInError(nil)
err := c.direct.TryLogout(ctx) err := c.direct.TryLogout(ctx)
goal.sendLogoutError(err) goal.sendLogoutError(err)
if err != nil { if err != nil {
@ -334,6 +336,7 @@ func (c *Auto) authRoutine() {
f = "TryLogin" f = "TryLogin"
} }
if err != nil { if err != nil {
health.SetAuthRoutineInError(err)
report(err, f) report(err, f)
bo.BackOff(ctx, err) bo.BackOff(ctx, err)
continue continue
@ -358,6 +361,7 @@ func (c *Auto) authRoutine() {
} }
// success // success
health.SetAuthRoutineInError(nil)
c.mu.Lock() c.mu.Lock()
c.loggedIn = true c.loggedIn = true
c.loginGoal = nil c.loginGoal = nil

@ -45,6 +45,7 @@ var (
anyInterfaceUp = true // until told otherwise anyInterfaceUp = true // until told otherwise
udp4Unbound bool udp4Unbound bool
controlHealth []string controlHealth []string
lastLoginErr error
) )
// Subsystem is the name of a subsystem whose health can be monitored. // Subsystem is the name of a subsystem whose health can be monitored.
@ -287,6 +288,15 @@ func SetUDP4Unbound(unbound bool) {
selfCheckLocked() selfCheckLocked()
} }
// SetAuthRoutineInError records the latest error encountered as a result of a
// login attempt. Providing a nil error indicates successful login, or that
// being logged in w/coordination is not currently desired.
func SetAuthRoutineInError(err error) {
mu.Lock()
defer mu.Unlock()
lastLoginErr = err
}
func timerSelfCheck() { func timerSelfCheck() {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@ -321,9 +331,12 @@ func overallErrorLocked() error {
if !anyInterfaceUp { if !anyInterfaceUp {
return errors.New("network down") return errors.New("network down")
} }
if ipnState != "Running" || !ipnWantRunning { if !ipnWantRunning {
return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning) return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning)
} }
if lastLoginErr != nil {
return fmt.Errorf("not logged in, last login error=%v", lastLoginErr)
}
now := time.Now() now := time.Now()
if !inMapPoll && (lastMapPollEndedAt.IsZero() || now.Sub(lastMapPollEndedAt) > 10*time.Second) { if !inMapPoll && (lastMapPollEndedAt.IsZero() || now.Sub(lastMapPollEndedAt) > 10*time.Second) {
return errors.New("not in map poll") return errors.New("not in map poll")

@ -2680,12 +2680,14 @@ func (b *LocalBackend) enterState(newState ipn.State) {
b.maybePauseControlClientLocked() b.maybePauseControlClientLocked()
b.mu.Unlock() b.mu.Unlock()
// prefs may change irrespective of state; WantRunning should be explicitly
// set before potential early return even if the state is unchanged.
health.SetIPNState(newState.String(), prefs.WantRunning)
if oldState == newState { if oldState == newState {
return return
} }
b.logf("Switching ipn state %v -> %v (WantRunning=%v, nm=%v)", b.logf("Switching ipn state %v -> %v (WantRunning=%v, nm=%v)",
oldState, newState, prefs.WantRunning, netMap != nil) oldState, newState, prefs.WantRunning, netMap != nil)
health.SetIPNState(newState.String(), prefs.WantRunning)
b.send(ipn.Notify{State: &newState}) b.send(ipn.Notify{State: &newState})
switch newState { switch newState {

Loading…
Cancel
Save