diff --git a/Dockerfile b/Dockerfile index 9d34317a..50fdde90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -113,6 +113,7 @@ ENV VPNSP=pia \ # Health HEALTH_OPENVPN_DURATION_INITIAL=6s \ HEALTH_OPENVPN_DURATION_ADDITION=5s \ + HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \ # DNS over TLS DOT=on \ DOT_PROVIDERS=cloudflare \ diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index 4e808d47..6e3c4119 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -71,10 +71,11 @@ func main() { osUser := user.New() unix := unix.New() cli := cli.New() + env := params.NewEnv() errorCh := make(chan error) go func() { - errorCh <- _main(ctx, buildInfo, args, logger, os, osUser, unix, cli) + errorCh <- _main(ctx, buildInfo, args, logger, env, os, osUser, unix, cli) }() select { @@ -112,12 +113,12 @@ var ( //nolint:gocognit,gocyclo func _main(ctx context.Context, buildInfo models.BuildInformation, - args []string, logger logging.ParentLogger, os os.OS, + args []string, logger logging.ParentLogger, env params.Env, os os.OS, osUser user.OSUser, unix unix.Unix, cli cli.CLI) error { if len(args) > 1 { // cli operation switch args[1] { case "healthcheck": - return cli.HealthCheck(ctx) + return cli.HealthCheck(ctx, env, os, logger) case "clientkey": return cli.ClientKey(args[2:], os.OpenFile) case "openvpnconfig": @@ -159,7 +160,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, } var allSettings configuration.Settings - err := allSettings.Read(params.NewEnv(), os, + err := allSettings.Read(env, os, logger.NewChild(logging.Settings{Prefix: "configuration: "})) if err != nil { return err @@ -365,8 +366,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, controlGroupHandler.Add(httpServerHandler) healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "}) - healthcheckServer := healthcheck.NewServer(constants.HealthcheckAddress, - allSettings.Health, healthLogger, openvpnLooper) + healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, openvpnLooper) healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( "HTTP health server", defaultGoRoutineSettings) go healthcheckServer.Run(healthServerCtx, healthServerDone) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index d5828cef..1664f459 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,11 +6,12 @@ import ( "github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/os" + "github.com/qdm12/golibs/params" ) type CLI interface { ClientKey(args []string, openFile os.OpenFileFunc) error - HealthCheck(ctx context.Context) error + HealthCheck(ctx context.Context, env params.Env, os os.OS, logger logging.Logger) error OpenvpnConfig(os os.OS, logger logging.Logger) error Update(ctx context.Context, args []string, os os.OS, logger logging.Logger) error } diff --git a/internal/cli/healthcheck.go b/internal/cli/healthcheck.go index 3513d5af..95543c9b 100644 --- a/internal/cli/healthcheck.go +++ b/internal/cli/healthcheck.go @@ -2,19 +2,36 @@ package cli import ( "context" + "net" "net/http" "time" - "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/configuration" "github.com/qdm12/gluetun/internal/healthcheck" + "github.com/qdm12/golibs/logging" + "github.com/qdm12/golibs/os" + "github.com/qdm12/golibs/params" ) -func (c *cli) HealthCheck(ctx context.Context) error { +func (c *cli) HealthCheck(ctx context.Context, env params.Env, + os os.OS, logger logging.Logger) error { + // Extract the health server port from the configuration. + config := configuration.Health{} + err := config.Read(env, os, logger) + if err != nil { + return err + } + _, port, err := net.SplitHostPort(config.ServerAddress) + if err != nil { + return err + } + const timeout = 10 * time.Second httpClient := &http.Client{Timeout: timeout} healthchecker := healthcheck.NewChecker(httpClient) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - const url = "http://" + constants.HealthcheckAddress + + url := "http://127.0.0.1:" + port return healthchecker.Check(ctx, url) } diff --git a/internal/configuration/health.go b/internal/configuration/health.go index 409d6610..2af20114 100644 --- a/internal/configuration/health.go +++ b/internal/configuration/health.go @@ -3,12 +3,15 @@ package configuration import ( "strings" + "github.com/qdm12/golibs/logging" + "github.com/qdm12/golibs/os" "github.com/qdm12/golibs/params" ) // Health contains settings for the healthcheck and health server. type Health struct { - OpenVPN HealthyWait + ServerAddress string + OpenVPN HealthyWait } func (settings *Health) String() string { @@ -18,6 +21,8 @@ func (settings *Health) String() string { func (settings *Health) lines() (lines []string) { lines = append(lines, lastIndent+"Health:") + lines = append(lines, indent+lastIndent+"Server address: "+settings.ServerAddress) + lines = append(lines, indent+lastIndent+"OpenVPN:") for _, line := range settings.OpenVPN.lines() { lines = append(lines, indent+indent+line) @@ -26,7 +31,23 @@ func (settings *Health) lines() (lines []string) { return lines } +// Read is to be used for the healthcheck query mode. +func (settings *Health) Read(env params.Env, os os.OS, logger logging.Logger) (err error) { + reader := newReader(env, os, logger) + return settings.read(reader) +} + func (settings *Health) read(r reader) (err error) { + var warning string + settings.ServerAddress, warning, err = r.env.ListeningAddress( + "HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999")) + if warning != "" { + r.logger.Warn("health server address: " + warning) + } + if err != nil { + return err + } + settings.OpenVPN.Initial, err = r.env.Duration("HEALTH_OPENVPN_DURATION_INITIAL", params.Default("6s")) if err != nil { return err diff --git a/internal/configuration/health_test.go b/internal/configuration/health_test.go index aa99994d..9a1add46 100644 --- a/internal/configuration/health_test.go +++ b/internal/configuration/health_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/golang/mock/gomock" + "github.com/qdm12/golibs/logging/mock_logging" "github.com/qdm12/golibs/params/mock_params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,7 +16,7 @@ func Test_Health_String(t *testing.T) { t.Parallel() var health Health - const expected = "|--Health:\n |--OpenVPN:\n |--Initial duration: 0s" + const expected = "|--Health:\n |--Server address: \n |--OpenVPN:\n |--Initial duration: 0s" s := health.String() @@ -32,12 +33,14 @@ func Test_Health_lines(t *testing.T) { "empty": { lines: []string{ "|--Health:", + " |--Server address: ", " |--OpenVPN:", " |--Initial duration: 0s", }, }, "filled settings": { settings: Health{ + ServerAddress: "address:9999", OpenVPN: HealthyWait{ Initial: time.Second, Addition: time.Minute, @@ -45,6 +48,7 @@ func Test_Health_lines(t *testing.T) { }, lines: []string{ "|--Health:", + " |--Server address: address:9999", " |--OpenVPN:", " |--Initial duration: 1s", " |--Addition duration: 1m0s", @@ -74,19 +78,35 @@ func Test_Health_read(t *testing.T) { openvpnInitialErr error openvpnAdditionDuration time.Duration openvpnAdditionErr error + serverAddress string + serverAddressWarning string + serverAddressErr error expected Health err error }{ "success": { openvpnInitialDuration: time.Second, openvpnAdditionDuration: time.Minute, + serverAddress: "127.0.0.1:9999", expected: Health{ + ServerAddress: "127.0.0.1:9999", OpenVPN: HealthyWait{ Initial: time.Second, Addition: time.Minute, }, }, }, + "listening address error": { + openvpnInitialDuration: time.Second, + openvpnAdditionDuration: time.Minute, + serverAddress: "127.0.0.1:9999", + serverAddressWarning: "warning", + serverAddressErr: errDummy, + expected: Health{ + ServerAddress: "127.0.0.1:9999", + }, + err: errDummy, + }, "initial error": { openvpnInitialDuration: time.Second, openvpnInitialErr: errDummy, @@ -120,17 +140,29 @@ func Test_Health_read(t *testing.T) { ctrl := gomock.NewController(t) env := mock_params.NewMockEnv(ctrl) - env.EXPECT(). - Duration("HEALTH_OPENVPN_DURATION_INITIAL", gomock.Any()). - Return(testCase.openvpnInitialDuration, testCase.openvpnInitialErr) - if testCase.openvpnInitialErr == nil { + logger := mock_logging.NewMockLogger(ctrl) + + env.EXPECT().ListeningAddress("HEALTH_SERVER_ADDRESS", gomock.Any()). + Return(testCase.serverAddress, testCase.serverAddressWarning, + testCase.serverAddressErr) + if testCase.serverAddressWarning != "" { + logger.EXPECT().Warn("health server address: " + testCase.serverAddressWarning) + } + + if testCase.serverAddressErr == nil { env.EXPECT(). - Duration("HEALTH_OPENVPN_DURATION_ADDITION", gomock.Any()). - Return(testCase.openvpnAdditionDuration, testCase.openvpnAdditionErr) + Duration("HEALTH_OPENVPN_DURATION_INITIAL", gomock.Any()). + Return(testCase.openvpnInitialDuration, testCase.openvpnInitialErr) + if testCase.openvpnInitialErr == nil { + env.EXPECT(). + Duration("HEALTH_OPENVPN_DURATION_ADDITION", gomock.Any()). + Return(testCase.openvpnAdditionDuration, testCase.openvpnAdditionErr) + } } r := reader{ - env: env, + env: env, + logger: logger, } var health Health diff --git a/internal/configuration/settings_test.go b/internal/configuration/settings_test.go index c51b1c78..08d09b3c 100644 --- a/internal/configuration/settings_test.go +++ b/internal/configuration/settings_test.go @@ -38,6 +38,7 @@ func Test_Settings_lines(t *testing.T) { " |--Process group ID: 0", " |--Timezone: NOT SET ⚠️ - it can cause time related issues", "|--Health:", + " |--Server address: ", " |--OpenVPN:", " |--Initial duration: 0s", "|--HTTP control server:", diff --git a/internal/constants/addresses.go b/internal/constants/addresses.go deleted file mode 100644 index 28c1f716..00000000 --- a/internal/constants/addresses.go +++ /dev/null @@ -1,6 +0,0 @@ -package constants - -const ( - // HealthcheckAddress is the default listening address for the healthcheck server. - HealthcheckAddress = "127.0.0.1:9999" -) diff --git a/internal/healthcheck/server.go b/internal/healthcheck/server.go index bcce922e..5788a958 100644 --- a/internal/healthcheck/server.go +++ b/internal/healthcheck/server.go @@ -17,7 +17,6 @@ type Server interface { } type server struct { - address string logger logging.Logger handler *handler resolver *net.Resolver @@ -31,10 +30,9 @@ type openvpnHealth struct { healthyTimer *time.Timer } -func NewServer(address string, config configuration.Health, +func NewServer(config configuration.Health, logger logging.Logger, openvpnLooper openvpn.Looper) Server { return &server{ - address: address, logger: logger, handler: newHandler(logger), resolver: net.DefaultResolver, @@ -53,7 +51,7 @@ func (s *server) Run(ctx context.Context, done chan<- struct{}) { go s.runHealthcheckLoop(ctx, loopDone) server := http.Server{ - Addr: s.address, + Addr: s.config.ServerAddress, Handler: s.handler, } serverDone := make(chan struct{}) @@ -68,7 +66,7 @@ func (s *server) Run(ctx context.Context, done chan<- struct{}) { } }() - s.logger.Info("listening on %s", s.address) + s.logger.Info("listening on " + s.config.ServerAddress) err := server.ListenAndServe() if err != nil && !errors.Is(ctx.Err(), context.Canceled) { s.logger.Error(err)