mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-19 01:25:21 -04:00
feat(tailscale): add connect/disconnect, exit-node and LAN-access controls (#2644)
* feat(tailscale): add connect/disconnect/exit-node/LAN-access backend The Tailscale backend previously exposed only read-only status (tailscale.getStatus, tailscale.refresh). This adds write actions through the existing tailscale.com/client/local integration: - tailscale.connect / tailscale.disconnect (EditPrefs WantRunning) - tailscale.setExitNode (EditPrefs ExitNodeID; empty id clears it and any legacy ExitNodeIP, mirroring `tailscale set --exit-node`) - tailscale.setAllowLanAccess (EditPrefs ExitNodeAllowLANAccess) The manager's client interface gains GetPrefs/EditPrefs; fetchState merges ExitNodeAllowLANAccess from prefs, and Peer exposes ExitNodeOption so the UI can list exit-node-capable peers. * feat(tailscale): expose the new actions in TailscaleService Adds connectTailscale/disconnectTailscale, setExitNode/clearExitNode and setAllowLanAccess wrappers, plus derived exitNodeOptions/currentExitNode and the exitNodeAllowLanAccess state. Write-action errors surface via ToastService. * feat(tailscale): add connection, exit-node and LAN-access controls to the widget The control-center widget toggle was a no-op. It now connects/disconnects, and the detail panel gains a connection status row with a connect/disconnect button, an exit-node picker and a LAN-access toggle.
This commit is contained in:
@@ -12,8 +12,16 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// blockingWatch is a watchFn that blocks until the context is cancelled, used
|
||||
// by tests that exercise direct manager calls rather than the watch loop.
|
||||
func blockingWatch(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// mockWatcher yields canned Notify events, then returns err or blocks until Close/context cancel.
|
||||
type mockWatcher struct {
|
||||
events []ipn.Notify
|
||||
@@ -68,8 +76,10 @@ func (w *mockWatcher) Close() error {
|
||||
|
||||
// mockClient implements tailscaleClient for testing.
|
||||
type mockClient struct {
|
||||
watchFn func(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error)
|
||||
statusFn func(ctx context.Context) (*ipnstate.Status, error)
|
||||
watchFn func(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error)
|
||||
statusFn func(ctx context.Context) (*ipnstate.Status, error)
|
||||
getPrefsFn func(ctx context.Context) (*ipn.Prefs, error)
|
||||
editPrefsFn func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
|
||||
}
|
||||
|
||||
func (c *mockClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) {
|
||||
@@ -80,6 +90,20 @@ func (c *mockClient) Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return c.statusFn(ctx)
|
||||
}
|
||||
|
||||
func (c *mockClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
||||
if c.getPrefsFn != nil {
|
||||
return c.getPrefsFn(ctx)
|
||||
}
|
||||
return &ipn.Prefs{}, nil
|
||||
}
|
||||
|
||||
func (c *mockClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
if c.editPrefsFn != nil {
|
||||
return c.editPrefsFn(ctx, mp)
|
||||
}
|
||||
return &ipn.Prefs{}, nil
|
||||
}
|
||||
|
||||
func runningStatus() *ipnstate.Status {
|
||||
return &ipnstate.Status{
|
||||
Version: "1.94.2",
|
||||
@@ -296,3 +320,78 @@ func TestManager_RefreshState(t *testing.T) {
|
||||
assert.True(t, state.Connected)
|
||||
assert.Equal(t, "cachyos", state.Self.Hostname)
|
||||
}
|
||||
|
||||
func TestManager_RefreshState_MergesPrefs(t *testing.T) {
|
||||
client := &mockClient{
|
||||
watchFn: blockingWatch,
|
||||
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
|
||||
getPrefsFn: func(ctx context.Context) (*ipn.Prefs, error) {
|
||||
return &ipn.Prefs{ExitNodeAllowLANAccess: true}, nil
|
||||
},
|
||||
}
|
||||
|
||||
m := newManager(client)
|
||||
defer m.Close()
|
||||
|
||||
m.RefreshState()
|
||||
|
||||
assert.True(t, m.GetState().ExitNodeAllowLANAccess)
|
||||
}
|
||||
|
||||
func TestManager_Actions_EditPrefs(t *testing.T) {
|
||||
var captured *ipn.MaskedPrefs
|
||||
client := &mockClient{
|
||||
watchFn: blockingWatch,
|
||||
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
|
||||
editPrefsFn: func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
captured = mp
|
||||
return &ipn.Prefs{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
m := newManager(client)
|
||||
defer m.Close()
|
||||
|
||||
require.NoError(t, m.Connect())
|
||||
require.NotNil(t, captured)
|
||||
assert.True(t, captured.WantRunningSet)
|
||||
assert.True(t, captured.WantRunning)
|
||||
|
||||
require.NoError(t, m.Disconnect())
|
||||
assert.True(t, captured.WantRunningSet)
|
||||
assert.False(t, captured.WantRunning)
|
||||
|
||||
require.NoError(t, m.SetExitNode("nABC123"))
|
||||
assert.True(t, captured.ExitNodeIDSet)
|
||||
assert.Equal(t, tailcfg.StableNodeID("nABC123"), captured.ExitNodeID)
|
||||
// ExitNodeIPSet must also be set so a stale legacy ExitNodeIP cannot
|
||||
// override the ID-based selection (mirrors `tailscale set --exit-node`).
|
||||
assert.True(t, captured.ExitNodeIPSet)
|
||||
|
||||
require.NoError(t, m.SetExitNode(""))
|
||||
assert.True(t, captured.ExitNodeIDSet)
|
||||
assert.Equal(t, tailcfg.StableNodeID(""), captured.ExitNodeID)
|
||||
// Clearing must zero both the ID and any legacy IP-based exit node.
|
||||
assert.True(t, captured.ExitNodeIPSet)
|
||||
|
||||
require.NoError(t, m.SetAllowLANAccess(true))
|
||||
assert.True(t, captured.ExitNodeAllowLANAccessSet)
|
||||
assert.True(t, captured.ExitNodeAllowLANAccess)
|
||||
}
|
||||
|
||||
func TestManager_Actions_PropagateError(t *testing.T) {
|
||||
client := &mockClient{
|
||||
watchFn: blockingWatch,
|
||||
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
|
||||
editPrefsFn: func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
return nil, fmt.Errorf("backend rejected edit")
|
||||
},
|
||||
}
|
||||
|
||||
m := newManager(client)
|
||||
defer m.Close()
|
||||
|
||||
assert.Error(t, m.Connect())
|
||||
assert.Error(t, m.SetExitNode("nABC123"))
|
||||
assert.Error(t, m.SetAllowLANAccess(true))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user