ipn/{ipnlocal,localapi}, cmd/tailscale: add logout command

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/1685/head
Brad Fitzpatrick 3 years ago committed by Brad Fitzpatrick
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,

@ -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,16 +695,40 @@ 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()
}
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() 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

@ -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…
Cancel
Save