Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 67325d3

Browse files
Maisem Alimaisem
Maisem Ali
authored andcommitted
cmd/tailscale/cli: add lose-ssh risk
This makes it so that the user is notified that the action they are about to take may result in them getting disconnected from the machine. It then waits for 5s for the user to maybe Ctrl+C out of it. It also introduces a `--accept-risk=lose-ssh` flag for automation, which allows the caller to pre-acknowledge the risk. The two actions that cause this are: - updating `--ssh` from `true` to `false` - running `tailscale down` Updates tailscale#3802 Signed-off-by: Maisem Ali <[email protected]>
1 parent 1336fb7 commit 67325d3

File tree

5 files changed

+185
-2
lines changed

5 files changed

+185
-2
lines changed

cmd/tailscale/cli/down.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cli
66

77
import (
88
"context"
9+
"flag"
910
"fmt"
1011

1112
"github.com/peterbourgon/ff/v3/ffcli"
@@ -17,14 +18,27 @@ var downCmd = &ffcli.Command{
1718
ShortUsage: "down",
1819
ShortHelp: "Disconnect from Tailscale",
1920

20-
Exec: runDown,
21+
Exec: runDown,
22+
FlagSet: newDownFlagSet(),
23+
}
24+
25+
func newDownFlagSet() *flag.FlagSet {
26+
downf := newFlagSet("down")
27+
registerAcceptRiskFlag(downf)
28+
return downf
2129
}
2230

2331
func runDown(ctx context.Context, args []string) error {
2432
if len(args) > 0 {
2533
return fmt.Errorf("too many non-flag arguments: %q", args)
2634
}
2735

36+
if isSSHOverTailscale() {
37+
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
38+
return err
39+
}
40+
}
41+
2842
st, err := localClient.Status(ctx)
2943
if err != nil {
3044
return fmt.Errorf("error fetching current status: %w", err)

cmd/tailscale/cli/risks.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package cli
6+
7+
import (
8+
"errors"
9+
"flag"
10+
"fmt"
11+
"os"
12+
"os/signal"
13+
"strings"
14+
"syscall"
15+
"time"
16+
)
17+
18+
var (
19+
riskTypes []string
20+
acceptedRisks string
21+
riskLoseSSH = registerRiskType("lose-ssh")
22+
)
23+
24+
func registerRiskType(riskType string) string {
25+
riskTypes = append(riskTypes, riskType)
26+
return riskType
27+
}
28+
29+
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
30+
// in presentRiskToUser.
31+
func registerAcceptRiskFlag(f *flag.FlagSet) {
32+
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
33+
}
34+
35+
// riskAccepted reports whether riskType is in acceptedRisks.
36+
func riskAccepted(riskType string) bool {
37+
for _, r := range strings.Split(acceptedRisks, ",") {
38+
if r == riskType {
39+
return true
40+
}
41+
}
42+
return false
43+
}
44+
45+
var errAborted = errors.New("aborted, no changes made")
46+
47+
// riskAbortTimeSeconds is the number of seconds to wait after displaying the
48+
// risk message before continuing with the operation.
49+
// It is used by the presentRiskToUser function below.
50+
const riskAbortTimeSeconds = 5
51+
52+
// presentRiskToUser displays the risk message and waits for the user to
53+
// cancel. It returns errorAborted if the user aborts.
54+
func presentRiskToUser(riskType, riskMessage string) error {
55+
if riskAccepted(riskType) {
56+
return nil
57+
}
58+
fmt.Println(riskMessage)
59+
fmt.Printf("To skip this warning, use --accept-risk=%s\n", riskType)
60+
61+
interrupt := make(chan os.Signal, 1)
62+
signal.Notify(interrupt, syscall.SIGINT)
63+
var msgLen int
64+
for left := riskAbortTimeSeconds; left > 0; left-- {
65+
msg := fmt.Sprintf("\rContinuing in %d seconds...", left)
66+
msgLen = len(msg)
67+
fmt.Print(msg)
68+
select {
69+
case <-interrupt:
70+
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen+1))
71+
return errAborted
72+
case <-time.After(time.Second):
73+
continue
74+
}
75+
}
76+
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen))
77+
return errAborted
78+
}

cmd/tailscale/cli/ssh.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"inet.af/netaddr"
2222
"tailscale.com/envknob"
2323
"tailscale.com/ipn/ipnstate"
24+
"tailscale.com/net/tsaddr"
2425
)
2526

2627
var sshCmd = &ffcli.Command{
@@ -179,3 +180,28 @@ func nodeDNSNameFromArg(st *ipnstate.Status, arg string) (dnsName string, ok boo
179180
}
180181
return "", false
181182
}
183+
184+
// getSSHClientEnvVar returns the "SSH_CLIENT" environment variable
185+
// for the current process group, if any.
186+
var getSSHClientEnvVar = func() string {
187+
return ""
188+
}
189+
190+
// isSSHOverTailscale checks if the invocation is in a SSH session over Tailscale.
191+
// It is used to detect if the user is about to take an action that might result in them
192+
// disconnecting from the machine (e.g. disabling SSH)
193+
func isSSHOverTailscale() bool {
194+
sshClient := getSSHClientEnvVar()
195+
if sshClient == "" {
196+
return false
197+
}
198+
ipStr, _, ok := strings.Cut(sshClient, " ")
199+
if !ok {
200+
return false
201+
}
202+
ip, err := netaddr.ParseIP(ipStr)
203+
if err != nil {
204+
return false
205+
}
206+
return tsaddr.IsTailscaleIP(ip)
207+
}

cmd/tailscale/cli/ssh_unix.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !js && !windows
6+
// +build !js,!windows
7+
8+
package cli
9+
10+
import (
11+
"bytes"
12+
"os"
13+
"path/filepath"
14+
"runtime"
15+
"strconv"
16+
17+
"golang.org/x/sys/unix"
18+
)
19+
20+
func init() {
21+
getSSHClientEnvVar = func() string {
22+
if os.Getenv("SUDO_USER") == "" {
23+
// No sudo, just check the env.
24+
return os.Getenv("SSH_CLIENT")
25+
}
26+
if runtime.GOOS != "linux" {
27+
// TODO(maisem): implement this for other platforms. It's not clear
28+
// if there is a way to get the environment for a given process on
29+
// darwin and bsd.
30+
return ""
31+
}
32+
// SID is the session ID of the user's login session.
33+
// It is also the process ID of the original shell that the user logged in with.
34+
// We only need to check the environment of that process.
35+
sid, err := unix.Getsid(os.Getpid())
36+
if err != nil {
37+
return ""
38+
}
39+
b, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(sid), "environ"))
40+
if err != nil {
41+
return ""
42+
}
43+
prefix := []byte("SSH_CLIENT=")
44+
for _, env := range bytes.Split(b, []byte{0}) {
45+
if bytes.HasPrefix(env, prefix) {
46+
return string(env[len(prefix):])
47+
}
48+
}
49+
return ""
50+
}
51+
}

cmd/tailscale/cli/up.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
114114
case "windows":
115115
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
116116
}
117+
118+
registerAcceptRiskFlag(upf)
117119
return upf
118120
}
119121

@@ -465,6 +467,18 @@ func runUp(ctx context.Context, args []string) error {
465467
backendState: st.BackendState,
466468
curExitNodeIP: exitNodeIP(curPrefs, st),
467469
}
470+
471+
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
472+
if upArgs.runSSH {
473+
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
474+
} else {
475+
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
476+
}
477+
if err != nil {
478+
return err
479+
}
480+
}
481+
468482
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
469483
if err != nil {
470484
fatalf("%s", err)
@@ -705,7 +719,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
705719
// correspond to an ipn.Pref.
706720
func preflessFlag(flagName string) bool {
707721
switch flagName {
708-
case "auth-key", "force-reauth", "reset", "qr", "json":
722+
case "auth-key", "force-reauth", "reset", "qr", "json", "accept-risk":
709723
return true
710724
}
711725
return false

0 commit comments

Comments
 (0)