feat(health): HEALTH_RESTART_VPN option
- You should really leave it to `on` ⚠️
- Turn it to `off` if you have trust issues with the healthcheck. Don't then report issues if the connection is dead though.
This commit is contained in:
@@ -165,6 +165,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||||
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||||
HEALTH_ICMP_TARGET_IP=0.0.0.0 \
|
HEALTH_ICMP_TARGET_IP=0.0.0.0 \
|
||||||
|
HEALTH_RESTART_VPN=on \
|
||||||
# DNS over TLS
|
# DNS over TLS
|
||||||
DOT=on \
|
DOT=on \
|
||||||
DOT_PROVIDERS=cloudflare \
|
DOT_PROVIDERS=cloudflare \
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ type Health struct {
|
|||||||
// in the health checker. It can be set to an unspecified address
|
// in the health checker. It can be set to an unspecified address
|
||||||
// such that the VPN server IP is used, which is also the default behavior.
|
// such that the VPN server IP is used, which is also the default behavior.
|
||||||
ICMPTargetIP netip.Addr
|
ICMPTargetIP netip.Addr
|
||||||
|
// RestartVPN indicates whether to restart the VPN connection
|
||||||
|
// when the healthcheck fails.
|
||||||
|
RestartVPN *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Health) Validate() (err error) {
|
func (h Health) Validate() (err error) {
|
||||||
@@ -50,6 +53,7 @@ func (h *Health) copy() (copied Health) {
|
|||||||
ReadTimeout: h.ReadTimeout,
|
ReadTimeout: h.ReadTimeout,
|
||||||
TargetAddress: h.TargetAddress,
|
TargetAddress: h.TargetAddress,
|
||||||
ICMPTargetIP: h.ICMPTargetIP,
|
ICMPTargetIP: h.ICMPTargetIP,
|
||||||
|
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +66,7 @@ func (h *Health) OverrideWith(other Health) {
|
|||||||
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||||
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||||
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
|
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
|
||||||
|
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Health) SetDefaults() {
|
func (h *Health) SetDefaults() {
|
||||||
@@ -72,6 +77,7 @@ func (h *Health) SetDefaults() {
|
|||||||
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||||
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||||
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
|
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
|
||||||
|
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Health) String() string {
|
func (h Health) String() string {
|
||||||
@@ -87,6 +93,7 @@ func (h Health) toLinesNode() (node *gotree.Node) {
|
|||||||
icmpTarget = h.ICMPTargetIP.String()
|
icmpTarget = h.ICMPTargetIP.String()
|
||||||
}
|
}
|
||||||
node.Appendf("ICMP target IP: %s", icmpTarget)
|
node.Appendf("ICMP target IP: %s", icmpTarget)
|
||||||
|
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,5 +105,9 @@ func (h *Health) Read(r *reader.Reader) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
├── Health settings:
|
├── Health settings:
|
||||||
| ├── Server listening address: 127.0.0.1:9999
|
| ├── Server listening address: 127.0.0.1:9999
|
||||||
| ├── Target address: cloudflare.com:443
|
| ├── Target address: cloudflare.com:443
|
||||||
| └── ICMP target IP: VPN server IP
|
| ├── ICMP target IP: VPN server IP
|
||||||
|
| └── Restart VPN on healthcheck failure: yes
|
||||||
├── Shadowsocks server settings:
|
├── Shadowsocks server settings:
|
||||||
| └── Enabled: no
|
| └── Enabled: no
|
||||||
├── HTTP proxy settings:
|
├── HTTP proxy settings:
|
||||||
|
|||||||
@@ -59,8 +59,9 @@ func (c *Checker) SetConfig(tlsDialAddr string, icmpTarget netip.Addr) {
|
|||||||
// and, on success, starts the periodic checks in a separate goroutine:
|
// and, on success, starts the periodic checks in a separate goroutine:
|
||||||
// - a "small" ICMP echo check every 15 seconds
|
// - a "small" ICMP echo check every 15 seconds
|
||||||
// - a "full" TCP+TLS check every 5 minutes
|
// - a "full" TCP+TLS check every 5 minutes
|
||||||
// It returns a channel `runError` that receives an error if one of the periodic checks fail.
|
// It returns a channel `runError` that receives an error (nil or not) when a periodic check is performed.
|
||||||
// It returns an error if the initial TCP+TLS check fails.
|
// It returns an error if the initial TCP+TLS check fails.
|
||||||
|
// The Checker has to be ultimately stopped by calling [Checker.Stop].
|
||||||
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
|
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
|
||||||
if c.tlsDialAddr == "" || c.icmpTarget.IsUnspecified() {
|
if c.tlsDialAddr == "" || c.icmpTarget.IsUnspecified() {
|
||||||
panic("call Checker.SetConfig with non empty values before Checker.Start")
|
panic("call Checker.SetConfig with non empty values before Checker.Start")
|
||||||
@@ -101,16 +102,16 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
|
|||||||
case <-smallCheckTimer.C:
|
case <-smallCheckTimer.C:
|
||||||
err := c.smallPeriodicCheck(ctx)
|
err := c.smallPeriodicCheck(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runErrorCh <- fmt.Errorf("periodic small check: %w", err)
|
err = fmt.Errorf("small periodic check: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
runErrorCh <- err
|
||||||
smallCheckTimer.Reset(smallCheckPeriod)
|
smallCheckTimer.Reset(smallCheckPeriod)
|
||||||
case <-fullCheckTimer.C:
|
case <-fullCheckTimer.C:
|
||||||
err := c.fullPeriodicCheck(ctx)
|
err := c.fullPeriodicCheck(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runErrorCh <- fmt.Errorf("periodic full check: %w", err)
|
err = fmt.Errorf("full periodic check: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
runErrorCh <- err
|
||||||
fullCheckTimer.Reset(fullCheckPeriod)
|
fullCheckTimer.Reset(fullCheckPeriod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
|||||||
l.restartVPN(loopCtx, err)
|
l.restartVPN(loopCtx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
_ = l.healthChecker.Stop()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if *l.dnsLooper.GetSettings().DoT.Enabled {
|
if *l.dnsLooper.GetSettings().DoT.Enabled {
|
||||||
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
||||||
@@ -78,13 +75,25 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
|||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
for {
|
||||||
case <-ctx.Done():
|
select {
|
||||||
case healthErr := <-healthErrCh:
|
case <-ctx.Done():
|
||||||
l.healthServer.SetError(healthErr)
|
_ = l.healthChecker.Stop()
|
||||||
// Note this restart call must be done in a separate goroutine
|
return
|
||||||
// from the VPN loop goroutine.
|
case healthErr := <-healthErrCh:
|
||||||
l.restartVPN(loopCtx, healthErr)
|
l.healthServer.SetError(healthErr)
|
||||||
|
if healthErr != nil {
|
||||||
|
if *l.healthSettings.RestartVPN {
|
||||||
|
// Note this restart call must be done in a separate goroutine
|
||||||
|
// from the VPN loop goroutine.
|
||||||
|
_ = l.healthChecker.Stop()
|
||||||
|
l.restartVPN(loopCtx, healthErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.logger.Warnf("healthcheck failed: %s", healthErr)
|
||||||
|
l.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user