From df3c9fa6688c46d9e884da57ca5a7dc4b497f360 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Tue, 28 Jul 2026 19:22:20 -0700 Subject: [PATCH] fix(healthcheck): prevent race condition on the healthchecker (#3400) Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Quentin McGaw Thanks also to @arturhoo for also trying to resolve this issue in #3410 --- internal/healthcheck/checker.go | 6 ------ internal/vpn/loop.go | 7 +++++++ internal/vpn/tunnelup.go | 13 +++++++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/healthcheck/checker.go b/internal/healthcheck/checker.go index 5d36f58d..4b1059a7 100644 --- a/internal/healthcheck/checker.go +++ b/internal/healthcheck/checker.go @@ -8,7 +8,6 @@ import ( "net" "net/netip" "strings" - "sync" "time" "github.com/qdm12/gluetun/internal/healthcheck/dns" @@ -24,7 +23,6 @@ type Checker struct { icmpTargetIPs []netip.Addr smallCheckType string startupOnFail bool - configMutex sync.Mutex icmpNotPermitted *bool @@ -55,8 +53,6 @@ func NewChecker(logger Logger) *Checker { func (c *Checker) SetConfig(tlsDialAddrs []string, icmpTargets []netip.Addr, smallCheckType string, startupOnFail bool, ) { - c.configMutex.Lock() - defer c.configMutex.Unlock() c.tlsDialAddrs = tlsDialAddrs c.icmpTargetIPs = icmpTargets c.smallCheckType = smallCheckType @@ -166,10 +162,8 @@ func (c *Checker) Stop() error { } func (c *Checker) smallPeriodicCheck(ctx context.Context) error { - c.configMutex.Lock() icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs)) copy(icmpTargetIPs, c.icmpTargetIPs) - c.configMutex.Unlock() tryTimeouts := []time.Duration{ 5 * time.Second, 5 * time.Second, diff --git a/internal/vpn/loop.go b/internal/vpn/loop.go index 77cf92ba..42c77401 100644 --- a/internal/vpn/loop.go +++ b/internal/vpn/loop.go @@ -45,6 +45,7 @@ type Loop struct { start <-chan struct{} running chan<- models.LoopStatus userTrigger bool + healthDone <-chan struct{} // Internal constant values backoffTime time.Duration } @@ -70,6 +71,11 @@ func NewLoop(vpnSettings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped) state := state.New(statusManager, vpnSettings) + // Initialize healthDone channel to a closed channel so that the first time + // we call <-l.healthDone it does not block + healthDone := make(chan struct{}) + close(healthDone) + return &Loop{ statusManager: statusManager, state: state, @@ -98,6 +104,7 @@ func NewLoop(vpnSettings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel stop: stop, stopped: stopped, userTrigger: true, + healthDone: healthDone, backoffTime: defaultBackoffTime, } } diff --git a/internal/vpn/tunnelup.go b/internal/vpn/tunnelup.go index 4843812a..3a842c51 100644 --- a/internal/vpn/tunnelup.go +++ b/internal/vpn/tunnelup.go @@ -81,6 +81,7 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) { _, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running) + <-l.healthDone // make sure the health checker is stopped before restarting it icmpTargetIPs := l.healthSettings.ICMPTargetIPs if len(icmpTargetIPs) == 1 && icmpTargetIPs[0].IsUnspecified() { icmpTargetIPs = []netip.Addr{data.serverIP} @@ -104,7 +105,12 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) { // Start collecting health errors asynchronously, since // we should not wait for the code below to complete // to start monitoring health and auto-healing. - go l.collectHealthErrors(ctx, loopCtx, healthErrCh) + // We keep track of when this goroutine is done with the healthDone + // channel to avoid a race condition where the health checker is stopped + // after being reconfigured and restarted, instead of before. + healthDone := make(chan struct{}) + l.healthDone = healthDone + go l.collectHealthErrors(ctx, loopCtx, healthDone, healthErrCh) err = l.publicip.RunOnce(ctx) if err != nil { @@ -140,7 +146,10 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) { } } -func (l *Loop) collectHealthErrors(ctx, loopCtx context.Context, healthErrCh <-chan error) { +func (l *Loop) collectHealthErrors(ctx, loopCtx context.Context, + done chan<- struct{}, healthErrCh <-chan error, +) { + defer close(done) var previousHealthErr error for { select {