[PR #6664] [client] Fix WGWatcher silently failing to restart on fast disconnect/reconnect #29788

Closed
opened 2026-08-05 08:08:58 -04:00 by saavagebueno · 0 comments
Owner

Original Pull Request: https://github.com/netbirdio/netbird/pull/6664

State: closed
Merged: Yes


Describe your changes

WGWatcher monitors WireGuard handshake liveness and tears down a dead transport so the guard reconnects. Fixes a race where the watcher could silently stop running, plus a related teardown race.

  1. Per-instance watcher (main fix). The watcher was a reused instance guarded by an enabled bool; disableWgWatcherIfNeeded only cancel()ed the goroutine, which cleared enabled after its loop returned (scheduler-dependent delay). On a fast disconnect + independent relay/ICE "ready", the re-enable saw a stale enabled == true and started nothing → a "connected" peer with no liveness watcher, never retried. Fix: create a fresh watcher per connection attempt, dropped on disable; lifecycle fully owned by Conn under conn.mu, so enable/disable can't race the old goroutine's async shutdown. Removed enabled/muEnabled.

  2. Recheck watcher ctx under conn.mu in onWGDisconnected. handshakeCheck runs lock-free; between its ctx check and acquiring conn.mu, a fast reconnect can slip in. onWGDisconnected checked only conn.ctx (engine-scoped), but disable cancels wgWatcherCtx — so a stale watcher could tear down the new connection on the old timeout (flap). Now it also rechecks watcherCtx.Err() under the lock.

  3. Log clarity. Distinguish "handshake not updated" from an actual timeout.

Deadlock constraint preserved: disableWgWatcherIfNeeded never waits for the goroutine (the timeout path reentrantly calls back under conn.mu); the cancelled goroutine drains via its own ctx.Done().

Verified: go test -run TestWGWatcher ./client/internal/peer/ passes, go build ./client/... clean.

No public GitHub issue — internal client-reliability fix for a scheduler-dependent race in the WireGuard liveness watcher. Pre-fix paths on main: disableWgWatcherIfNeeded, onWGDisconnected.

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)
  • This change does not modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — OR I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See CONTRIBUTING.md.

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Internal concurrency fix in the client's WireGuard liveness watcher. No public CLI/API/gRPC/config surface changes; observable behavior is unchanged.

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

N/A

Summary by CodeRabbit

  • Bug Fixes
    • Improved WireGuard handshake watcher lifecycle during reconnects to prevent stale timeout callbacks from tearing down newer connections.
    • Added per-attempt cancellation checks after timeout triggers to avoid disrupting active handshakes.
    • Refined watcher behavior to be single-shot, with safer enable/disable handling and endpoint resets only when applicable.
    • Updated warning wording when the handshake time hasn’t advanced (“not updated”).
  • Tests
    • Updated watcher and connection tests for the revised handshake preparation behavior and callback signature.
    • Removed require-based assertions in watcher tests.
**Original Pull Request:** https://github.com/netbirdio/netbird/pull/6664 **State:** closed **Merged:** Yes --- ## Describe your changes `WGWatcher` monitors WireGuard handshake liveness and tears down a dead transport so the guard reconnects. Fixes a race where the watcher could silently stop running, plus a related teardown race. 1. **Per-instance watcher (main fix).** The watcher was a reused instance guarded by an `enabled` bool; `disableWgWatcherIfNeeded` only `cancel()`ed the goroutine, which cleared `enabled` *after* its loop returned (scheduler-dependent delay). On a fast disconnect + independent relay/ICE "ready", the re-enable saw a stale `enabled == true` and started nothing → a "connected" peer with no liveness watcher, never retried. Fix: create a fresh watcher per connection attempt, dropped on disable; lifecycle fully owned by `Conn` under `conn.mu`, so enable/disable can't race the old goroutine's async shutdown. Removed `enabled`/`muEnabled`. 2. **Recheck watcher ctx under `conn.mu` in `onWGDisconnected`.** `handshakeCheck` runs lock-free; between its ctx check and acquiring `conn.mu`, a fast reconnect can slip in. `onWGDisconnected` checked only `conn.ctx` (engine-scoped), but disable cancels `wgWatcherCtx` — so a stale watcher could tear down the **new** connection on the old timeout (flap). Now it also rechecks `watcherCtx.Err()` under the lock. 3. **Log clarity.** Distinguish "handshake not updated" from an actual timeout. Deadlock constraint preserved: `disableWgWatcherIfNeeded` never waits for the goroutine (the timeout path reentrantly calls back under `conn.mu`); the cancelled goroutine drains via its own `ctx.Done()`. Verified: `go test -run TestWGWatcher ./client/internal/peer/` passes, `go build ./client/...` clean. ## Issue ticket number and link No public GitHub issue — internal client-reliability fix for a scheduler-dependent race in the WireGuard liveness watcher. Pre-fix paths on `main`: [`disableWgWatcherIfNeeded`](https://github.com/netbirdio/netbird/blob/f6900fb07c68cfcececc48cdc0de1e42bf97b5e2/client/internal/peer/conn.go#L819), [`onWGDisconnected`](https://github.com/netbirdio/netbird/blob/f6900fb07c68cfcececc48cdc0de1e42bf97b5e2/client/internal/peer/conn.go#L670). ## Stack <!-- branch-stack --> ### Checklist - [x] Is it a bug fix - [ ] Is a typo/documentation fix - [ ] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) - [x] This change does **not** modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — **OR** I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See [CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first). > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [ ] I added/updated documentation for this change - [x] Documentation is **not needed** for this change (explain why) Internal concurrency fix in the client's WireGuard liveness watcher. No public CLI/API/gRPC/config surface changes; observable behavior is unchanged. ### Docs PR URL (required if "docs added" is checked) Paste the PR link from https://github.com/netbirdio/docs here: N/A <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved WireGuard handshake watcher lifecycle during reconnects to prevent stale timeout callbacks from tearing down newer connections. * Added per-attempt cancellation checks after timeout triggers to avoid disrupting active handshakes. * Refined watcher behavior to be single-shot, with safer enable/disable handling and endpoint resets only when applicable. * Updated warning wording when the handshake time hasn’t advanced (“not updated”). * **Tests** * Updated watcher and connection tests for the revised handshake preparation behavior and callback signature. * Removed `require`-based assertions in watcher tests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
saavagebueno added the pull-request label 2026-08-05 08:08:58 -04:00
Sign in to join this conversation.
No Label pull-request
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#29788