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:
Brad Fitzpatrick
2024-04-03 10:51:51 -07:00
committed by Brad Fitzpatrick
parent 3f4c5daa15
commit a5e1f7d703
10 changed files with 226 additions and 12 deletions

View File

@@ -3158,10 +3158,60 @@ func (b *LocalBackend) checkFunnelEnabledLocked(p *ipn.Prefs) error {
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()
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 {
mp.EggSet = false
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
// delete it later.
profile := b.pm.CurrentProfile()
unlock.UnlockEarly()
// TODO(bradfitz): call/make editPrefsLocked here and stay locked until
// before the cc.Logout.
_, err := b.EditPrefs(&ipn.MaskedPrefs{
_, err := b.editPrefsLockedOnEntry(&ipn.MaskedPrefs{
WantRunningSet: true,
LoggedOutSet: true,
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
})
}, unlock)
if err != nil {
return err
}
// b.mu is now unlocked, after editPrefsLockedOnEntry.
// Clear any previous dial plan(s), if set.
b.dialPlan.Store(nil)

View File

@@ -455,6 +455,61 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
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) {
b := new(LocalBackend)
_, err := b.FileTargets()