mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-16 03:31:39 +00:00
ipn/{ipnlocal,localapi}, cmd/tailscale: add logout command
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
11127666b2
commit
3167e55ddf
@ -212,3 +212,8 @@ func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
|||||||
}
|
}
|
||||||
return &p, nil
|
return &p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Logout(ctx context.Context) error {
|
||||||
|
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
@ -64,6 +64,7 @@ change in the future.
|
|||||||
Subcommands: []*ffcli.Command{
|
Subcommands: []*ffcli.Command{
|
||||||
upCmd,
|
upCmd,
|
||||||
downCmd,
|
downCmd,
|
||||||
|
logoutCmd,
|
||||||
netcheckCmd,
|
netcheckCmd,
|
||||||
ipCmd,
|
ipCmd,
|
||||||
statusCmd,
|
statusCmd,
|
||||||
|
34
cmd/tailscale/cli/logout.go
Normal file
34
cmd/tailscale/cli/logout.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v2/ffcli"
|
||||||
|
"tailscale.com/client/tailscale"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logoutCmd = &ffcli.Command{
|
||||||
|
Name: "logout",
|
||||||
|
ShortUsage: "logout [flags]",
|
||||||
|
ShortHelp: "down + expire current node key",
|
||||||
|
|
||||||
|
LongHelp: strings.TrimSpace(`
|
||||||
|
"tailscale logout" brings the network down and invalidates
|
||||||
|
the current node key, forcing a future use of it to cause
|
||||||
|
a reauthentication.
|
||||||
|
`),
|
||||||
|
Exec: runLogout,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogout(ctx context.Context, args []string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
log.Fatalf("too many non-flag arguments: %q", args)
|
||||||
|
}
|
||||||
|
return tailscale.Logout(ctx)
|
||||||
|
}
|
@ -100,11 +100,22 @@ func (s Status) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoginGoal struct {
|
type LoginGoal struct {
|
||||||
_ structs.Incomparable
|
_ structs.Incomparable
|
||||||
wantLoggedIn bool // true if we *want* to be logged in
|
wantLoggedIn bool // true if we *want* to be logged in
|
||||||
token *tailcfg.Oauth2Token // oauth token to use when logging in
|
token *tailcfg.Oauth2Token // oauth token to use when logging in
|
||||||
flags LoginFlags // flags to use when logging in
|
flags LoginFlags // flags to use when logging in
|
||||||
url string // auth url that needs to be visited
|
url string // auth url that needs to be visited
|
||||||
|
loggedOutResult chan<- error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *LoginGoal) sendLogoutError(err error) {
|
||||||
|
if g.loggedOutResult == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case g.loggedOutResult <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client connects to a tailcontrol server for a node.
|
// Client connects to a tailcontrol server for a node.
|
||||||
@ -363,6 +374,7 @@ func (c *Client) authRoutine() {
|
|||||||
|
|
||||||
if !goal.wantLoggedIn {
|
if !goal.wantLoggedIn {
|
||||||
err := c.direct.TryLogout(ctx)
|
err := c.direct.TryLogout(ctx)
|
||||||
|
goal.sendLogoutError(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report(err, "TryLogout")
|
report(err, "TryLogout")
|
||||||
bo.BackOff(ctx, err)
|
bo.BackOff(ctx, err)
|
||||||
@ -402,7 +414,8 @@ func (c *Client) authRoutine() {
|
|||||||
report(err, f)
|
report(err, f)
|
||||||
bo.BackOff(ctx, err)
|
bo.BackOff(ctx, err)
|
||||||
continue
|
continue
|
||||||
} else if url != "" {
|
}
|
||||||
|
if url != "" {
|
||||||
if goal.url != "" {
|
if goal.url != "" {
|
||||||
err = fmt.Errorf("[unexpected] server required a new URL?")
|
err = fmt.Errorf("[unexpected] server required a new URL?")
|
||||||
report(err, "WaitLoginURL")
|
report(err, "WaitLoginURL")
|
||||||
@ -682,18 +695,42 @@ func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
|||||||
c.cancelAuth()
|
c.cancelAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Logout() {
|
func (c *Client) StartLogout() {
|
||||||
c.logf("client.Logout()")
|
c.logf("client.StartLogout()")
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.loginGoal = &LoginGoal{
|
c.loginGoal = &LoginGoal{
|
||||||
wantLoggedIn: false,
|
wantLoggedIn: false,
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
c.cancelAuth()
|
c.cancelAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) Logout(ctx context.Context) error {
|
||||||
|
c.logf("client.Logout()")
|
||||||
|
|
||||||
|
errc := make(chan error, 1)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
c.loginGoal = &LoginGoal{
|
||||||
|
wantLoggedIn: false,
|
||||||
|
loggedOutResult: errc,
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
c.cancelAuth()
|
||||||
|
|
||||||
|
timer := time.NewTimer(10 * time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case err := <-errc:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
return context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateEndpoints sets the client's discovered endpoints and sends
|
// UpdateEndpoints sets the client's discovered endpoints and sends
|
||||||
// them to the control server if they've changed.
|
// them to the control server if they've changed.
|
||||||
//
|
//
|
||||||
|
@ -261,15 +261,14 @@ const (
|
|||||||
func (c *Direct) TryLogout(ctx context.Context) error {
|
func (c *Direct) TryLogout(ctx context.Context) error {
|
||||||
c.logf("direct.TryLogout()")
|
c.logf("direct.TryLogout()")
|
||||||
|
|
||||||
c.mu.Lock()
|
mustRegen, newURL, err := c.doLogin(ctx, loginOpt{Logout: true})
|
||||||
defer c.mu.Unlock()
|
c.logf("TryLogout control response: mustRegen=%v, newURL=%v, err=%v", mustRegen, newURL, err)
|
||||||
|
|
||||||
// TODO(crawshaw): Tell the server. This node key should be
|
c.mu.Lock()
|
||||||
// immediately invalidated.
|
|
||||||
//if !c.persist.PrivateNodeKey.IsZero() {
|
|
||||||
//}
|
|
||||||
c.persist = persist.Persist{}
|
c.persist = persist.Persist{}
|
||||||
return nil
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
|
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
|
||||||
@ -298,10 +297,11 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, opt loginOpt) (newURL strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
type loginOpt struct {
|
type loginOpt struct {
|
||||||
Token *tailcfg.Oauth2Token
|
Token *tailcfg.Oauth2Token
|
||||||
Flags LoginFlags
|
Flags LoginFlags
|
||||||
Regen bool
|
Regen bool
|
||||||
URL string
|
URL string
|
||||||
|
Logout bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, err error) {
|
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, err error) {
|
||||||
@ -324,14 +324,18 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
}
|
}
|
||||||
|
|
||||||
regen := opt.Regen
|
regen := opt.Regen
|
||||||
if expired {
|
if opt.Logout {
|
||||||
c.logf("Old key expired -> regen=true")
|
c.logf("logging out...")
|
||||||
systemd.Status("key expired; run 'tailscale up' to authenticate")
|
} else {
|
||||||
regen = true
|
if expired {
|
||||||
}
|
c.logf("Old key expired -> regen=true")
|
||||||
if (opt.Flags & LoginInteractive) != 0 {
|
systemd.Status("key expired; run 'tailscale up' to authenticate")
|
||||||
c.logf("LoginInteractive -> regen=true")
|
regen = true
|
||||||
regen = true
|
}
|
||||||
|
if (opt.Flags & LoginInteractive) != 0 {
|
||||||
|
c.logf("LoginInteractive -> regen=true")
|
||||||
|
regen = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, opt.URL != "")
|
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, opt.URL != "")
|
||||||
@ -348,8 +352,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
}
|
}
|
||||||
|
|
||||||
var oldNodeKey wgkey.Key
|
var oldNodeKey wgkey.Key
|
||||||
if opt.URL != "" {
|
switch {
|
||||||
} else if regen || persist.PrivateNodeKey.IsZero() {
|
case opt.Logout:
|
||||||
|
tryingNewKey = persist.PrivateNodeKey
|
||||||
|
case opt.URL != "":
|
||||||
|
// Nothing.
|
||||||
|
case regen || persist.PrivateNodeKey.IsZero():
|
||||||
c.logf("Generating a new nodekey.")
|
c.logf("Generating a new nodekey.")
|
||||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||||
key, err := wgkey.NewPrivate()
|
key, err := wgkey.NewPrivate()
|
||||||
@ -358,7 +366,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
return regen, opt.URL, err
|
return regen, opt.URL, err
|
||||||
}
|
}
|
||||||
tryingNewKey = key
|
tryingNewKey = key
|
||||||
} else {
|
default:
|
||||||
// Try refreshing the current key first
|
// Try refreshing the current key first
|
||||||
tryingNewKey = persist.PrivateNodeKey
|
tryingNewKey = persist.PrivateNodeKey
|
||||||
}
|
}
|
||||||
@ -367,6 +375,9 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tryingNewKey.IsZero() {
|
if tryingNewKey.IsZero() {
|
||||||
|
if opt.Logout {
|
||||||
|
return false, "", errors.New("no nodekey to log out")
|
||||||
|
}
|
||||||
log.Fatalf("tryingNewKey is empty, give up")
|
log.Fatalf("tryingNewKey is empty, give up")
|
||||||
}
|
}
|
||||||
if backendLogID == "" {
|
if backendLogID == "" {
|
||||||
@ -382,6 +393,9 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
Followup: opt.URL,
|
Followup: opt.URL,
|
||||||
Timestamp: &now,
|
Timestamp: &now,
|
||||||
}
|
}
|
||||||
|
if opt.Logout {
|
||||||
|
request.Expiry = time.Unix(123, 0) // far in the past
|
||||||
|
}
|
||||||
c.logf("RegisterReq: onode=%v node=%v fup=%v",
|
c.logf("RegisterReq: onode=%v node=%v fup=%v",
|
||||||
request.OldNodeKey.ShortString(),
|
request.OldNodeKey.ShortString(),
|
||||||
request.NodeKey.ShortString(), opt.URL != "")
|
request.NodeKey.ShortString(), opt.URL != "")
|
||||||
@ -403,6 +417,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
c.logf("RegisterReq sign error: %v", err)
|
c.logf("RegisterReq sign error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if debugRegister {
|
||||||
|
j, _ := json.MarshalIndent(request, "", "\t")
|
||||||
|
c.logf("RegisterRequest: %s", j)
|
||||||
|
}
|
||||||
|
|
||||||
bodyData, err := encode(request, &serverKey, &machinePrivKey)
|
bodyData, err := encode(request, &serverKey, &machinePrivKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return regen, opt.URL, err
|
return regen, opt.URL, err
|
||||||
@ -431,6 +450,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||||
return regen, opt.URL, fmt.Errorf("register request: %v", err)
|
return regen, opt.URL, fmt.Errorf("register request: %v", err)
|
||||||
}
|
}
|
||||||
|
if debugRegister {
|
||||||
|
j, _ := json.MarshalIndent(resp, "", "\t")
|
||||||
|
c.logf("RegisterResponse: %s", j)
|
||||||
|
}
|
||||||
|
|
||||||
// Log without PII:
|
// Log without PII:
|
||||||
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
|
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
|
||||||
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
|
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
|
||||||
@ -902,7 +926,10 @@ func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey
|
|||||||
return decodeMsg(msg, v, serverKey, mkey)
|
return decodeMsg(msg, v, serverKey, mkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
|
var (
|
||||||
|
debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
|
||||||
|
debugRegister, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_REGISTER"))
|
||||||
|
)
|
||||||
|
|
||||||
var jsonEscapedZero = []byte(`\u0000`)
|
var jsonEscapedZero = []byte(`\u0000`)
|
||||||
|
|
||||||
|
@ -1961,19 +1961,28 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
|
|||||||
// transitions the local engine to the logged-out state without
|
// transitions the local engine to the logged-out state without
|
||||||
// waiting for controlclient to be in that state.
|
// waiting for controlclient to be in that state.
|
||||||
//
|
//
|
||||||
// TODO(danderson): controlclient Logout does nothing useful, and we
|
|
||||||
// shouldn't be transitioning to a state based on what we believe
|
|
||||||
// controlclient may have done.
|
|
||||||
//
|
|
||||||
// NOTE(apenwarr): No easy way to persist logged-out status.
|
// NOTE(apenwarr): No easy way to persist logged-out status.
|
||||||
// Maybe that's for the better; if someone logs out accidentally,
|
// Maybe that's for the better; if someone logs out accidentally,
|
||||||
// rebooting will fix it.
|
// rebooting will fix it.
|
||||||
func (b *LocalBackend) Logout() {
|
func (b *LocalBackend) Logout() {
|
||||||
|
b.logout(context.Background(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) LogoutSync(ctx context.Context) error {
|
||||||
|
return b.logout(ctx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
cc := b.cc
|
cc := b.cc
|
||||||
b.setNetMapLocked(nil)
|
b.setNetMapLocked(nil)
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
b.EditPrefs(&ipn.MaskedPrefs{
|
||||||
|
WantRunningSet: true,
|
||||||
|
Prefs: ipn.Prefs{WantRunning: true},
|
||||||
|
})
|
||||||
|
|
||||||
if cc == nil {
|
if cc == nil {
|
||||||
// Double Logout can happen via repeated IPN
|
// Double Logout can happen via repeated IPN
|
||||||
// connections to ipnserver making it repeatedly
|
// connections to ipnserver making it repeatedly
|
||||||
@ -1982,16 +1991,22 @@ func (b *LocalBackend) Logout() {
|
|||||||
// on the transition to zero.
|
// on the transition to zero.
|
||||||
// Previously this crashed when we asserted that c was non-nil
|
// Previously this crashed when we asserted that c was non-nil
|
||||||
// here.
|
// here.
|
||||||
return
|
return errors.New("no controlclient")
|
||||||
}
|
}
|
||||||
|
|
||||||
cc.Logout()
|
var err error
|
||||||
|
if sync {
|
||||||
|
err = cc.Logout(ctx)
|
||||||
|
} else {
|
||||||
|
cc.StartLogout()
|
||||||
|
}
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
b.setNetMapLocked(nil)
|
b.setNetMapLocked(nil)
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
b.stateMachine()
|
b.stateMachine()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// assertClientLocked crashes if there is no controlclient in this backend.
|
// assertClientLocked crashes if there is no controlclient in this backend.
|
||||||
|
@ -87,6 +87,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.serveGoroutines(w, r)
|
h.serveGoroutines(w, r)
|
||||||
case "/localapi/v0/status":
|
case "/localapi/v0/status":
|
||||||
h.serveStatus(w, r)
|
h.serveStatus(w, r)
|
||||||
|
case "/localapi/v0/logout":
|
||||||
|
h.serveLogout(w, r)
|
||||||
case "/localapi/v0/prefs":
|
case "/localapi/v0/prefs":
|
||||||
h.servePrefs(w, r)
|
h.servePrefs(w, r)
|
||||||
case "/localapi/v0/check-ip-forwarding":
|
case "/localapi/v0/check-ip-forwarding":
|
||||||
@ -200,6 +202,23 @@ func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
e.Encode(st)
|
e.Encode(st)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "logout access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "want POST", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := h.b.LogoutSync(r.Context())
|
||||||
|
if err == nil {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.PermitRead {
|
if !h.PermitRead {
|
||||||
http.Error(w, "prefs access denied", http.StatusForbidden)
|
http.Error(w, "prefs access denied", http.StatusForbidden)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user