mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
ipn/{ipnlocal,localapi}: add API to toggle use of exit node
This is primarily for GUIs, so they don't need to remember the most recently used exit node themselves. This adds some CLI commands, but they're disabled and behind the WIP envknob, as we need to consider naming (on/off is ambiguous with running an exit node, etc) as well as automatic exit node selection in the future. For now the CLI commands are effectively developer debug things to test the LocalAPI. Updates tailscale/corp#18724 Change-Id: I9a32b00e3ffbf5b29bfdcad996a4296b5e37be7e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
3f4c5daa15
commit
a5e1f7d703
@ -1418,6 +1418,15 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
|
|||||||
return &cv, nil
|
return &cv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUseExitNode toggles the use of an exit node on or off.
|
||||||
|
// To turn it on, there must have been a previously used exit node.
|
||||||
|
// The most previously used one is reused.
|
||||||
|
// This is a convenience method for GUIs. To select an actual one, update the prefs.
|
||||||
|
func (lc *LocalClient) SetUseExitNode(ctx context.Context, on bool) error {
|
||||||
|
_, err := lc.send(ctx, "POST", "/localapi/v0/set-use-exit-node-enabled?enabled="+strconv.FormatBool(on), http.StatusOK, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
|
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
|
||||||
// the filesystem. This is used on platforms like Windows and MacOS to let
|
// the filesystem. This is used on platforms like Windows and MacOS to let
|
||||||
// Taildrive know to use the file server running in the GUI app.
|
// Taildrive know to use the file server running in the GUI app.
|
||||||
|
@ -833,6 +833,10 @@ func TestPrefFlagMapping(t *testing.T) {
|
|||||||
// Handled by the tailscale share subcommand, we don't want a CLI
|
// Handled by the tailscale share subcommand, we don't want a CLI
|
||||||
// flag for this.
|
// flag for this.
|
||||||
continue
|
continue
|
||||||
|
case "InternalExitNodePrior":
|
||||||
|
// Used internally by LocalBackend as part of exit node usage toggling.
|
||||||
|
// No CLI flag for this.
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
xmaps "golang.org/x/exp/maps"
|
xmaps "golang.org/x/exp/maps"
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
@ -25,7 +26,10 @@
|
|||||||
ShortUsage: "exit-node [flags]",
|
ShortUsage: "exit-node [flags]",
|
||||||
ShortHelp: "Show machines on your tailnet configured as exit nodes",
|
ShortHelp: "Show machines on your tailnet configured as exit nodes",
|
||||||
LongHelp: "Show machines on your tailnet configured as exit nodes",
|
LongHelp: "Show machines on your tailnet configured as exit nodes",
|
||||||
Subcommands: []*ffcli.Command{
|
Exec: func(context.Context, []string) error {
|
||||||
|
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
|
||||||
|
},
|
||||||
|
Subcommands: append([]*ffcli.Command{
|
||||||
{
|
{
|
||||||
Name: "list",
|
Name: "list",
|
||||||
ShortUsage: "exit-node list [flags]",
|
ShortUsage: "exit-node list [flags]",
|
||||||
@ -36,17 +40,51 @@
|
|||||||
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
|
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
|
||||||
return fs
|
return fs
|
||||||
})(),
|
})(),
|
||||||
|
}},
|
||||||
|
(func() []*ffcli.Command {
|
||||||
|
if !envknob.UseWIPCode() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []*ffcli.Command{
|
||||||
|
{
|
||||||
|
Name: "connect",
|
||||||
|
ShortUsage: "exit-node connect",
|
||||||
|
ShortHelp: "connect to most recently used exit node",
|
||||||
|
Exec: exitNodeSetUse(true),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "disconnect",
|
||||||
|
ShortUsage: "exit-node disconnect",
|
||||||
|
ShortHelp: "disconnect from current exit node, if any",
|
||||||
|
Exec: exitNodeSetUse(false),
|
||||||
},
|
},
|
||||||
Exec: func(context.Context, []string) error {
|
}
|
||||||
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
|
})()...),
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var exitNodeArgs struct {
|
var exitNodeArgs struct {
|
||||||
filter string
|
filter string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exitNodeSetUse(wantOn bool) func(ctx context.Context, args []string) error {
|
||||||
|
return func(ctx context.Context, args []string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return errors.New("unexpected non-flag arguments")
|
||||||
|
}
|
||||||
|
err := localClient.SetUseExitNode(ctx, wantOn)
|
||||||
|
if err != nil {
|
||||||
|
if !wantOn {
|
||||||
|
pref, err := localClient.GetPrefs(ctx)
|
||||||
|
if err == nil && pref.ExitNodeID == "" {
|
||||||
|
// Two processes concurrently turned it off.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runExitNodeList returns a formatted list of exit nodes for a tailnet.
|
// runExitNodeList returns a formatted list of exit nodes for a tailnet.
|
||||||
// If the exit node has location and priority data, only the highest
|
// If the exit node has location and priority data, only the highest
|
||||||
// priority node for each city location is shown to the user.
|
// priority node for each city location is shown to the user.
|
||||||
|
@ -42,6 +42,7 @@ func (src *Prefs) Clone() *Prefs {
|
|||||||
AllowSingleHosts bool
|
AllowSingleHosts bool
|
||||||
ExitNodeID tailcfg.StableNodeID
|
ExitNodeID tailcfg.StableNodeID
|
||||||
ExitNodeIP netip.Addr
|
ExitNodeIP netip.Addr
|
||||||
|
InternalExitNodePrior string
|
||||||
ExitNodeAllowLANAccess bool
|
ExitNodeAllowLANAccess bool
|
||||||
CorpDNS bool
|
CorpDNS bool
|
||||||
RunSSH bool
|
RunSSH bool
|
||||||
|
@ -69,6 +69,7 @@ func (v PrefsView) RouteAll() bool { return v.ж.RouteAll }
|
|||||||
func (v PrefsView) AllowSingleHosts() bool { return v.ж.AllowSingleHosts }
|
func (v PrefsView) AllowSingleHosts() bool { return v.ж.AllowSingleHosts }
|
||||||
func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID }
|
func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID }
|
||||||
func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP }
|
func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP }
|
||||||
|
func (v PrefsView) InternalExitNodePrior() string { return v.ж.InternalExitNodePrior }
|
||||||
func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess }
|
func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess }
|
||||||
func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS }
|
func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS }
|
||||||
func (v PrefsView) RunSSH() bool { return v.ж.RunSSH }
|
func (v PrefsView) RunSSH() bool { return v.ж.RunSSH }
|
||||||
@ -104,6 +105,7 @@ func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
|||||||
AllowSingleHosts bool
|
AllowSingleHosts bool
|
||||||
ExitNodeID tailcfg.StableNodeID
|
ExitNodeID tailcfg.StableNodeID
|
||||||
ExitNodeIP netip.Addr
|
ExitNodeIP netip.Addr
|
||||||
|
InternalExitNodePrior string
|
||||||
ExitNodeAllowLANAccess bool
|
ExitNodeAllowLANAccess bool
|
||||||
CorpDNS bool
|
CorpDNS bool
|
||||||
RunSSH bool
|
RunSSH bool
|
||||||
|
@ -3158,10 +3158,60 @@ func (b *LocalBackend) checkFunnelEnabledLocked(p *ipn.Prefs) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
// SetUseExitNodeEnabled turns on or off the most recently selected exit node.
|
||||||
|
//
|
||||||
|
// On success, it returns the resulting prefs (or current prefs, in the case of no change).
|
||||||
|
// Setting the value to false when use of an exit node is already false is not an error,
|
||||||
|
// nor is true when the exit node is already in use.
|
||||||
|
func (b *LocalBackend) SetUseExitNodeEnabled(v bool) (ipn.PrefsView, error) {
|
||||||
unlock := b.lockAndGetUnlock()
|
unlock := b.lockAndGetUnlock()
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
|
p0 := b.pm.CurrentPrefs()
|
||||||
|
if v && p0.ExitNodeID() != "" {
|
||||||
|
// Already on.
|
||||||
|
return p0, nil
|
||||||
|
}
|
||||||
|
if !v && p0.ExitNodeID() == "" {
|
||||||
|
// Already off.
|
||||||
|
return p0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var zero ipn.PrefsView
|
||||||
|
if v && p0.InternalExitNodePrior() == "" {
|
||||||
|
if !p0.ExitNodeIP().IsValid() {
|
||||||
|
return zero, errors.New("no exit node IP to enable & prior exit node IP was never resolved an a node")
|
||||||
|
}
|
||||||
|
return zero, errors.New("no prior exit node to enable")
|
||||||
|
}
|
||||||
|
|
||||||
|
mp := &ipn.MaskedPrefs{}
|
||||||
|
if v {
|
||||||
|
mp.ExitNodeIDSet = true
|
||||||
|
mp.ExitNodeID = tailcfg.StableNodeID(p0.InternalExitNodePrior())
|
||||||
|
} else {
|
||||||
|
mp.ExitNodeIDSet = true
|
||||||
|
mp.ExitNodeID = ""
|
||||||
|
mp.InternalExitNodePriorSet = true
|
||||||
|
mp.InternalExitNodePrior = string(p0.ExitNodeID())
|
||||||
|
}
|
||||||
|
return b.editPrefsLockedOnEntry(mp, unlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||||
|
if mp.SetsInternal() {
|
||||||
|
return ipn.PrefsView{}, errors.New("can't set Internal fields")
|
||||||
|
}
|
||||||
|
unlock := b.lockAndGetUnlock()
|
||||||
|
defer unlock()
|
||||||
|
return b.editPrefsLockedOnEntry(mp, unlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning: b.mu must be held on entry, but it unlocks it on the way out.
|
||||||
|
// TODO(bradfitz): redo the locking on all these weird methods like this.
|
||||||
|
func (b *LocalBackend) editPrefsLockedOnEntry(mp *ipn.MaskedPrefs, unlock unlockOnce) (ipn.PrefsView, error) {
|
||||||
|
defer unlock() // for error paths
|
||||||
|
|
||||||
if mp.EggSet {
|
if mp.EggSet {
|
||||||
mp.EggSet = false
|
mp.EggSet = false
|
||||||
b.egg = true
|
b.egg = true
|
||||||
@ -4651,18 +4701,16 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
|||||||
// Grab the current profile before we unlock the mutex, so that we can
|
// Grab the current profile before we unlock the mutex, so that we can
|
||||||
// delete it later.
|
// delete it later.
|
||||||
profile := b.pm.CurrentProfile()
|
profile := b.pm.CurrentProfile()
|
||||||
unlock.UnlockEarly()
|
|
||||||
|
|
||||||
// TODO(bradfitz): call/make editPrefsLocked here and stay locked until
|
_, err := b.editPrefsLockedOnEntry(&ipn.MaskedPrefs{
|
||||||
// before the cc.Logout.
|
|
||||||
_, err := b.EditPrefs(&ipn.MaskedPrefs{
|
|
||||||
WantRunningSet: true,
|
WantRunningSet: true,
|
||||||
LoggedOutSet: true,
|
LoggedOutSet: true,
|
||||||
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
|
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
|
||||||
})
|
}, unlock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// b.mu is now unlocked, after editPrefsLockedOnEntry.
|
||||||
|
|
||||||
// Clear any previous dial plan(s), if set.
|
// Clear any previous dial plan(s), if set.
|
||||||
b.dialPlan.Store(nil)
|
b.dialPlan.Store(nil)
|
||||||
|
@ -455,6 +455,61 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetUseExitNodeEnabled(t *testing.T) {
|
||||||
|
lb := newTestLocalBackend(t)
|
||||||
|
|
||||||
|
// Can't turn it on if it never had an old value.
|
||||||
|
if _, err := lb.SetUseExitNodeEnabled(true); err == nil {
|
||||||
|
t.Fatal("expected success")
|
||||||
|
}
|
||||||
|
|
||||||
|
// But we can turn it off when it's already off.
|
||||||
|
if _, err := lb.SetUseExitNodeEnabled(false); err != nil {
|
||||||
|
t.Fatal("expected failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give it an initial exit node in use.
|
||||||
|
if _, err := lb.EditPrefs(&ipn.MaskedPrefs{
|
||||||
|
ExitNodeIDSet: true,
|
||||||
|
Prefs: ipn.Prefs{
|
||||||
|
ExitNodeID: "foo",
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("enabling first exit node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now turn off that exit node.
|
||||||
|
if prefs, err := lb.SetUseExitNodeEnabled(false); err != nil {
|
||||||
|
t.Fatal("expected failure")
|
||||||
|
} else {
|
||||||
|
if g, w := prefs.ExitNodeID(), tailcfg.StableNodeID(""); g != w {
|
||||||
|
t.Fatalf("unexpected exit node ID %q; want %q", g, w)
|
||||||
|
}
|
||||||
|
if g, w := prefs.InternalExitNodePrior(), "foo"; g != w {
|
||||||
|
t.Fatalf("unexpected exit node prior %q; want %q", g, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And turn it back on.
|
||||||
|
if prefs, err := lb.SetUseExitNodeEnabled(true); err != nil {
|
||||||
|
t.Fatal("expected failure")
|
||||||
|
} else {
|
||||||
|
if g, w := prefs.ExitNodeID(), tailcfg.StableNodeID("foo"); g != w {
|
||||||
|
t.Fatalf("unexpected exit node ID %q; want %q", g, w)
|
||||||
|
}
|
||||||
|
if g, w := prefs.InternalExitNodePrior(), "foo"; g != w {
|
||||||
|
t.Fatalf("unexpected exit node prior %q; want %q", g, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we block setting an Internal field.
|
||||||
|
if _, err := lb.EditPrefs(&ipn.MaskedPrefs{
|
||||||
|
InternalExitNodePriorSet: true,
|
||||||
|
}); err == nil {
|
||||||
|
t.Fatalf("unexpected success; want an error trying to set an internal field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFileTargets(t *testing.T) {
|
func TestFileTargets(t *testing.T) {
|
||||||
b := new(LocalBackend)
|
b := new(LocalBackend)
|
||||||
_, err := b.FileTargets()
|
_, err := b.FileTargets()
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||||
"set-gui-visible": (*Handler).serveSetGUIVisible,
|
"set-gui-visible": (*Handler).serveSetGUIVisible,
|
||||||
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
|
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
|
||||||
|
"set-use-exit-node-enabled": (*Handler).serveSetUseExitNodeEnabled,
|
||||||
"start": (*Handler).serveStart,
|
"start": (*Handler).serveStart,
|
||||||
"status": (*Handler).serveStatus,
|
"status": (*Handler).serveStatus,
|
||||||
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
|
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
|
||||||
@ -2108,6 +2109,32 @@ type setGUIVisibleRequest struct {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveSetUseExitNodeEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := strconv.ParseBool(r.URL.Query().Get("enabled"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid 'enabled' parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefs, err := h.b.SetUseExitNodeEnabled(v)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
e := json.NewEncoder(w)
|
||||||
|
e.SetIndent("", "\t")
|
||||||
|
e.Encode(prefs)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.PermitWrite {
|
if !h.PermitWrite {
|
||||||
http.Error(w, "lock sign access denied", http.StatusForbidden)
|
http.Error(w, "lock sign access denied", http.StatusForbidden)
|
||||||
|
16
ipn/prefs.go
16
ipn/prefs.go
@ -105,6 +105,14 @@ type Prefs struct {
|
|||||||
ExitNodeID tailcfg.StableNodeID
|
ExitNodeID tailcfg.StableNodeID
|
||||||
ExitNodeIP netip.Addr
|
ExitNodeIP netip.Addr
|
||||||
|
|
||||||
|
// InternalExitNodePrior is the most recently used ExitNodeID in string form. It is set by
|
||||||
|
// the backend on transition from exit node on to off and used by the
|
||||||
|
// backend. It's not of type tailcfg.StableNodeID because in the future we plan
|
||||||
|
// to overload this field to mean things like "Anything in country $FOO" too.
|
||||||
|
//
|
||||||
|
// As an Internal field, it can't be set by LocalAPI clients.
|
||||||
|
InternalExitNodePrior string
|
||||||
|
|
||||||
// ExitNodeAllowLANAccess indicates whether locally accessible subnets should be
|
// ExitNodeAllowLANAccess indicates whether locally accessible subnets should be
|
||||||
// routed directly or via the exit node.
|
// routed directly or via the exit node.
|
||||||
ExitNodeAllowLANAccess bool
|
ExitNodeAllowLANAccess bool
|
||||||
@ -279,6 +287,7 @@ type MaskedPrefs struct {
|
|||||||
AllowSingleHostsSet bool `json:",omitempty"`
|
AllowSingleHostsSet bool `json:",omitempty"`
|
||||||
ExitNodeIDSet bool `json:",omitempty"`
|
ExitNodeIDSet bool `json:",omitempty"`
|
||||||
ExitNodeIPSet bool `json:",omitempty"`
|
ExitNodeIPSet bool `json:",omitempty"`
|
||||||
|
InternalExitNodePriorSet bool `json:",omitempty"` // Internal; can't be set by LocalAPI clients
|
||||||
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
||||||
CorpDNSSet bool `json:",omitempty"`
|
CorpDNSSet bool `json:",omitempty"`
|
||||||
RunSSHSet bool `json:",omitempty"`
|
RunSSHSet bool `json:",omitempty"`
|
||||||
@ -303,6 +312,12 @@ type MaskedPrefs struct {
|
|||||||
DriveSharesSet bool `json:",omitempty"`
|
DriveSharesSet bool `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetsInternal reports whether mp has any of the Internal*Set field bools set
|
||||||
|
// to true.
|
||||||
|
func (mp *MaskedPrefs) SetsInternal() bool {
|
||||||
|
return mp.InternalExitNodePriorSet
|
||||||
|
}
|
||||||
|
|
||||||
type AutoUpdatePrefsMask struct {
|
type AutoUpdatePrefsMask struct {
|
||||||
CheckSet bool `json:",omitempty"`
|
CheckSet bool `json:",omitempty"`
|
||||||
ApplySet bool `json:",omitempty"`
|
ApplySet bool `json:",omitempty"`
|
||||||
@ -544,6 +559,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
|||||||
p.AllowSingleHosts == p2.AllowSingleHosts &&
|
p.AllowSingleHosts == p2.AllowSingleHosts &&
|
||||||
p.ExitNodeID == p2.ExitNodeID &&
|
p.ExitNodeID == p2.ExitNodeID &&
|
||||||
p.ExitNodeIP == p2.ExitNodeIP &&
|
p.ExitNodeIP == p2.ExitNodeIP &&
|
||||||
|
p.InternalExitNodePrior == p2.InternalExitNodePrior &&
|
||||||
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
||||||
p.CorpDNS == p2.CorpDNS &&
|
p.CorpDNS == p2.CorpDNS &&
|
||||||
p.RunSSH == p2.RunSSH &&
|
p.RunSSH == p2.RunSSH &&
|
||||||
|
@ -41,6 +41,7 @@ func TestPrefsEqual(t *testing.T) {
|
|||||||
"AllowSingleHosts",
|
"AllowSingleHosts",
|
||||||
"ExitNodeID",
|
"ExitNodeID",
|
||||||
"ExitNodeIP",
|
"ExitNodeIP",
|
||||||
|
"InternalExitNodePrior",
|
||||||
"ExitNodeAllowLANAccess",
|
"ExitNodeAllowLANAccess",
|
||||||
"CorpDNS",
|
"CorpDNS",
|
||||||
"RunSSH",
|
"RunSSH",
|
||||||
@ -614,6 +615,19 @@ func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
|||||||
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMaskedPrefsSetsInternal(t *testing.T) {
|
||||||
|
for _, f := range fieldsOf(reflect.TypeFor[MaskedPrefs]()) {
|
||||||
|
if !strings.HasSuffix(f, "Set") || !strings.HasPrefix(f, "Internal") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mp := new(MaskedPrefs)
|
||||||
|
reflect.ValueOf(mp).Elem().FieldByName(f).SetBool(true)
|
||||||
|
if !mp.SetsInternal() {
|
||||||
|
t.Errorf("MaskedPrefs.%sSet=true but SetsInternal=false", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMaskedPrefsFields(t *testing.T) {
|
func TestMaskedPrefsFields(t *testing.T) {
|
||||||
have := map[string]bool{}
|
have := map[string]bool{}
|
||||||
for _, f := range fieldsOf(reflect.TypeFor[Prefs]()) {
|
for _, f := range fieldsOf(reflect.TypeFor[Prefs]()) {
|
||||||
|
Loading…
Reference in New Issue
Block a user