mirror of
https://github.com/coder/coder.git
synced 2026-09-01 14:53:15 +08:00
fix: add VPN wake rebind hook (#26739)
Add a CoderVPN WakeRequest RPC so Coder Desktop can trigger the existing link-change recovery path (Rebind + ReSTUN) immediately on OS wake, instead of waiting for magicsock's idle re-STUN timer. Wake events are debounced to at most one rebind per 5s to avoid duplicate resets of peer path trust. Closes #26736
This commit is contained in:
@@ -495,6 +495,12 @@ func (c *Conn) MagicsockSetDebugLoggingEnabled(enabled bool) {
|
||||
c.magicConn.SetDebugLoggingEnabled(enabled)
|
||||
}
|
||||
|
||||
// Rebind resets local network bindings and rediscovers peer paths.
|
||||
func (c *Conn) Rebind() {
|
||||
c.magicConn.Rebind()
|
||||
c.magicConn.ReSTUN("wake")
|
||||
}
|
||||
|
||||
func (c *Conn) SetAddresses(ips []netip.Prefix) error {
|
||||
c.configMaps.setAddresses(ips)
|
||||
c.nodeUpdater.setAddresses(ips)
|
||||
|
||||
@@ -32,6 +32,7 @@ type Conn interface {
|
||||
Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error)
|
||||
Node() *tailnet.Node
|
||||
DERPMap() *tailcfg.DERPMap
|
||||
Rebind()
|
||||
Close() error
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
|
||||
}
|
||||
|
||||
const expectedHandshake = "codervpn tunnel 1.2\n"
|
||||
const expectedHandshake = "codervpn tunnel 1.3\n"
|
||||
|
||||
// TestSpeaker_RawPeer tests the speaker with a peer that we simulate by directly making reads and
|
||||
// writes to the other end of the pipe. There should be at least one test that does this, rather
|
||||
|
||||
@@ -164,6 +164,15 @@ func (t *Tunnel) handleRPC(req *request[*TunnelMessage, *ManagerMessage]) {
|
||||
ErrorMessage: errStr,
|
||||
},
|
||||
}
|
||||
case *ManagerMessage_Wake:
|
||||
if t.updater.rebind() {
|
||||
t.logger.Info(t.ctx, "handling system wake; rebinding")
|
||||
} else {
|
||||
t.logger.Debug(t.ctx, "ignoring system wake; tunnel is not running or a rebind happened recently")
|
||||
}
|
||||
resp.Msg = &TunnelMessage_Wake{
|
||||
Wake: &WakeResponse{Success: true},
|
||||
}
|
||||
default:
|
||||
t.logger.Warn(t.ctx, "unhandled manager request", slog.F("request", msg))
|
||||
}
|
||||
@@ -338,6 +347,8 @@ type updater struct {
|
||||
// workspaces contains the workspaces to which agents are currently connected via the tunnel.
|
||||
workspaces map[uuid.UUID]tailnet.Workspace
|
||||
conn Conn
|
||||
// lastRebind debounces wake-triggered rebinds.
|
||||
lastRebind time.Time
|
||||
|
||||
clock quartz.Clock
|
||||
}
|
||||
@@ -551,6 +562,33 @@ func (u *updater) stop() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// wakeRebindDebounce is the minimum interval between wake-triggered rebinds.
|
||||
// Rebinding resets peer path trust, so duplicate wake events must not each
|
||||
// trigger one.
|
||||
const wakeRebindDebounce = 5 * time.Second
|
||||
|
||||
// rebind resets network bindings and rediscovers peer paths. It reports
|
||||
// whether it ran; calls within wakeRebindDebounce of the previous rebind, or
|
||||
// while the tunnel is not running, are dropped.
|
||||
func (u *updater) rebind() bool {
|
||||
u.mu.Lock()
|
||||
conn := u.conn
|
||||
if conn == nil {
|
||||
u.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
now := u.clock.Now()
|
||||
if now.Sub(u.lastRebind) < wakeRebindDebounce {
|
||||
u.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
u.lastRebind = now
|
||||
u.mu.Unlock()
|
||||
|
||||
conn.Rebind()
|
||||
return true
|
||||
}
|
||||
|
||||
// sendAgentUpdate sends a peer update message to the manager with the current
|
||||
// state of the agents, including the latest network status.
|
||||
func (u *updater) sendAgentUpdate() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -53,9 +54,10 @@ func (f *fakeClient) NewConn(context.Context, *url.URL, string, *Options) (Conn,
|
||||
|
||||
func newFakeConn(state tailnet.WorkspaceUpdate, hsTime time.Time) *fakeConn {
|
||||
return &fakeConn{
|
||||
closed: make(chan struct{}),
|
||||
state: state,
|
||||
hsTime: hsTime,
|
||||
closed: make(chan struct{}),
|
||||
rebinds: make(chan struct{}, 1),
|
||||
state: state,
|
||||
hsTime: hsTime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +71,7 @@ type fakeConn struct {
|
||||
returnPing chan struct{}
|
||||
hsTime time.Time
|
||||
closed chan struct{}
|
||||
rebinds chan struct{}
|
||||
doClose sync.Once
|
||||
}
|
||||
|
||||
@@ -122,6 +125,10 @@ func (f *fakeConn) GetPeerDiagnostics(uuid.UUID) tailnet.PeerDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeConn) Rebind() {
|
||||
f.rebinds <- struct{}{}
|
||||
}
|
||||
|
||||
func (f *fakeConn) Close() error {
|
||||
f.doClose.Do(func() {
|
||||
close(f.closed)
|
||||
@@ -188,6 +195,81 @@ func TestTunnel_StartStop(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestTunnel_Wake(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
client := newFakeClient(ctx, t)
|
||||
conn := newFakeConn(tailnet.WorkspaceUpdate{}, time.Time{})
|
||||
|
||||
mClock := quartz.NewMock(t)
|
||||
_, mgr := setupTunnel(t, ctx, client, mClock)
|
||||
|
||||
sendWake := func() *TunnelMessage {
|
||||
wakeCh := make(chan *TunnelMessage, 1)
|
||||
go func() {
|
||||
defer close(wakeCh)
|
||||
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
|
||||
Msg: &ManagerMessage_Wake{
|
||||
Wake: &WakeRequest{},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
wakeCh <- r
|
||||
}()
|
||||
return testutil.TryReceive(ctx, t, wakeCh)
|
||||
}
|
||||
requireNoRebind := func() {
|
||||
// Rebinds happen before the RPC reply, so any rebind would
|
||||
// already be buffered by now.
|
||||
select {
|
||||
case <-conn.rebinds:
|
||||
t.Fatal("unexpected rebind")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Waking before the tunnel is started must not rebind.
|
||||
wakeResp := sendWake()
|
||||
require.True(t, wakeResp.GetWake().GetSuccess())
|
||||
requireNoRebind()
|
||||
|
||||
startCh := make(chan *TunnelMessage, 1)
|
||||
go func() {
|
||||
defer close(startCh)
|
||||
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
|
||||
Msg: &ManagerMessage_Start{
|
||||
Start: &StartRequest{
|
||||
TunnelFileDescriptor: 2,
|
||||
CoderUrl: "https://coder.example.com",
|
||||
ApiToken: "fakeToken",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
startCh <- r
|
||||
}()
|
||||
testutil.RequireSend(ctx, t, client.ch, conn)
|
||||
startResp := testutil.TryReceive(ctx, t, startCh)
|
||||
require.NotNil(t, startResp.GetStart())
|
||||
|
||||
// The first wake after start triggers a rebind.
|
||||
wakeResp = sendWake()
|
||||
require.True(t, wakeResp.GetWake().GetSuccess())
|
||||
testutil.TryReceive(ctx, t, conn.rebinds)
|
||||
|
||||
// A wake within the debounce window is dropped but still succeeds.
|
||||
wakeResp = sendWake()
|
||||
require.True(t, wakeResp.GetWake().GetSuccess())
|
||||
requireNoRebind()
|
||||
|
||||
// After the debounce window elapses, a wake rebinds again.
|
||||
mClock.Advance(wakeRebindDebounce).MustWait(ctx)
|
||||
wakeResp = sendWake()
|
||||
require.True(t, wakeResp.GetWake().GetSuccess())
|
||||
testutil.TryReceive(ctx, t, conn.rebinds)
|
||||
}
|
||||
|
||||
func TestTunnel_PeerUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ var CurrentSupportedVersions = RPCVersionList{
|
||||
// - preferred_derp: The server that DERP relayed connections are
|
||||
// using, if they're not using P2P.
|
||||
// - preferred_derp_latency: The latency to the preferred DERP
|
||||
{Major: 1, Minor: 2},
|
||||
// 1.3 adds WakeRequest and WakeResponse.
|
||||
{Major: 1, Minor: 3},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+516
-363
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ message ManagerMessage {
|
||||
NetworkSettingsResponse network_settings = 3;
|
||||
StartRequest start = 4;
|
||||
StopRequest stop = 5;
|
||||
WakeRequest wake = 6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ message TunnelMessage {
|
||||
NetworkSettingsRequest network_settings = 4;
|
||||
StartResponse start = 5;
|
||||
StopResponse stop = 6;
|
||||
WakeResponse wake = 7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,3 +292,11 @@ message Status {
|
||||
// be populated.
|
||||
PeerUpdate peer_update = 3;
|
||||
}
|
||||
|
||||
// WakeRequest is sent by the manager after the system wakes from sleep. The
|
||||
// tunnel uses this as a hint to rediscover network paths.
|
||||
message WakeRequest {}
|
||||
|
||||
message WakeResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user