diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index 83afce82c..6af322842 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -31,7 +31,7 @@ func ActLikeCLI() bool { return false } switch os.Args[1] { - case "up", "status", "netcheck", "ping", "version", + case "up", "down", "status", "netcheck", "ping", "version", "debug", "-V", "--version", "-h", "--help": return true @@ -58,6 +58,7 @@ change in the future. `), Subcommands: []*ffcli.Command{ upCmd, + downCmd, netcheckCmd, statusCmd, pingCmd, diff --git a/cmd/tailscale/cli/down.go b/cmd/tailscale/cli/down.go new file mode 100644 index 000000000..5ea1eb6f1 --- /dev/null +++ b/cmd/tailscale/cli/down.go @@ -0,0 +1,67 @@ +// 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" + "time" + + "github.com/peterbourgon/ff/v2/ffcli" + "tailscale.com/ipn" +) + +var downCmd = &ffcli.Command{ + Name: "down", + ShortUsage: "down", + ShortHelp: "Disconnect from Tailscale", + + Exec: runDown, +} + +func runDown(ctx context.Context, args []string) error { + if len(args) > 0 { + log.Fatalf("too many non-flag arguments: %q", args) + } + + c, bc, ctx, cancel := connect(ctx) + defer cancel() + + timer := time.AfterFunc(5*time.Second, func() { + log.Fatalf("timeout running stop") + }) + defer timer.Stop() + + bc.SetNotifyCallback(func(n ipn.Notify) { + if n.ErrMessage != nil { + log.Fatal(*n.ErrMessage) + } + if n.Status != nil { + cur := n.Status.BackendState + switch cur { + case "Stopped": + log.Printf("already stopped") + cancel() + default: + log.Printf("was in state %q", cur) + } + return + } + if n.State != nil { + log.Printf("now in state %q", *n.State) + if *n.State == ipn.Stopped { + cancel() + } + return + } + log.Printf("Notify: %#v", n) + }) + + bc.RequestStatus() + bc.SetWantRunning(false) + pump(ctx, bc, c) + + return nil +} diff --git a/ipn/backend.go b/ipn/backend.go index 0e9827b59..abfd7a778 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -144,6 +144,9 @@ type Backend interface { // WantRunning. This may cause the wireguard engine to // reconfigure or stop. SetPrefs(*Prefs) + // SetWantRunning is like SetPrefs but sets only the + // WantRunning field. + SetWantRunning(wantRunning bool) // RequestEngineStatus polls for an update from the wireguard // engine. Only needed if you want to display byte // counts. Connection events are emitted automatically without diff --git a/ipn/fake_test.go b/ipn/fake_test.go index 3d504d10a..9b16cceaa 100644 --- a/ipn/fake_test.go +++ b/ipn/fake_test.go @@ -79,6 +79,10 @@ func (b *FakeBackend) SetPrefs(new *Prefs) { } } +func (b *FakeBackend) SetWantRunning(v bool) { + b.SetPrefs(&Prefs{WantRunning: v}) +} + func (b *FakeBackend) RequestEngineStatus() { b.notify(Notify{Engine: &EngineStatus{}}) } diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 67a115ee2..55b9e11f7 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -90,6 +90,12 @@ type StatusBuilder struct { st Status } +func (sb *StatusBuilder) SetBackendState(v string) { + sb.mu.Lock() + defer sb.mu.Unlock() + sb.st.BackendState = v +} + func (sb *StatusBuilder) Status() *Status { sb.mu.Lock() defer sb.mu.Unlock() diff --git a/ipn/local.go b/ipn/local.go index bddf1db0a..41d87c981 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -144,6 +144,8 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) { b.mu.Lock() defer b.mu.Unlock() + sb.SetBackendState(b.state.String()) + // TODO: hostinfo, and its networkinfo // TODO: EngineStatus copy (and deprecate it?) if b.netMap != nil { @@ -796,6 +798,18 @@ func (b *LocalBackend) shieldsAreUp() bool { return b.prefs.ShieldsUp } +func (b *LocalBackend) SetWantRunning(wantRunning bool) { + b.mu.Lock() + new := b.prefs.Clone() + b.mu.Unlock() + if new.WantRunning == wantRunning { + return + } + new.WantRunning = wantRunning + b.logf("SetWantRunning: %v", wantRunning) + b.SetPrefs(new) +} + // SetPrefs saves new user preferences and propagates them throughout // the system. Implements Backend. func (b *LocalBackend) SetPrefs(new *Prefs) { diff --git a/ipn/message.go b/ipn/message.go index cad014a61..4e672e4fc 100644 --- a/ipn/message.go +++ b/ipn/message.go @@ -57,6 +57,7 @@ type Command struct { Login *oauth2.Token Logout *NoArgs SetPrefs *SetPrefsArgs + SetWantRunning *bool RequestEngineStatus *NoArgs RequestStatus *NoArgs FakeExpireAfter *FakeExpireAfterArgs @@ -144,6 +145,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error { } else if c := cmd.SetPrefs; c != nil { bs.b.SetPrefs(c.New) return nil + } else if c := cmd.SetWantRunning; c != nil { + bs.b.SetWantRunning(*c) + return nil } else if c := cmd.RequestEngineStatus; c != nil { bs.b.RequestEngineStatus() return nil @@ -266,6 +270,10 @@ func (bc *BackendClient) Ping(ip string) { bc.send(Command{Ping: &PingArgs{IP: ip}}) } +func (bc *BackendClient) SetWantRunning(v bool) { + bc.send(Command{SetWantRunning: &v}) +} + // MaxMessageSize is the maximum message size, in bytes. const MaxMessageSize = 10 << 20