package healthcheck import ( "context" "errors" "fmt" "net" "time" ) func (s *Server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) { defer close(done) timeoutIndex := 0 healthcheckTimeouts := []time.Duration{ 2 * time.Second, 4 * time.Second, 6 * time.Second, 8 * time.Second, // This can be useful when the connection is under stress // See https://github.com/qdm12/gluetun/issues/2270 10 * time.Second, } s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait) for { previousErr := s.handler.getErr() timeout := healthcheckTimeouts[timeoutIndex] healthcheckCtx, healthcheckCancel := context.WithTimeout( ctx, timeout) err := s.healthCheck(healthcheckCtx) healthcheckCancel() s.handler.setErr(err) switch { case previousErr != nil && err == nil: // First success s.logger.Info("healthy!") timeoutIndex = 0 s.vpn.healthyTimer.Stop() s.vpn.healthyWait = *s.config.VPN.Initial case previousErr == nil && err != nil: // First failure s.logger.Debug("unhealthy: " + err.Error()) s.vpn.healthyTimer.Stop() s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait) case previousErr != nil && err != nil: // Nth failure if timeoutIndex < len(healthcheckTimeouts)-1 { timeoutIndex++ } select { case <-s.vpn.healthyTimer.C: timeoutIndex = 0 // retry next with the smallest timeout s.onUnhealthyVPN(ctx) default: } case previousErr == nil && err == nil: // Nth success timer := time.NewTimer(s.config.SuccessWait) select { case <-ctx.Done(): return case <-timer.C: } } } } func (s *Server) healthCheck(ctx context.Context) (err error) { // TODO use mullvad API if current provider is Mullvad address, err := makeAddressToDial(s.config.TargetAddress) if err != nil { return err } const dialNetwork = "tcp4" connection, err := s.dialer.DialContext(ctx, dialNetwork, address) if err != nil { return fmt.Errorf("dialing: %w", err) } err = connection.Close() if err != nil { return fmt.Errorf("closing connection: %w", err) } return nil } func makeAddressToDial(address string) (addressToDial string, err error) { host, port, err := net.SplitHostPort(address) if err != nil { addrErr := new(net.AddrError) ok := errors.As(err, &addrErr) if !ok || addrErr.Err != "missing port in address" { return "", fmt.Errorf("splitting host and port from address: %w", err) } host = address const defaultPort = "443" port = defaultPort } address = net.JoinHostPort(host, port) return address, nil }