Skip to content

Fix daemon races when tunnels are opened and closed concurrently - #49

Merged
alebeck merged 1 commit into
alebeck:mainfrom
dkoryto:fix/daemon-concurrency
Oct 4, 2026
Merged

alebeck merged 1 commit into
alebeck:mainfrom
dkoryto:fix/daemon-concurrency

Conversation

@dkoryto

@dkoryto dkoryto commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes races in the daemon that show up when the same tunnel is opened or closed by several clients at once, plus data races in the tunnel code flagged by -race.

  • Concurrent close crashed the daemon. Close only checked Status, which stays Open until the run loop has shut the tunnel down. Two clients closing the same tunnel could both close the stop channel, panicking with close of closed channel and taking down every tunnel. The stop channel is now closed through a sync.Once.
  • Concurrent opens failed with address already in use. Every client got past the "already running" check. Concurrent opens of a tunnel now share a single attempt through singleflight: the other clients report the tunnel as already running, or get the same error if the attempt failed.
  • Unsynchronized Status and LastConn. They were written by the tunnel goroutines and read by the daemon for list. They are now guarded by a mutex in Tunnel, and the daemon lists tunnels through Snapshot.
  • WaitGroup misuse. Add was called inside the new goroutine, racing with Wait. goWait (formerly waitFor) now calls Add before starting the goroutine.
  • Tunnels are removed before close responds. closeTunnel now removes the tunnel itself instead of leaving it to the closing goroutine, so a reopen right after close can never find the old entry and be rejected as already running. Since a tunnel is now removed in two places, removal checks that it is still the same tunnel, so it never drops one that was reopened under the same name.

Testing

  • New e2e tests open and close the same tunnel from several clients at once. On main they reproduce the daemon panic and the address already in use failures.
  • Unit tests for Tunnel.Close, Snapshot and tunnel removal.
  • With -race, the daemon logs 24 data races during the e2e suite on main and none with this change. make test passes.

Changes during review

Rebased onto main (after #48) and squashed. The name reservation in the daemon was replaced with singleflight from golang.org/x/sync (already a dependency), so waiting clients share a failed attempt instead of retrying one after another. listStatus is now shared with waitForStatus, and single CLI runs in the e2e tests use a new mustRun helper.

@alebeck
alebeck force-pushed the fix/daemon-concurrency branch from 38d12db to c5f79c0 Compare October 4, 2026 15:54
@alebeck alebeck changed the title Fix/daemon concurrency Fix daemon races when tunnels are opened and closed concurrently Oct 4, 2026
@alebeck
alebeck force-pushed the fix/daemon-concurrency branch from c5f79c0 to 3bef6c3 Compare October 4, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants