mirror of https://github.com/tailscale/tailscale/
Merge f8c9f1aca7 into 37b4dd047f
commit
da224d515d
@ -0,0 +1,212 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/types/opt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getCmd = &ffcli.Command{
|
||||||
|
Name: "get",
|
||||||
|
ShortUsage: "tailscale get <setting>",
|
||||||
|
ShortHelp: "Print specified settings",
|
||||||
|
LongHelp: `"tailscale get" prints a specific setting.
|
||||||
|
|
||||||
|
Only one setting will be printed.
|
||||||
|
|
||||||
|
SETTINGS
|
||||||
|
` + getSettings.settings(),
|
||||||
|
FlagSet: newFlagSet("get"),
|
||||||
|
Exec: runGet,
|
||||||
|
UsageFunc: usageFuncNoDefaultValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
type getSettingsT map[string]string
|
||||||
|
|
||||||
|
// makeGetSettingsT returns a [getSettingsT] with all of the settings controlled
|
||||||
|
// by the given flagsets. Each setting gets its help text from its flag's Usage.
|
||||||
|
func makeGetSettingsT(flagsets ...*flag.FlagSet) getSettingsT {
|
||||||
|
settings := make(getSettingsT)
|
||||||
|
for _, fs := range flagsets {
|
||||||
|
fs.VisitAll(func(f *flag.Flag) {
|
||||||
|
if preflessFlag(f.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := settings[f.Name]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings[f.Name] = f.Usage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings returns a string of all the settings known to the get command.
|
||||||
|
// The result is formatted for use in help text.
|
||||||
|
func (s getSettingsT) settings() string {
|
||||||
|
var b strings.Builder
|
||||||
|
names := slices.Sorted(maps.Keys(s))
|
||||||
|
for _, name := range names {
|
||||||
|
usage := s.usage(name)
|
||||||
|
if strings.HasPrefix(usage, hidden) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(" ")
|
||||||
|
b.WriteString(name)
|
||||||
|
b.WriteString("\n ")
|
||||||
|
b.WriteString(usage)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupPrefOfFlag(p *ipn.Prefs, name string) (v reflect.Value, err error) {
|
||||||
|
prefs, ok := prefsOfFlag[name]
|
||||||
|
if !ok {
|
||||||
|
return reflect.Value{}, fmt.Errorf("missing pref flag mapping for %s", name)
|
||||||
|
}
|
||||||
|
if len(prefs) != 1 {
|
||||||
|
return reflect.Value{}, fmt.Errorf("expected only one pref flag mapping for %s, not %q", name, prefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
switch r := recover().(type) {
|
||||||
|
case nil: // noop
|
||||||
|
case error:
|
||||||
|
err = fmt.Errorf("bad pref flag %q for %s: %w", prefs, name, r)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("bad pref flag %q for %s: %v", prefs, name, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
v = reflect.ValueOf(p).Elem()
|
||||||
|
for _, n := range strings.Split(prefs[0], ".") {
|
||||||
|
v = v.FieldByName(n)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup returns a function that can be used to look up the associated
|
||||||
|
// preference for a given flag name.
|
||||||
|
func (s getSettingsT) lookup(name string) func(*ipn.Prefs, *ipnstate.Status) string {
|
||||||
|
if _, ok := s[name]; !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "advertise-connector":
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
value, err := lookupPrefOfFlag(p, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", value.FieldByName("Advertise"))
|
||||||
|
}
|
||||||
|
case "advertise-exit-node":
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
return strconv.FormatBool(p.AdvertisesExitNode())
|
||||||
|
}
|
||||||
|
case "advertise-tags":
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
value, err := lookupPrefOfFlag(p, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
v := value.Interface().([]string)
|
||||||
|
return strings.Join(v, ",")
|
||||||
|
}
|
||||||
|
case "advertise-routes":
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for i, r := range p.AdvertiseRoutes {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteRune(',')
|
||||||
|
}
|
||||||
|
b.WriteString(r.String())
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
case "exit-node":
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
ip := exitNodeIP(p, st)
|
||||||
|
if ip.IsValid() {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
case "snat-subnet-routes":
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
value, err := lookupPrefOfFlag(p, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%t", !value.Bool())
|
||||||
|
}
|
||||||
|
case "stateful-filtering":
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
value, err := lookupPrefOfFlag(p, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
v := value.Interface().(opt.Bool)
|
||||||
|
return v.Not().String()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||||
|
value, err := lookupPrefOfFlag(p, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", value) // fmt prints the concrete value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage returns the usage string for a given flag name.
|
||||||
|
func (s getSettingsT) usage(name string) string {
|
||||||
|
usage, ok := s[name]
|
||||||
|
if !ok {
|
||||||
|
panic("unknown setting: " + name)
|
||||||
|
}
|
||||||
|
return usage
|
||||||
|
}
|
||||||
|
|
||||||
|
var getSettings = makeGetSettingsT(setFlagSet, upFlagSet)
|
||||||
|
|
||||||
|
func runGet(ctx context.Context, args []string) (retErr error) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
fatalf("must provide only one non-flag argument: %q", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
setting := args[0]
|
||||||
|
lookup := getSettings.lookup(setting)
|
||||||
|
if lookup == nil {
|
||||||
|
fatalf("unknown setting: %s", setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs, err := localClient.GetPrefs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := localClient.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outln(lookup(prefs, status))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -0,0 +1,247 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/client/local"
|
||||||
|
"tailscale.com/ipn/ipnlocal"
|
||||||
|
"tailscale.com/ipn/ipnserver"
|
||||||
|
"tailscale.com/ipn/store/mem"
|
||||||
|
"tailscale.com/tsd"
|
||||||
|
"tailscale.com/tstest"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/logid"
|
||||||
|
"tailscale.com/wgengine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSettingsArePairedWithPrefFlags(t *testing.T) {
|
||||||
|
// Every get setting should have a corresponding prefsOfFlag.
|
||||||
|
// Some prefsOfFlag might not be in getSettings because it is either
|
||||||
|
// a prefless flag or it doesn't apply to this operating system.
|
||||||
|
for name := range getSettings {
|
||||||
|
if _, ok := prefsOfFlag[name]; !ok {
|
||||||
|
t.Errorf("mismatched getter: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSettingsArePairedWithSetFlags(t *testing.T) {
|
||||||
|
// Every set flag should have a corresponding get setting,
|
||||||
|
// except for prefless flags, which don't have get settings.
|
||||||
|
setFlagSet.VisitAll(func(f *flag.Flag) {
|
||||||
|
if preflessFlag(f.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := getSettings[f.Name]; !ok {
|
||||||
|
t.Errorf("missing set flag: %s", f.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSettingsArePairedWithUpFlags(t *testing.T) {
|
||||||
|
// Every up flag should have a corresponding get setting,
|
||||||
|
// except for prefless flags, which don't have get settings.
|
||||||
|
upFlagSet.VisitAll(func(f *flag.Flag) {
|
||||||
|
if preflessFlag(f.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := getSettings[f.Name]; !ok {
|
||||||
|
t.Errorf("missing up flag: %s", f.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSettingsWillRoundtrip(t *testing.T) {
|
||||||
|
for _, tt := range []struct{ flag, value string }{
|
||||||
|
// --nickname is at the top-level in .ProfileName
|
||||||
|
{"nickname", "home"},
|
||||||
|
{"nickname", "work"},
|
||||||
|
// --update-check is nested in .AutoUpdate.Check
|
||||||
|
{"update-check", "false"},
|
||||||
|
{"update-check", "true"},
|
||||||
|
} {
|
||||||
|
name := tt.flag + "=" + tt.value
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// Capture outln calls
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
tstest.Replace[io.Writer](t, &Stdout, &stdout)
|
||||||
|
|
||||||
|
// Use a fake localClient that processes settings updates
|
||||||
|
lc := newLocalClient(t)
|
||||||
|
tstest.Replace(t, &localClient, lc)
|
||||||
|
|
||||||
|
// setCmd.FlagSet must be reset to parse arguments
|
||||||
|
cmd := *setCmd
|
||||||
|
cmd.FlagSet = newSetFlagSet(effectiveGOOS(), &setArgs)
|
||||||
|
tstest.Replace(t, &setCmd, &cmd)
|
||||||
|
tstest.Replace(t, &setFlagSet, cmd.FlagSet)
|
||||||
|
|
||||||
|
// Capture errors from setCmd
|
||||||
|
cmd.FlagSet.Init(cmd.FlagSet.Name(), flag.PanicOnError)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Fatal(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Capture errors from getCmd
|
||||||
|
tstest.Replace(t, &Fatalf, t.Fatalf)
|
||||||
|
|
||||||
|
arg := "--" + tt.flag + "=" + tt.value
|
||||||
|
t.Logf("tailscale set %s", arg)
|
||||||
|
if err := setCmd.ParseAndRun(t.Context(), []string{arg}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.Reset()
|
||||||
|
arg = tt.flag
|
||||||
|
t.Logf("tailscale get %s", arg)
|
||||||
|
if err := runGet(t.Context(), []string{arg}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := stdout.String()
|
||||||
|
want := tt.value + "\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDefaultSettings(t *testing.T) {
|
||||||
|
// Fetch the default settings from all of the flags
|
||||||
|
for _, fs := range []*flag.FlagSet{setFlagSet, upFlagSet} {
|
||||||
|
fs.VisitAll(func(f *flag.Flag) {
|
||||||
|
if preflessFlag(f.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(f.Name, func(t *testing.T) {
|
||||||
|
// Capture outln calls
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
tstest.Replace[io.Writer](t, &Stdout, &stdout)
|
||||||
|
|
||||||
|
// Use a fake localClient that processes settings updates
|
||||||
|
lc := newLocalClient(t)
|
||||||
|
tstest.Replace(t, &localClient, lc)
|
||||||
|
|
||||||
|
if err := runGet(t.Context(), []string{f.Name}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := f.DefValue
|
||||||
|
switch f.Name {
|
||||||
|
case "auto-update":
|
||||||
|
// Unset by tailscale up.
|
||||||
|
want = "unset"
|
||||||
|
case "login-server":
|
||||||
|
// The default settings is empty,
|
||||||
|
// but tailscale up sets it on start.
|
||||||
|
want = ""
|
||||||
|
}
|
||||||
|
want += "\n"
|
||||||
|
|
||||||
|
got := stdout.String()
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("tailscale get %s: got %q, want %q", f.Name, got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setFlagSet.VisitAll(func(f *flag.Flag) {
|
||||||
|
if preflessFlag(f.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := getSettings[f.Name]; !ok {
|
||||||
|
t.Errorf("missing set flag: %s", f.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(sfllaw): Replace the following test IPN server and client once
|
||||||
|
// https://github.com/tailscale/tailscale/issues/15575 is complete.
|
||||||
|
|
||||||
|
func newLocalListener(t testing.TB) net.Listener {
|
||||||
|
sock := filepath.Join(t.TempDir(), "sock")
|
||||||
|
l, err := net.Listen("unix", sock)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocalBackend(t testing.TB, logID logid.PublicID) *ipnlocal.LocalBackend {
|
||||||
|
var logf logger.Logf = func(_ string, _ ...any) {}
|
||||||
|
if testing.Verbose() {
|
||||||
|
logf = tstest.WhileTestRunningLogger(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
sys := new(tsd.System)
|
||||||
|
if _, ok := sys.StateStore.GetOK(); !ok {
|
||||||
|
sys.Set(new(mem.Store))
|
||||||
|
}
|
||||||
|
if _, ok := sys.Engine.GetOK(); !ok {
|
||||||
|
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(eng.Close)
|
||||||
|
|
||||||
|
sys.Set(eng)
|
||||||
|
}
|
||||||
|
|
||||||
|
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return lb
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocalClient(t testing.TB) *local.Client {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Connect over a Unix domain socket for admin access,
|
||||||
|
// which keeps ipnauth_notwindows happy, but ipnauth_windows
|
||||||
|
// wants a different guarantee on Windows.
|
||||||
|
t.Skip("newLocalClient doesn't know to authorize with safesocket.WindowsClientConn")
|
||||||
|
}
|
||||||
|
|
||||||
|
var logf logger.Logf = func(_ string, _ ...any) {}
|
||||||
|
if testing.Verbose() {
|
||||||
|
logf = tstest.WhileTestRunningLogger(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
logID := logid.PublicID{}
|
||||||
|
|
||||||
|
lb := newLocalBackend(t, logID)
|
||||||
|
t.Cleanup(lb.Shutdown)
|
||||||
|
|
||||||
|
// Connect over Unix domain socket for admin access.
|
||||||
|
l := newLocalListener(t)
|
||||||
|
t.Cleanup(func() { l.Close() })
|
||||||
|
|
||||||
|
srv := ipnserver.New(logf, logID, lb.NetMon())
|
||||||
|
srv.SetLocalBackend(lb)
|
||||||
|
|
||||||
|
go srv.Run(t.Context(), l)
|
||||||
|
|
||||||
|
return &local.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
var std net.Dialer
|
||||||
|
return std.DialContext(ctx, "unix", l.Addr().String())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue