2021-04-30 03:18:25 +00:00
|
|
|
// Copyright (c) 2020 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 controlclient
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/types/empty"
|
|
|
|
"tailscale.com/types/netmap"
|
|
|
|
"tailscale.com/types/persist"
|
|
|
|
"tailscale.com/types/structs"
|
|
|
|
)
|
|
|
|
|
|
|
|
// State is the high-level state of the client. It is used only in
|
|
|
|
// unit tests for proper sequencing, don't depend on it anywhere else.
|
2021-04-30 03:27:00 +00:00
|
|
|
//
|
|
|
|
// TODO(apenwarr): eliminate the state, as it's now obsolete.
|
|
|
|
//
|
|
|
|
// apenwarr: Historical note: controlclient.Auto was originally
|
|
|
|
// intended to be the state machine for the whole tailscale client, but that
|
|
|
|
// turned out to not be the right abstraction layer, and it moved to
|
|
|
|
// ipn.Backend. Since ipn.Backend now has a state machine, it would be
|
|
|
|
// much better if controlclient could be a simple stateless API. But the
|
|
|
|
// current server-side API (two interlocking polling https calls) makes that
|
|
|
|
// very hard to implement. A server side API change could untangle this and
|
|
|
|
// remove all the statefulness.
|
2021-04-30 03:18:25 +00:00
|
|
|
type State int
|
|
|
|
|
|
|
|
const (
|
|
|
|
StateNew = State(iota)
|
|
|
|
StateNotAuthenticated
|
|
|
|
StateAuthenticating
|
|
|
|
StateURLVisitRequired
|
|
|
|
StateAuthenticated
|
|
|
|
StateSynchronized // connected and received map update
|
|
|
|
)
|
|
|
|
|
|
|
|
func (s State) MarshalText() ([]byte, error) {
|
|
|
|
return []byte(s.String()), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s State) String() string {
|
|
|
|
switch s {
|
|
|
|
case StateNew:
|
|
|
|
return "state:new"
|
|
|
|
case StateNotAuthenticated:
|
|
|
|
return "state:not-authenticated"
|
|
|
|
case StateAuthenticating:
|
|
|
|
return "state:authenticating"
|
|
|
|
case StateURLVisitRequired:
|
|
|
|
return "state:url-visit-required"
|
|
|
|
case StateAuthenticated:
|
|
|
|
return "state:authenticated"
|
|
|
|
case StateSynchronized:
|
|
|
|
return "state:synchronized"
|
|
|
|
default:
|
|
|
|
return fmt.Sprintf("state:unknown:%d", int(s))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type Status struct {
|
ipnlocal: don't assume NeedsLogin immediately after StartLogout().
Previously, there was no server round trip required to log out, so when
you asked ipnlocal to Logout(), it could clear the netmap immediately
and switch to NeedsLogin state.
In v1.8, we added a true Logout operation. ipn.Logout() would trigger
an async cc.StartLogout() and *also* immediately switch to NeedsLogin.
Unfortunately, some frontends would see NeedsLogin and immediately
trigger a new StartInteractiveLogin() operation, before the
controlclient auth state machine actually acted on the Logout command,
thus accidentally invalidating the entire logout operation, retaining
the netmap, and violating the user's expectations.
Instead, add a new LogoutFinished signal from controlclient
(paralleling LoginFinished) and, upon starting a logout, don't update
the ipn state machine until it's received.
Updates: #1918 (BUG-2)
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-20 06:46:57 +00:00
|
|
|
_ structs.Incomparable
|
|
|
|
LoginFinished *empty.Message // nonempty when login finishes
|
|
|
|
LogoutFinished *empty.Message // nonempty when logout finishes
|
2021-10-26 17:19:35 +00:00
|
|
|
Err error
|
ipnlocal: don't assume NeedsLogin immediately after StartLogout().
Previously, there was no server round trip required to log out, so when
you asked ipnlocal to Logout(), it could clear the netmap immediately
and switch to NeedsLogin state.
In v1.8, we added a true Logout operation. ipn.Logout() would trigger
an async cc.StartLogout() and *also* immediately switch to NeedsLogin.
Unfortunately, some frontends would see NeedsLogin and immediately
trigger a new StartInteractiveLogin() operation, before the
controlclient auth state machine actually acted on the Logout command,
thus accidentally invalidating the entire logout operation, retaining
the netmap, and violating the user's expectations.
Instead, add a new LogoutFinished signal from controlclient
(paralleling LoginFinished) and, upon starting a logout, don't update
the ipn state machine until it's received.
Updates: #1918 (BUG-2)
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-20 06:46:57 +00:00
|
|
|
URL string // interactive URL to visit to finish logging in
|
|
|
|
NetMap *netmap.NetworkMap // server-pushed configuration
|
2021-04-30 03:27:00 +00:00
|
|
|
|
|
|
|
// The internal state should not be exposed outside this
|
|
|
|
// package, but we have some automated tests elsewhere that need to
|
|
|
|
// use them. Please don't use these fields.
|
|
|
|
// TODO(apenwarr): Unexport or remove these.
|
|
|
|
State State
|
|
|
|
Persist *persist.Persist // locally persisted configuration
|
|
|
|
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
2021-04-30 03:18:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Equal reports whether s and s2 are equal.
|
|
|
|
func (s *Status) Equal(s2 *Status) bool {
|
|
|
|
if s == nil && s2 == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return s != nil && s2 != nil &&
|
|
|
|
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
|
ipnlocal: don't assume NeedsLogin immediately after StartLogout().
Previously, there was no server round trip required to log out, so when
you asked ipnlocal to Logout(), it could clear the netmap immediately
and switch to NeedsLogin state.
In v1.8, we added a true Logout operation. ipn.Logout() would trigger
an async cc.StartLogout() and *also* immediately switch to NeedsLogin.
Unfortunately, some frontends would see NeedsLogin and immediately
trigger a new StartInteractiveLogin() operation, before the
controlclient auth state machine actually acted on the Logout command,
thus accidentally invalidating the entire logout operation, retaining
the netmap, and violating the user's expectations.
Instead, add a new LogoutFinished signal from controlclient
(paralleling LoginFinished) and, upon starting a logout, don't update
the ipn state machine until it's received.
Updates: #1918 (BUG-2)
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-20 06:46:57 +00:00
|
|
|
(s.LogoutFinished == nil) == (s2.LogoutFinished == nil) &&
|
2021-04-30 03:18:25 +00:00
|
|
|
s.Err == s2.Err &&
|
|
|
|
s.URL == s2.URL &&
|
|
|
|
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
|
|
|
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
|
|
|
|
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
|
|
|
|
s.State == s2.State
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s Status) String() string {
|
|
|
|
b, err := json.MarshalIndent(s, "", "\t")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return s.State.String() + " " + string(b)
|
|
|
|
}
|