mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
cmd/tailscale/cli: add beginnings of tailscale set
Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
parent
a471681e28
commit
19b5586573
@ -157,6 +157,7 @@ func Run(args []string) (err error) {
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
downCmd,
|
||||
setCmd,
|
||||
logoutCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
@ -177,7 +178,9 @@ func Run(args []string) (err error) {
|
||||
UsageFunc: usageFunc,
|
||||
}
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
c.UsageFunc = usageFunc
|
||||
if c.UsageFunc == nil {
|
||||
c.UsageFunc = usageFunc
|
||||
}
|
||||
}
|
||||
if envknob.UseWIPCode() {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, idTokenCmd)
|
||||
@ -292,6 +295,56 @@ func strSliceContains(ss []string, s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
|
||||
func usageFuncNoDefaultValues(c *ffcli.Command) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "USAGE\n")
|
||||
if c.ShortUsage != "" {
|
||||
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
|
||||
} else {
|
||||
fmt.Fprintf(&b, " %s\n", c.Name)
|
||||
}
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
if c.LongHelp != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
|
||||
}
|
||||
|
||||
if len(c.Subcommands) > 0 {
|
||||
fmt.Fprintf(&b, "SUBCOMMANDS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
for _, subcommand := range c.Subcommands {
|
||||
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
|
||||
}
|
||||
tw.Flush()
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
|
||||
if countFlags(c.FlagSet) > 0 {
|
||||
fmt.Fprintf(&b, "FLAGS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
c.FlagSet.VisitAll(func(f *flag.Flag) {
|
||||
var s string
|
||||
name, usage := flag.UnquoteUsage(f)
|
||||
s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments.
|
||||
if len(name) > 0 {
|
||||
s += " " + name
|
||||
}
|
||||
// Four spaces before the tab triggers good alignment
|
||||
// for both 4- and 8-space tab stops.
|
||||
s += "\n \t"
|
||||
s += strings.ReplaceAll(usage, "\n", "\n \t")
|
||||
|
||||
fmt.Fprintln(&b, s)
|
||||
})
|
||||
tw.Flush()
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func usageFunc(c *ffcli.Command) string {
|
||||
var b strings.Builder
|
||||
|
||||
|
@ -39,7 +39,7 @@ func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
|
||||
fs := newUpFlagSet(goos, &upArgs)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
updateMaskedPrefsFromUpOrSetFlag(mp, f.Name)
|
||||
got := mp.Pretty()
|
||||
wantEmpty := preflessFlag(f.Name)
|
||||
isEmpty := got == "MaskedPrefs{}"
|
||||
|
131
cmd/tailscale/cli/set.go
Normal file
131
cmd/tailscale/cli/set.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2022 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"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/safesocket"
|
||||
)
|
||||
|
||||
var setCmd = &ffcli.Command{
|
||||
Name: "set",
|
||||
ShortUsage: "set [flags]",
|
||||
ShortHelp: "Change specified preferences",
|
||||
LongHelp: `"tailscale set" allows changing specific preferences.
|
||||
|
||||
Unlike "tailscale up", this command does not require the complete set of desired settings.
|
||||
|
||||
Only settings explicitly mentioned will be set. There are no default values.`,
|
||||
FlagSet: setFlagSet,
|
||||
Exec: runSet,
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
}
|
||||
|
||||
type setArgsT struct {
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
shieldsUp bool
|
||||
runSSH bool
|
||||
hostname string
|
||||
advertiseRoutes string
|
||||
advertiseDefaultRoute bool
|
||||
opUser string
|
||||
acceptedRisks string
|
||||
}
|
||||
|
||||
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf := newFlagSet("set")
|
||||
|
||||
setf.BoolVar(&setArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", false, "accept DNS configuration from the admin panel")
|
||||
setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
setf.BoolVar(&setArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
setf.BoolVar(&setArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
setf.BoolVar(&setArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
||||
setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
setf.StringVar(&setArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
registerAcceptRiskFlag(setf, &setArgs.acceptedRisks)
|
||||
return setf
|
||||
}
|
||||
|
||||
var (
|
||||
setArgs setArgsT
|
||||
setFlagSet = newSetFlagSet(effectiveGOOS(), &setArgs)
|
||||
)
|
||||
|
||||
func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
routes, err := calcAdvertiseRoutes(setArgs.advertiseRoutes, setArgs.advertiseDefaultRoute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maskedPrefs := &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
RouteAll: setArgs.acceptRoutes,
|
||||
CorpDNS: setArgs.acceptDNS,
|
||||
ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
|
||||
ShieldsUp: setArgs.shieldsUp,
|
||||
RunSSH: setArgs.runSSH,
|
||||
Hostname: setArgs.hostname,
|
||||
AdvertiseRoutes: routes,
|
||||
OperatorUser: setArgs.opUser,
|
||||
},
|
||||
}
|
||||
|
||||
if setArgs.exitNodeIP != "" {
|
||||
if err := maskedPrefs.Prefs.SetExitNodeIP(setArgs.exitNodeIP, st); err != nil {
|
||||
var e ipn.ExitNodeLocalIPError
|
||||
if errors.As(err, &e) {
|
||||
return fmt.Errorf("%w; did you mean --advertise-exit-node?", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
setFlagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpOrSetFlag(maskedPrefs, f.Name)
|
||||
})
|
||||
|
||||
if maskedPrefs.IsEmpty() {
|
||||
println("no flags specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
if maskedPrefs.RunSSHSet {
|
||||
curPrefs, err := localClient.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wantSSH, haveSSH := maskedPrefs.RunSSH, curPrefs.RunSSH
|
||||
if err := presentSSHToggleRisk(wantSSH, haveSSH, setArgs.acceptedRisks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = localClient.EditPrefs(ctx, maskedPrefs)
|
||||
return err
|
||||
}
|
@ -380,15 +380,8 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
// Do this after validations to avoid the 5s delay if we're going to error
|
||||
// out anyway.
|
||||
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
|
||||
if wantSSH != haveSSH && isSSHOverTailscale() {
|
||||
if wantSSH {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, env.upArgs.acceptedRisks)
|
||||
} else {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, env.upArgs.acceptedRisks)
|
||||
}
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if err := presentSSHToggleRisk(wantSSH, haveSSH, env.upArgs.acceptedRisks); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
@ -413,13 +406,23 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
visitFlags = env.flagSet.VisitAll
|
||||
}
|
||||
visitFlags(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(justEditMP, f.Name)
|
||||
updateMaskedPrefsFromUpOrSetFlag(justEditMP, f.Name)
|
||||
})
|
||||
}
|
||||
|
||||
return simpleUp, justEditMP, nil
|
||||
}
|
||||
|
||||
func presentSSHToggleRisk(wantSSH, haveSSH bool, acceptedRisks string) error {
|
||||
if !isSSHOverTailscale() || wantSSH == haveSSH {
|
||||
return nil
|
||||
}
|
||||
if wantSSH {
|
||||
return presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, acceptedRisks)
|
||||
}
|
||||
return presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, acceptedRisks)
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
var egg bool
|
||||
if len(args) > 0 {
|
||||
@ -773,7 +776,7 @@ func preflessFlag(flagName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||
func updateMaskedPrefsFromUpOrSetFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||
if preflessFlag(flagName) {
|
||||
return
|
||||
}
|
||||
|
15
ipn/prefs.go
15
ipn/prefs.go
@ -244,6 +244,21 @@ func (p *Prefs) ApplyEdits(m *MaskedPrefs) {
|
||||
}
|
||||
}
|
||||
|
||||
// IsEmpty reports whether there are no masks set or if m is nil.
|
||||
func (m *MaskedPrefs) IsEmpty() bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
mv := reflect.ValueOf(m).Elem()
|
||||
fields := mv.NumField()
|
||||
for i := 1; i < fields; i++ {
|
||||
if mv.Field(i).Bool() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MaskedPrefs) Pretty() string {
|
||||
if m == nil {
|
||||
return "MaskedPrefs{<nil>}"
|
||||
|
@ -826,3 +826,48 @@ func TestControlURLOrDefault(t *testing.T) {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskedPrefsIsEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mp *MaskedPrefs
|
||||
wantEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
wantEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
wantEmpty: true,
|
||||
mp: &MaskedPrefs{},
|
||||
},
|
||||
{
|
||||
name: "no-masks",
|
||||
wantEmpty: true,
|
||||
mp: &MaskedPrefs{
|
||||
Prefs: Prefs{
|
||||
WantRunning: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-mask",
|
||||
wantEmpty: false,
|
||||
mp: &MaskedPrefs{
|
||||
Prefs: Prefs{
|
||||
WantRunning: true,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.mp.IsEmpty()
|
||||
if got != tc.wantEmpty {
|
||||
t.Fatalf("mp.IsEmpty = %t; want %t", got, tc.wantEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user