[GH-ISSUE #5887] [client] Refactor conntrack done channel to per-run cancellation #12239

Open
opened 2026-08-05 01:32:43 -04:00 by saavagebueno · 1 comment
Owner

Originally created by @coderabbitai[bot] on GitHub (Apr 14, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/5887

Originally assigned to: @lixmal on GitHub.

Summary

The shared buffered channel c.done in client/internal/netflow/conntrack/conntrack.go is reused across Start/Stop cycles. A fast StopStart sequence could consume the stop token before the old receiverRoutine/reconnect goroutine does, allowing the old loop to miss cancellation, observe started == true, and attach a second netlink listener — leaking a connection and producing duplicate flow events.

In practice this is mitigated by the flow manager serializing calls via Update(), making the race theoretical rather than observed. However, the correct long-term fix is per-run cancellation.

Proposed fix

  • Allocate a fresh context.Context (or a new channel) in Start() for each run.
  • Pass that per-run cancel token into receiverRoutine(), reconnect(), and any subroutines instead of the shared c.done.
  • Update Stop() to cancel only the current per-run token.
  • Remove the stale-drain select block in Start() (no longer needed with per-run contexts).
  • Ensure c.conn ownership remains scoped to the active run.

References

/cc @lixmal

Originally created by @coderabbitai[bot] on GitHub (Apr 14, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/5887 Originally assigned to: @lixmal on GitHub. ## Summary The shared buffered channel `c.done` in `client/internal/netflow/conntrack/conntrack.go` is reused across `Start`/`Stop` cycles. A fast `Stop`→`Start` sequence could consume the stop token before the old `receiverRoutine`/`reconnect` goroutine does, allowing the old loop to miss cancellation, observe `started == true`, and attach a second netlink listener — leaking a connection and producing duplicate flow events. In practice this is mitigated by the flow manager serializing calls via `Update()`, making the race theoretical rather than observed. However, the correct long-term fix is per-run cancellation. ## Proposed fix - Allocate a fresh `context.Context` (or a new channel) in `Start()` for each run. - Pass that per-run cancel token into `receiverRoutine()`, `reconnect()`, and any subroutines instead of the shared `c.done`. - Update `Stop()` to cancel only the current per-run token. - Remove the stale-drain `select` block in `Start()` (no longer needed with per-run contexts). - Ensure `c.conn` ownership remains scoped to the active run. ## References - PR introducing the reconnect fix: https://github.com/netbirdio/netbird/pull/5885 - Review comment: https://github.com/netbirdio/netbird/pull/5885#discussion_r3080660731 /cc @lixmal
Author
Owner

@CoderSufiyan commented on GitHub (Jul 16, 2026):

@lixmal I noticed this issue was assigned to you after I had already completed an implementation. I opened PR #6807 with per-run cancellation and listener ownership, including a regression test for a stale reconnect completing after a replacement run starts. Please let me know if you would like to use this contribution or prefer that I close it to avoid overlapping with planned work.

<!-- gh-comment-id:4994289471 --> @CoderSufiyan commented on GitHub (Jul 16, 2026): @lixmal I noticed this issue was assigned to you after I had already completed an implementation. I opened PR #6807 with per-run cancellation and listener ownership, including a regression test for a stale reconnect completing after a replacement run starts. Please let me know if you would like to use this contribution or prefer that I close it to avoid overlapping with planned work.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#12239