mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +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{
|
Subcommands: []*ffcli.Command{
|
||||||
upCmd,
|
upCmd,
|
||||||
downCmd,
|
downCmd,
|
||||||
|
setCmd,
|
||||||
logoutCmd,
|
logoutCmd,
|
||||||
netcheckCmd,
|
netcheckCmd,
|
||||||
ipCmd,
|
ipCmd,
|
||||||
@ -177,7 +178,9 @@ func Run(args []string) (err error) {
|
|||||||
UsageFunc: usageFunc,
|
UsageFunc: usageFunc,
|
||||||
}
|
}
|
||||||
for _, c := range rootCmd.Subcommands {
|
for _, c := range rootCmd.Subcommands {
|
||||||
c.UsageFunc = usageFunc
|
if c.UsageFunc == nil {
|
||||||
|
c.UsageFunc = usageFunc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if envknob.UseWIPCode() {
|
if envknob.UseWIPCode() {
|
||||||
rootCmd.Subcommands = append(rootCmd.Subcommands, idTokenCmd)
|
rootCmd.Subcommands = append(rootCmd.Subcommands, idTokenCmd)
|
||||||
@ -292,6 +295,56 @@ func strSliceContains(ss []string, s string) bool {
|
|||||||
return false
|
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 {
|
func usageFunc(c *ffcli.Command) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
|
|||||||
fs := newUpFlagSet(goos, &upArgs)
|
fs := newUpFlagSet(goos, &upArgs)
|
||||||
fs.VisitAll(func(f *flag.Flag) {
|
fs.VisitAll(func(f *flag.Flag) {
|
||||||
mp := new(ipn.MaskedPrefs)
|
mp := new(ipn.MaskedPrefs)
|
||||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
updateMaskedPrefsFromUpOrSetFlag(mp, f.Name)
|
||||||
got := mp.Pretty()
|
got := mp.Pretty()
|
||||||
wantEmpty := preflessFlag(f.Name)
|
wantEmpty := preflessFlag(f.Name)
|
||||||
isEmpty := got == "MaskedPrefs{}"
|
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
|
// Do this after validations to avoid the 5s delay if we're going to error
|
||||||
// out anyway.
|
// out anyway.
|
||||||
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
|
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
|
||||||
if wantSSH != haveSSH && isSSHOverTailscale() {
|
if err := presentSSHToggleRisk(wantSSH, haveSSH, env.upArgs.acceptedRisks); err != nil {
|
||||||
if wantSSH {
|
return false, nil, err
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
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 = env.flagSet.VisitAll
|
||||||
}
|
}
|
||||||
visitFlags(func(f *flag.Flag) {
|
visitFlags(func(f *flag.Flag) {
|
||||||
updateMaskedPrefsFromUpFlag(justEditMP, f.Name)
|
updateMaskedPrefsFromUpOrSetFlag(justEditMP, f.Name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return simpleUp, justEditMP, nil
|
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) {
|
func runUp(ctx context.Context, args []string) (retErr error) {
|
||||||
var egg bool
|
var egg bool
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@ -773,7 +776,7 @@ func preflessFlag(flagName string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
|
func updateMaskedPrefsFromUpOrSetFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||||
if preflessFlag(flagName) {
|
if preflessFlag(flagName) {
|
||||||
return
|
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 {
|
func (m *MaskedPrefs) Pretty() string {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return "MaskedPrefs{<nil>}"
|
return "MaskedPrefs{<nil>}"
|
||||||
|
@ -826,3 +826,48 @@ func TestControlURLOrDefault(t *testing.T) {
|
|||||||
t.Errorf("got %q; want %q", got, want)
|
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