Feat: healthcheck uses ping instead of DNS

This commit is contained in:
Quentin McGaw (desktop)
2021-09-11 21:49:46 +00:00
parent 0eccd068e5
commit 541a4a3271
8 changed files with 215 additions and 27 deletions

View File

@@ -3,9 +3,6 @@ package healthcheck
import (
"context"
"errors"
"fmt"
"net"
"time"
)
@@ -17,7 +14,7 @@ func (s *Server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) {
for {
previousErr := s.handler.getErr()
err := healthCheck(ctx, s.resolver)
err := healthCheck(ctx, s.pinger)
s.handler.setErr(err)
if previousErr != nil && err == nil {
@@ -59,20 +56,23 @@ func (s *Server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) {
}
}
var (
errNoIPResolved = errors.New("no IP address resolved")
)
func healthCheck(ctx context.Context, resolver *net.Resolver) (err error) {
func healthCheck(ctx context.Context, pinger Pinger) (err error) {
// TODO use mullvad API if current provider is Mullvad
const domainToResolve = "github.com"
ips, err := resolver.LookupIP(ctx, "ip", domainToResolve)
switch {
case err != nil:
// If we run without root, you need to run this on the gluetun binary:
// setcap cap_net_raw=+ep /path/to/your/compiled/binary
// Alternatively, we could have a separate binary just for healthcheck to
// reduce attack surface.
errCh := make(chan error)
go func() {
errCh <- pinger.Run()
}()
select {
case <-ctx.Done():
pinger.Stop()
<-errCh
return ctx.Err()
case err = <-errCh:
return err
case len(ips) == 0:
return fmt.Errorf("%w for %s", errNoIPResolved, domainToResolve)
default:
return nil
}
}

View File

@@ -0,0 +1,20 @@
//go:build integration
package healthcheck
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_healthCheck_ping(t *testing.T) {
t.Parallel()
pinger := newPinger()
err := healthCheck(context.Background(), pinger)
assert.NoError(t, err)
}

View File

@@ -0,0 +1,81 @@
package healthcheck
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func Test_healthCheck(t *testing.T) {
t.Parallel()
canceledCtx, cancel := context.WithCancel(context.Background())
cancel()
someErr := errors.New("error")
testCases := map[string]struct {
ctx context.Context
runErr error
stopCall bool
err error
}{
"success": {
ctx: context.Background(),
},
"error": {
ctx: context.Background(),
runErr: someErr,
err: someErr,
},
"context canceled": {
ctx: canceledCtx,
stopCall: true,
err: context.Canceled,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stopped := make(chan struct{})
pinger := NewMockPinger(ctrl)
pinger.EXPECT().Run().DoAndReturn(func() error {
if testCase.stopCall {
<-stopped
}
return testCase.runErr
})
if testCase.stopCall {
pinger.EXPECT().Stop().DoAndReturn(func() {
close(stopped)
})
}
err := healthCheck(testCase.ctx, pinger)
assert.ErrorIs(t, testCase.err, err)
})
}
t.Run("canceled real pinger", func(t *testing.T) {
t.Parallel()
pinger := newPinger()
canceledCtx, cancel := context.WithCancel(context.Background())
cancel()
err := healthCheck(canceledCtx, pinger)
assert.ErrorIs(t, context.Canceled, err)
})
}

View File

@@ -0,0 +1,19 @@
package healthcheck
import "github.com/go-ping/ping"
//go:generate mockgen -destination=pinger_mock_test.go -package healthcheck . Pinger
type Pinger interface {
Run() error
Stop()
}
func newPinger() (pinger *ping.Pinger) {
const addrToPing = "1.1.1.1"
const count = 1
pinger = ping.New(addrToPing)
pinger.Count = count
pinger.SetPrivileged(true)
return pinger
}

View File

@@ -0,0 +1,60 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/healthcheck (interfaces: Pinger)
// Package healthcheck is a generated GoMock package.
package healthcheck
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockPinger is a mock of Pinger interface.
type MockPinger struct {
ctrl *gomock.Controller
recorder *MockPingerMockRecorder
}
// MockPingerMockRecorder is the mock recorder for MockPinger.
type MockPingerMockRecorder struct {
mock *MockPinger
}
// NewMockPinger creates a new mock instance.
func NewMockPinger(ctrl *gomock.Controller) *MockPinger {
mock := &MockPinger{ctrl: ctrl}
mock.recorder = &MockPingerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPinger) EXPECT() *MockPingerMockRecorder {
return m.recorder
}
// Run mocks base method.
func (m *MockPinger) Run() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Run")
ret0, _ := ret[0].(error)
return ret0
}
// Run indicates an expected call of Run.
func (mr *MockPingerMockRecorder) Run() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockPinger)(nil).Run))
}
// Stop mocks base method.
func (m *MockPinger) Stop() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Stop")
}
// Stop indicates an expected call of Stop.
func (mr *MockPingerMockRecorder) Stop() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockPinger)(nil).Stop))
}

View File

@@ -2,7 +2,6 @@ package healthcheck
import (
"context"
"net"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/vpn"
@@ -16,20 +15,20 @@ type ServerRunner interface {
}
type Server struct {
logger logging.Logger
handler *handler
resolver *net.Resolver
config configuration.Health
vpn vpnHealth
logger logging.Logger
handler *handler
pinger Pinger
config configuration.Health
vpn vpnHealth
}
func NewServer(config configuration.Health,
logger logging.Logger, vpnLooper vpn.Looper) *Server {
return &Server{
logger: logger,
handler: newHandler(logger),
resolver: net.DefaultResolver,
config: config,
logger: logger,
handler: newHandler(logger),
pinger: newPinger(),
config: config,
vpn: vpnHealth{
looper: vpnLooper,
healthyWait: config.VPN.Initial,