Skip to content

Commit 8589052

Browse files
authored
feat(healthcheck): combination of ICMP and TCP+TLS checks (#2923)
- New option: `HEALTH_ICMP_TARGET_IP` defaults to `0.0.0.0` meaning use the VPN server public IP address. - Options removed: `HEALTH_VPN_INITIAL_DURATION` and `HEALTH_VPN_ADDITIONAL_DURATION` - times and retries are handpicked and hardcoded. - Less aggressive checks and less false positive detection
1 parent 3400165 commit 8589052

25 files changed

Lines changed: 722 additions & 366 deletions

Dockerfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
164164
# Health
165165
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
166166
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
167-
HEALTH_SUCCESS_WAIT_DURATION=5s \
168-
HEALTH_VPN_DURATION_INITIAL=6s \
169-
HEALTH_VPN_DURATION_ADDITION=5s \
167+
HEALTH_ICMP_TARGET_IP=0.0.0.0 \
170168
# DNS over TLS
171169
DOT=on \
172170
DOT_PROVIDERS=cloudflare \

cmd/gluetun/main.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
414414
return fmt.Errorf("starting public ip loop: %w", err)
415415
}
416416

417+
healthLogger := logger.New(log.SetComponent("healthcheck"))
418+
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
419+
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
420+
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
421+
go healthcheckServer.Run(healthServerCtx, healthServerDone)
422+
healthChecker := healthcheck.NewChecker(healthLogger)
423+
417424
updaterLogger := logger.New(log.SetComponent("updater"))
418425

419426
unzipper := unzip.New(httpClient)
@@ -424,8 +431,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
424431

425432
vpnLogger := logger.New(log.SetComponent("vpn"))
426433
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
427-
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
428-
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
434+
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
435+
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
429436
buildInfo, *allSettings.Version.Enabled)
430437
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
431438
"vpn", goroutine.OptionTimeout(time.Second))
@@ -476,12 +483,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
476483
<-httpServerReady
477484
controlGroupHandler.Add(httpServerHandler)
478485

479-
healthLogger := logger.New(log.SetComponent("healthcheck"))
480-
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
481-
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
482-
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
483-
go healthcheckServer.Run(healthServerCtx, healthServerDone)
484-
485486
orderHandler := goshutdown.NewOrderHandler("gluetun",
486487
order.OptionTimeout(totalShutdownTimeout),
487488
order.OptionOnSuccess(defaultShutdownOnSuccess),

internal/configuration/settings/deprecated.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99

1010
func readObsolete(r *reader.Reader) (warnings []string) {
1111
keyToMessage := map[string]string{
12-
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
13-
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
14-
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
12+
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
13+
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
14+
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
15+
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
16+
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
1517
}
1618
sortedKeys := maps.Keys(keyToMessage)
1719
slices.Sort(sortedKeys)

internal/configuration/settings/health.go

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package settings
22

33
import (
44
"fmt"
5+
"net/netip"
56
"os"
67
"time"
78

@@ -24,16 +25,13 @@ type Health struct {
2425
// HTTP server. It defaults to 500 milliseconds.
2526
ReadTimeout time.Duration
2627
// TargetAddress is the address (host or host:port)
27-
// to TCP dial to periodically for the health check.
28+
// to TCP TLS dial to periodically for the health check.
2829
// It cannot be the empty string in the internal state.
2930
TargetAddress string
30-
// SuccessWait is the duration to wait to re-run the
31-
// healthcheck after a successful healthcheck.
32-
// It defaults to 5 seconds and cannot be zero in
33-
// the internal state.
34-
SuccessWait time.Duration
35-
// VPN has health settings specific to the VPN loop.
36-
VPN HealthyWait
31+
// ICMPTargetIP is the IP address to use for ICMP echo requests
32+
// in the health checker. It can be set to an unspecified address
33+
// such that the VPN server IP is used, which is also the default behavior.
34+
ICMPTargetIP netip.Addr
3735
}
3836

3937
func (h Health) Validate() (err error) {
@@ -42,11 +40,6 @@ func (h Health) Validate() (err error) {
4240
return fmt.Errorf("server listening address is not valid: %w", err)
4341
}
4442

45-
err = h.VPN.validate()
46-
if err != nil {
47-
return fmt.Errorf("health VPN settings: %w", err)
48-
}
49-
5043
return nil
5144
}
5245

@@ -56,8 +49,7 @@ func (h *Health) copy() (copied Health) {
5649
ReadHeaderTimeout: h.ReadHeaderTimeout,
5750
ReadTimeout: h.ReadTimeout,
5851
TargetAddress: h.TargetAddress,
59-
SuccessWait: h.SuccessWait,
60-
VPN: h.VPN.copy(),
52+
ICMPTargetIP: h.ICMPTargetIP,
6153
}
6254
}
6355

@@ -69,8 +61,7 @@ func (h *Health) OverrideWith(other Health) {
6961
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
7062
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
7163
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
72-
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
73-
h.VPN.overrideWith(other.VPN)
64+
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
7465
}
7566

7667
func (h *Health) SetDefaults() {
@@ -80,9 +71,7 @@ func (h *Health) SetDefaults() {
8071
const defaultReadTimeout = 500 * time.Millisecond
8172
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
8273
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
83-
const defaultSuccessWait = 5 * time.Second
84-
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
85-
h.VPN.setDefaults()
74+
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
8675
}
8776

8877
func (h Health) String() string {
@@ -93,27 +82,21 @@ func (h Health) toLinesNode() (node *gotree.Node) {
9382
node = gotree.New("Health settings:")
9483
node.Appendf("Server listening address: %s", h.ServerAddress)
9584
node.Appendf("Target address: %s", h.TargetAddress)
96-
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
97-
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
98-
node.Appendf("Read timeout: %s", h.ReadTimeout)
99-
node.AppendNode(h.VPN.toLinesNode("VPN"))
85+
icmpTarget := "VPN server IP"
86+
if !h.ICMPTargetIP.IsUnspecified() {
87+
icmpTarget = h.ICMPTargetIP.String()
88+
}
89+
node.Appendf("ICMP target IP: %s", icmpTarget)
10090
return node
10191
}
10292

10393
func (h *Health) Read(r *reader.Reader) (err error) {
10494
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
10595
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
10696
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
107-
108-
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
97+
h.ICMPTargetIP, err = r.NetipAddr("HEALTH_ICMP_TARGET_IP")
10998
if err != nil {
11099
return err
111100
}
112-
113-
err = h.VPN.read(r)
114-
if err != nil {
115-
return fmt.Errorf("VPN health settings: %w", err)
116-
}
117-
118101
return nil
119102
}

internal/configuration/settings/healthywait.go

Lines changed: 0 additions & 76 deletions
This file was deleted.

internal/configuration/settings/settings_test.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,7 @@ func Test_Settings_String(t *testing.T) {
5858
├── Health settings:
5959
| ├── Server listening address: 127.0.0.1:9999
6060
| ├── Target address: cloudflare.com:443
61-
| ├── Duration to wait after success: 5s
62-
| ├── Read header timeout: 100ms
63-
| ├── Read timeout: 500ms
64-
| └── VPN wait durations:
65-
| ├── Initial duration: 6s
66-
| └── Additional duration: 5s
61+
| └── ICMP target IP: VPN server IP
6762
├── Shadowsocks server settings:
6863
| └── Enabled: no
6964
├── HTTP proxy settings:

0 commit comments

Comments
 (0)