From fed09562e5d8bdfad694adef094d8ece38316e90 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Mon, 4 May 2026 03:28:48 +0000 Subject: [PATCH] feat(vpn): rotate filtered servers on internal vpn restarts - Fix #290 --- internal/provider/airvpn/connection.go | 2 +- internal/provider/airvpn/provider.go | 10 +- internal/provider/cyberghost/connection.go | 2 +- internal/provider/cyberghost/provider.go | 11 +- internal/provider/example/connection.go | 2 +- internal/provider/example/provider.go | 9 +- internal/provider/expressvpn/connection.go | 2 +- .../provider/expressvpn/connection_test.go | 4 +- internal/provider/expressvpn/provider.go | 10 +- internal/provider/fastestvpn/connection.go | 2 +- internal/provider/fastestvpn/provider.go | 9 +- internal/provider/giganews/connection.go | 2 +- internal/provider/giganews/provider.go | 10 +- internal/provider/hidemyass/connection.go | 2 +- internal/provider/hidemyass/provider.go | 9 +- internal/provider/ipvanish/connection.go | 2 +- internal/provider/ipvanish/provider.go | 10 +- internal/provider/ivpn/connection.go | 2 +- internal/provider/ivpn/connection_test.go | 4 +- internal/provider/ivpn/provider.go | 9 +- internal/provider/mullvad/connection.go | 2 +- internal/provider/mullvad/connection_test.go | 4 +- internal/provider/mullvad/provider.go | 9 +- internal/provider/nordvpn/connection.go | 2 +- internal/provider/nordvpn/provider.go | 9 +- .../provider/perfectprivacy/connection.go | 2 +- internal/provider/perfectprivacy/provider.go | 10 +- internal/provider/privado/connection.go | 2 +- internal/provider/privado/provider.go | 9 +- .../privateinternetaccess/connection.go | 2 +- .../privateinternetaccess/provider.go | 10 +- internal/provider/privatevpn/connection.go | 2 +- internal/provider/privatevpn/provider.go | 10 +- internal/provider/protonvpn/connection.go | 2 +- internal/provider/protonvpn/provider.go | 9 +- internal/provider/providers.go | 49 +++---- internal/provider/purevpn/connection.go | 2 +- internal/provider/purevpn/provider.go | 10 +- internal/provider/slickvpn/connection.go | 2 +- internal/provider/slickvpn/provider.go | 9 +- internal/provider/surfshark/connection.go | 2 +- internal/provider/surfshark/provider.go | 11 +- internal/provider/torguard/connection.go | 2 +- internal/provider/torguard/provider.go | 10 +- internal/provider/utils/connection.go | 6 +- internal/provider/utils/connection_test.go | 3 +- internal/provider/utils/pick.go | 77 ++++++++-- internal/provider/utils/pick_test.go | 135 ++++++++++++++++-- internal/provider/vpnsecure/connection.go | 2 +- internal/provider/vpnsecure/provider.go | 9 +- internal/provider/vpnunlimited/connection.go | 2 +- internal/provider/vpnunlimited/provider.go | 10 +- internal/provider/vyprvpn/connection.go | 2 +- internal/provider/vyprvpn/provider.go | 10 +- internal/provider/windscribe/connection.go | 2 +- .../provider/windscribe/connection_test.go | 4 +- internal/provider/windscribe/provider.go | 9 +- 57 files changed, 345 insertions(+), 220 deletions(-) diff --git a/internal/provider/airvpn/connection.go b/internal/provider/airvpn/connection.go index 6faa4f68..2d851f9c 100644 --- a/internal/provider/airvpn/connection.go +++ b/internal/provider/airvpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 1194, 1637) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/airvpn/provider.go b/internal/provider/airvpn/provider.go index 377506ec..9a7dc7ed 100644 --- a/internal/provider/airvpn/provider.go +++ b/internal/provider/airvpn/provider.go @@ -1,26 +1,24 @@ package airvpn import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/airvpn/updater" "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, -) *Provider { +func New(storage common.Storage, client *http.Client) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client), } } diff --git a/internal/provider/cyberghost/connection.go b/internal/provider/cyberghost/connection.go index c3feb1ee..c5f2f4ea 100644 --- a/internal/provider/cyberghost/connection.go +++ b/internal/provider/cyberghost/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 443, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/cyberghost/provider.go b/internal/provider/cyberghost/provider.go index f7cd49b4..d4222035 100644 --- a/internal/provider/cyberghost/provider.go +++ b/internal/provider/cyberghost/provider.go @@ -1,25 +1,24 @@ package cyberghost import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/cyberghost/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - updaterWarner common.Warner, parallelResolver common.ParallelResolver, +func New(storage common.Storage, updaterWarner common.Warner, + parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(parallelResolver, updaterWarner), } } diff --git a/internal/provider/example/connection.go b/internal/provider/example/connection.go index 9f529291..6e6b1477 100644 --- a/internal/provider/example/connection.go +++ b/internal/provider/example/connection.go @@ -13,5 +13,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support // combination. If one combination is not supported, set it to `0`. defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/example/provider.go b/internal/provider/example/provider.go index 020a1b42..44672ca6 100644 --- a/internal/provider/example/provider.go +++ b/internal/provider/example/provider.go @@ -1,28 +1,27 @@ package example import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/example/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } // TODO: remove unneeded arguments once the updater is implemented. -func New(storage common.Storage, randSource rand.Source, - updaterWarner common.Warner, client *http.Client, +func New(storage common.Storage, updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver), } } diff --git a/internal/provider/expressvpn/connection.go b/internal/provider/expressvpn/connection.go index 5893ec0c..acd175fe 100644 --- a/internal/provider/expressvpn/connection.go +++ b/internal/provider/expressvpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(0, 1195, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/expressvpn/connection_test.go b/internal/provider/expressvpn/connection_test.go index 42559dc3..95287663 100644 --- a/internal/provider/expressvpn/connection_test.go +++ b/internal/provider/expressvpn/connection_test.go @@ -2,7 +2,6 @@ package expressvpn import ( "errors" - "math/rand" "net/netip" "testing" @@ -80,12 +79,11 @@ func Test_Provider_GetConnection(t *testing.T) { storage := common.NewMockStorage(ctrl) storage.EXPECT().FilterServers(provider, testCase.selection). Return(testCase.filteredServers, testCase.storageErr) - randSource := rand.NewSource(0) unzipper := (common.Unzipper)(nil) warner := (common.Warner)(nil) parallelResolver := (common.ParallelResolver)(nil) - provider := New(storage, randSource, unzipper, warner, parallelResolver) + provider := New(storage, unzipper, warner, parallelResolver) if testCase.panicMessage != "" { assert.PanicsWithValue(t, testCase.panicMessage, func() { diff --git a/internal/provider/expressvpn/provider.go b/internal/provider/expressvpn/provider.go index de55d2b3..00a4a6e4 100644 --- a/internal/provider/expressvpn/provider.go +++ b/internal/provider/expressvpn/provider.go @@ -1,26 +1,24 @@ package expressvpn import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/expressvpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/fastestvpn/connection.go b/internal/provider/fastestvpn/connection.go index f927e050..498fe7d8 100644 --- a/internal/provider/fastestvpn/connection.go +++ b/internal/provider/fastestvpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(4443, 4443, 51820) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/fastestvpn/provider.go b/internal/provider/fastestvpn/provider.go index d804894b..87fff14d 100644 --- a/internal/provider/fastestvpn/provider.go +++ b/internal/provider/fastestvpn/provider.go @@ -1,27 +1,26 @@ package fastestvpn import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/fastestvpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner, parallelResolver), } } diff --git a/internal/provider/giganews/connection.go b/internal/provider/giganews/connection.go index 13a9ae43..d103e25f 100644 --- a/internal/provider/giganews/connection.go +++ b/internal/provider/giganews/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/giganews/provider.go b/internal/provider/giganews/provider.go index 99a5afba..4fbb350b 100644 --- a/internal/provider/giganews/provider.go +++ b/internal/provider/giganews/provider.go @@ -1,26 +1,24 @@ package giganews import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/giganews/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/hidemyass/connection.go b/internal/provider/hidemyass/connection.go index 856d8db0..dad90eaf 100644 --- a/internal/provider/hidemyass/connection.go +++ b/internal/provider/hidemyass/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(8080, 553, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/hidemyass/provider.go b/internal/provider/hidemyass/provider.go index cc778d16..e4bef0c1 100644 --- a/internal/provider/hidemyass/provider.go +++ b/internal/provider/hidemyass/provider.go @@ -1,27 +1,26 @@ package hidemyass import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/hidemyass/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner, parallelResolver), } } diff --git a/internal/provider/ipvanish/connection.go b/internal/provider/ipvanish/connection.go index e66e9ece..f17b65cb 100644 --- a/internal/provider/ipvanish/connection.go +++ b/internal/provider/ipvanish/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/ipvanish/provider.go b/internal/provider/ipvanish/provider.go index c00dcdc6..d32a6677 100644 --- a/internal/provider/ipvanish/provider.go +++ b/internal/provider/ipvanish/provider.go @@ -1,26 +1,24 @@ package ipvanish import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/ipvanish/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/ivpn/connection.go b/internal/provider/ivpn/connection.go index 85558f69..634c2548 100644 --- a/internal/provider/ivpn/connection.go +++ b/internal/provider/ivpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 1194, 58237) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/ivpn/connection_test.go b/internal/provider/ivpn/connection_test.go index a952c663..10f251b7 100644 --- a/internal/provider/ivpn/connection_test.go +++ b/internal/provider/ivpn/connection_test.go @@ -2,7 +2,6 @@ package ivpn import ( "errors" - "math/rand" "net/http" "net/netip" "testing" @@ -91,12 +90,11 @@ func Test_Provider_GetConnection(t *testing.T) { storage := common.NewMockStorage(ctrl) storage.EXPECT().FilterServers(provider, testCase.selection). Return(testCase.filteredServers, testCase.storageErr) - randSource := rand.NewSource(0) client := (*http.Client)(nil) warner := (common.Warner)(nil) parallelResolver := (common.ParallelResolver)(nil) - provider := New(storage, randSource, client, warner, parallelResolver) + provider := New(storage, client, warner, parallelResolver) connection, err := provider.GetConnection(testCase.selection, testCase.ipv6Supported) diff --git a/internal/provider/ivpn/provider.go b/internal/provider/ivpn/provider.go index 1bee35aa..ebf2759b 100644 --- a/internal/provider/ivpn/provider.go +++ b/internal/provider/ivpn/provider.go @@ -1,27 +1,26 @@ package ivpn import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/ivpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner, parallelResolver), } } diff --git a/internal/provider/mullvad/connection.go b/internal/provider/mullvad/connection.go index 876fe596..e04f0dda 100644 --- a/internal/provider/mullvad/connection.go +++ b/internal/provider/mullvad/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(0, 0, 51820) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/mullvad/connection_test.go b/internal/provider/mullvad/connection_test.go index 5b821c01..ccdf4a0d 100644 --- a/internal/provider/mullvad/connection_test.go +++ b/internal/provider/mullvad/connection_test.go @@ -2,7 +2,6 @@ package mullvad import ( "errors" - "math/rand" "net/http" "net/netip" "testing" @@ -59,10 +58,9 @@ func Test_Provider_GetConnection(t *testing.T) { storage := common.NewMockStorage(ctrl) storage.EXPECT().FilterServers(provider, testCase.selection). Return(testCase.filteredServers, testCase.storageErr) - randSource := rand.NewSource(0) client := (*http.Client)(nil) - provider := New(storage, randSource, client) + provider := New(storage, client) connection, err := provider.GetConnection(testCase.selection, testCase.ipv6Supported) diff --git a/internal/provider/mullvad/provider.go b/internal/provider/mullvad/provider.go index 45e927e6..491c55be 100644 --- a/internal/provider/mullvad/provider.go +++ b/internal/provider/mullvad/provider.go @@ -1,26 +1,25 @@ package mullvad import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/mullvad/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, +func New(storage common.Storage, client *http.Client, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client), } } diff --git a/internal/provider/nordvpn/connection.go b/internal/provider/nordvpn/connection.go index 3e475141..9cc8f76b 100644 --- a/internal/provider/nordvpn/connection.go +++ b/internal/provider/nordvpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/nordvpn/provider.go b/internal/provider/nordvpn/provider.go index fd885b27..356c1e19 100644 --- a/internal/provider/nordvpn/provider.go +++ b/internal/provider/nordvpn/provider.go @@ -1,26 +1,25 @@ package nordvpn import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/nordvpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner), } } diff --git a/internal/provider/perfectprivacy/connection.go b/internal/provider/perfectprivacy/connection.go index 1c019e5f..9e3d0266 100644 --- a/internal/provider/perfectprivacy/connection.go +++ b/internal/provider/perfectprivacy/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 443, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/perfectprivacy/provider.go b/internal/provider/perfectprivacy/provider.go index 5ca1b4d9..e4488396 100644 --- a/internal/provider/perfectprivacy/provider.go +++ b/internal/provider/perfectprivacy/provider.go @@ -1,25 +1,23 @@ package perfectprivacy import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/perfectprivacy/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner), } } diff --git a/internal/provider/privado/connection.go b/internal/provider/privado/connection.go index 64170a5b..4e5d0ce3 100644 --- a/internal/provider/privado/connection.go +++ b/internal/provider/privado/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(1194, 1194, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/privado/provider.go b/internal/provider/privado/provider.go index 1fb99136..7868cdd2 100644 --- a/internal/provider/privado/provider.go +++ b/internal/provider/privado/provider.go @@ -1,26 +1,25 @@ package privado import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/privado/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner), } } diff --git a/internal/provider/privateinternetaccess/connection.go b/internal/provider/privateinternetaccess/connection.go index 652da1f0..c2608e3e 100644 --- a/internal/provider/privateinternetaccess/connection.go +++ b/internal/provider/privateinternetaccess/connection.go @@ -22,5 +22,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support } return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/privateinternetaccess/provider.go b/internal/provider/privateinternetaccess/provider.go index ca64f31a..da5ecf78 100644 --- a/internal/provider/privateinternetaccess/provider.go +++ b/internal/provider/privateinternetaccess/provider.go @@ -1,7 +1,6 @@ package privateinternetaccess import ( - "math/rand" "net/http" "net/netip" "time" @@ -9,11 +8,12 @@ import ( "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/privateinternetaccess/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker timeNow func() time.Time common.Fetcher // Port forwarding @@ -21,14 +21,14 @@ type Provider struct { apiIP netip.Addr } -func New(storage common.Storage, randSource rand.Source, - timeNow func() time.Time, client *http.Client, +func New(storage common.Storage, timeNow func() time.Time, + client *http.Client, ) *Provider { const jsonPortForwardPath = "/gluetun/piaportforward.json" return &Provider{ storage: storage, timeNow: timeNow, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), portForwardPath: jsonPortForwardPath, Fetcher: updater.New(client), } diff --git a/internal/provider/privatevpn/connection.go b/internal/provider/privatevpn/connection.go index ee117ec7..8fcf258f 100644 --- a/internal/provider/privatevpn/connection.go +++ b/internal/provider/privatevpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 1194, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/privatevpn/provider.go b/internal/provider/privatevpn/provider.go index 72221727..0678b01e 100644 --- a/internal/provider/privatevpn/provider.go +++ b/internal/provider/privatevpn/provider.go @@ -1,26 +1,24 @@ package privatevpn import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/privatevpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/protonvpn/connection.go b/internal/provider/protonvpn/connection.go index b7b77d25..5d3469ad 100644 --- a/internal/provider/protonvpn/connection.go +++ b/internal/provider/protonvpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/protonvpn/provider.go b/internal/provider/protonvpn/provider.go index 8af0457f..ea330365 100644 --- a/internal/provider/protonvpn/provider.go +++ b/internal/provider/protonvpn/provider.go @@ -1,28 +1,27 @@ package protonvpn import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/protonvpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher internalToExternalPorts map[uint16]uint16 } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, email, password string, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner, email, password), } } diff --git a/internal/provider/providers.go b/internal/provider/providers.go index aea562be..07402096 100644 --- a/internal/provider/providers.go +++ b/internal/provider/providers.go @@ -2,7 +2,6 @@ package provider import ( "fmt" - "math/rand" "net/http" "time" @@ -55,34 +54,32 @@ func NewProviders(storage Storage, timeNow func() time.Time, parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher, extractor custom.Extractor, credentials settings.Updater, ) *Providers { - randSource := rand.NewSource(timeNow().UnixNano()) - //nolint:lll providerNameToProvider := map[string]Provider{ - providers.Airvpn: airvpn.New(storage, randSource, client), + providers.Airvpn: airvpn.New(storage, client), providers.Custom: custom.New(extractor), - providers.Cyberghost: cyberghost.New(storage, randSource, updaterWarner, parallelResolver), - providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver), - providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.HideMyAss: hidemyass.New(storage, randSource, client, updaterWarner, parallelResolver), - providers.Ipvanish: ipvanish.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.Ivpn: ivpn.New(storage, randSource, client, updaterWarner, parallelResolver), - providers.Mullvad: mullvad.New(storage, randSource, client), - providers.Nordvpn: nordvpn.New(storage, randSource, client, updaterWarner), - providers.Perfectprivacy: perfectprivacy.New(storage, randSource, unzipper, updaterWarner), - providers.Privado: privado.New(storage, randSource, client, updaterWarner), - providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client), - providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword), - providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver), - providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver), - providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver), - providers.Torguard: torguard.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.VPNSecure: vpnsecure.New(storage, randSource, client, updaterWarner, parallelResolver), - providers.VPNUnlimited: vpnunlimited.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.Vyprvpn: vyprvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.Windscribe: windscribe.New(storage, randSource, client, updaterWarner), + providers.Cyberghost: cyberghost.New(storage, updaterWarner, parallelResolver), + providers.Expressvpn: expressvpn.New(storage, unzipper, updaterWarner, parallelResolver), + providers.Fastestvpn: fastestvpn.New(storage, client, updaterWarner, parallelResolver), + providers.Giganews: giganews.New(storage, unzipper, updaterWarner, parallelResolver), + providers.HideMyAss: hidemyass.New(storage, client, updaterWarner, parallelResolver), + providers.Ipvanish: ipvanish.New(storage, unzipper, updaterWarner, parallelResolver), + providers.Ivpn: ivpn.New(storage, client, updaterWarner, parallelResolver), + providers.Mullvad: mullvad.New(storage, client), + providers.Nordvpn: nordvpn.New(storage, client, updaterWarner), + providers.Perfectprivacy: perfectprivacy.New(storage, unzipper, updaterWarner), + providers.Privado: privado.New(storage, client, updaterWarner), + providers.PrivateInternetAccess: privateinternetaccess.New(storage, timeNow, client), + providers.Privatevpn: privatevpn.New(storage, unzipper, updaterWarner, parallelResolver), + providers.Protonvpn: protonvpn.New(storage, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword), + providers.Purevpn: purevpn.New(storage, ipFetcher, unzipper, updaterWarner, parallelResolver), + providers.SlickVPN: slickvpn.New(storage, client, updaterWarner, parallelResolver), + providers.Surfshark: surfshark.New(storage, client, unzipper, updaterWarner, parallelResolver), + providers.Torguard: torguard.New(storage, unzipper, updaterWarner, parallelResolver), + providers.VPNSecure: vpnsecure.New(storage, client, updaterWarner, parallelResolver), + providers.VPNUnlimited: vpnunlimited.New(storage, unzipper, updaterWarner, parallelResolver), + providers.Vyprvpn: vyprvpn.New(storage, unzipper, updaterWarner, parallelResolver), + providers.Windscribe: windscribe.New(storage, client, updaterWarner), } targetLength := len(providers.AllWithCustom()) diff --git a/internal/provider/purevpn/connection.go b/internal/provider/purevpn/connection.go index 6f9bf61a..08a7b36f 100644 --- a/internal/provider/purevpn/connection.go +++ b/internal/provider/purevpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(80, 53, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/purevpn/provider.go b/internal/provider/purevpn/provider.go index f95d16c2..40d3f884 100644 --- a/internal/provider/purevpn/provider.go +++ b/internal/provider/purevpn/provider.go @@ -1,26 +1,24 @@ package purevpn import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/purevpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - ipFetcher common.IPFetcher, unzipper common.Unzipper, +func New(storage common.Storage, ipFetcher common.IPFetcher, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/slickvpn/connection.go b/internal/provider/slickvpn/connection.go index 3c30e883..e881bcb5 100644 --- a/internal/provider/slickvpn/connection.go +++ b/internal/provider/slickvpn/connection.go @@ -10,5 +10,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support connection models.Connection, err error, ) { defaults := utils.NewConnectionDefaults(443, 443, 0) //nolint:mnd - return utils.GetConnection(p.Name(), p.storage, selection, defaults, ipv6Supported, p.randSource) + return utils.GetConnection(p.Name(), p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/slickvpn/provider.go b/internal/provider/slickvpn/provider.go index a41d60a8..3f77a758 100644 --- a/internal/provider/slickvpn/provider.go +++ b/internal/provider/slickvpn/provider.go @@ -1,27 +1,26 @@ package slickvpn import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/slickvpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner, parallelResolver), } } diff --git a/internal/provider/surfshark/connection.go b/internal/provider/surfshark/connection.go index 6c15156c..dae42028 100644 --- a/internal/provider/surfshark/connection.go +++ b/internal/provider/surfshark/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(1443, 1194, 51820) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/surfshark/provider.go b/internal/provider/surfshark/provider.go index ef6a6d48..f3bb1773 100644 --- a/internal/provider/surfshark/provider.go +++ b/internal/provider/surfshark/provider.go @@ -1,27 +1,26 @@ package surfshark import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/surfshark/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, unzipper common.Unzipper, updaterWarner common.Warner, - parallelResolver common.ParallelResolver, +func New(storage common.Storage, client *http.Client, unzipper common.Unzipper, + updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/torguard/connection.go b/internal/provider/torguard/connection.go index 0a9d3aea..8d8ea714 100644 --- a/internal/provider/torguard/connection.go +++ b/internal/provider/torguard/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(1912, 1912, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/torguard/provider.go b/internal/provider/torguard/provider.go index 27f694b4..573e1d9d 100644 --- a/internal/provider/torguard/provider.go +++ b/internal/provider/torguard/provider.go @@ -1,26 +1,24 @@ package torguard import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/torguard/updater" + "github.com/qdm12/gluetun/internal/provider/utils" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/utils/connection.go b/internal/provider/utils/connection.go index 7e10d739..35a06d74 100644 --- a/internal/provider/utils/connection.go +++ b/internal/provider/utils/connection.go @@ -2,7 +2,6 @@ package utils import ( "fmt" - "math/rand" "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants/vpn" @@ -35,7 +34,8 @@ func GetConnection(provider string, selection settings.ServerSelection, defaults ConnectionDefaults, ipv6Supported bool, - randSource rand.Source) ( + connPicker *ConnectionPicker, +) ( connection models.Connection, err error, ) { servers, err := storage.FilterServers(provider, selection) @@ -75,5 +75,5 @@ func GetConnection(provider string, } } - return pickConnection(connections, selection, randSource) + return pickConnection(connections, selection, connPicker) } diff --git a/internal/provider/utils/connection_test.go b/internal/provider/utils/connection_test.go index beccbe2b..f9f015bc 100644 --- a/internal/provider/utils/connection_test.go +++ b/internal/provider/utils/connection_test.go @@ -183,6 +183,7 @@ func Test_GetConnection(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) + connPicker := NewConnectionPicker() storage := common.NewMockStorage(ctrl) storage.EXPECT(). @@ -191,7 +192,7 @@ func Test_GetConnection(t *testing.T) { connection, err := GetConnection(testCase.provider, storage, testCase.serverSelection, testCase.defaults, testCase.ipv6Supported, - testCase.randSource) + connPicker) assert.Equal(t, testCase.connection, connection) if testCase.errMessage != "" { diff --git a/internal/provider/utils/pick.go b/internal/provider/utils/pick.go index de3135ac..e6ae0415 100644 --- a/internal/provider/utils/pick.go +++ b/internal/provider/utils/pick.go @@ -1,23 +1,86 @@ package utils import ( + "encoding/binary" "errors" "fmt" - "math/rand" + "hash/fnv" "net/netip" + "sync" "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants/vpn" "github.com/qdm12/gluetun/internal/models" ) +// ConnectionPicker is a struct that holds the state of the connection pool cycler. +type ConnectionPicker struct { + mutex sync.Mutex + fingerprint uint64 + nextIndex uint +} + +func NewConnectionPicker() *ConnectionPicker { + return &ConnectionPicker{} +} + +func (c *ConnectionPicker) pickConnection(connections []models.Connection, +) models.Connection { + fingerprint := fingerprintPool(connections) + + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.fingerprint != fingerprint || c.nextIndex >= uint(len(connections)) { + c.fingerprint = fingerprint + c.nextIndex = 0 + } + + connection := connections[c.nextIndex] + c.nextIndex++ + if c.nextIndex >= uint(len(connections)) { + c.nextIndex = 0 + } + + return connection +} + +func fingerprintPool(connections []models.Connection) uint64 { + hasher := fnv.New64a() + + for _, connection := range connections { + _, _ = hasher.Write([]byte(connection.Type)) + _, _ = hasher.Write([]byte("|")) + _, _ = hasher.Write(connection.IP.AsSlice()) + _, _ = hasher.Write([]byte("|")) + _, _ = hasher.Write(binary.BigEndian.AppendUint16(nil, connection.Port)) + _, _ = hasher.Write([]byte("|")) + _, _ = hasher.Write([]byte(connection.Protocol)) + _, _ = hasher.Write([]byte("|")) + _, _ = hasher.Write([]byte(connection.Hostname)) + _, _ = hasher.Write([]byte("|")) + _, _ = hasher.Write([]byte(connection.PubKey)) + _, _ = hasher.Write([]byte("|")) + _, _ = hasher.Write([]byte(connection.ServerName)) + _, _ = hasher.Write([]byte("|")) + if connection.PortForward { + _, _ = hasher.Write([]byte("1")) + } else { + _, _ = hasher.Write([]byte("0")) + } + _, _ = hasher.Write([]byte("\n")) + } + + return hasher.Sum64() +} + // pickConnection picks a connection from a pool of connections. // If the VPN protocol is Wireguard and the target IP is set, // it finds the connection corresponding to this target IP. -// Otherwise, it picks a random connection from the pool of connections +// Otherwise, it cycles through the pool of connections. // and sets the target IP address as the IP if this one is set. func pickConnection(connections []models.Connection, - selection settings.ServerSelection, randSource rand.Source) ( + selection settings.ServerSelection, picker *ConnectionPicker) ( connection models.Connection, err error, ) { if len(connections) == 0 { @@ -40,7 +103,7 @@ func pickConnection(connections []models.Connection, return getTargetIPConnection(connections, targetIP) } - connection = pickRandomConnection(connections, randSource) + connection = picker.pickConnection(connections) if targetIPSet { connection.IP = targetIP } @@ -48,12 +111,6 @@ func pickConnection(connections []models.Connection, return connection, nil } -func pickRandomConnection(connections []models.Connection, - source rand.Source, -) models.Connection { - return connections[rand.New(source).Intn(len(connections))] //nolint:gosec -} - func getTargetIPConnection(connections []models.Connection, targetIP netip.Addr, ) (connection models.Connection, err error) { diff --git a/internal/provider/utils/pick_test.go b/internal/provider/utils/pick_test.go index 83380684..563c2d33 100644 --- a/internal/provider/utils/pick_test.go +++ b/internal/provider/utils/pick_test.go @@ -1,26 +1,137 @@ package utils import ( - "math/rand" + "net/netip" "testing" + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants/vpn" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" ) -func Test_pickRandomConnection(t *testing.T) { +func Test_ConnectionPicker_pickConnection(t *testing.T) { t.Parallel() - connections := []models.Connection{ - {Port: 1}, {Port: 2}, {Port: 3}, {Port: 4}, + + picker := NewConnectionPicker() + + poolA := []models.Connection{ + {Port: 1}, {Port: 2}, {Port: 3}, } - source := rand.NewSource(0) + connection := picker.pickConnection(poolA) + assert.Equal(t, models.Connection{Port: 1}, connection) - connection := pickRandomConnection(connections, source) - assert.Equal(t, models.Connection{Port: 3}, connection) - - connection = pickRandomConnection(connections, source) - assert.Equal(t, models.Connection{Port: 3}, connection) - - connection = pickRandomConnection(connections, source) + connection = picker.pickConnection(poolA) assert.Equal(t, models.Connection{Port: 2}, connection) + + connection = picker.pickConnection(poolA) + assert.Equal(t, models.Connection{Port: 3}, connection) + + connection = picker.pickConnection(poolA) + assert.Equal(t, models.Connection{Port: 1}, connection) + + poolB := []models.Connection{ + {Port: 10}, {Port: 20}, + } + connection = picker.pickConnection(poolB) + assert.Equal(t, models.Connection{Port: 10}, connection) + + connection = picker.pickConnection(poolB) + assert.Equal(t, models.Connection{Port: 20}, connection) + + connection = picker.pickConnection(poolB) + assert.Equal(t, models.Connection{Port: 10}, connection) +} + +func Test_pickConnection(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + connections []models.Connection + selection settings.ServerSelection + connection1 models.Connection + connection2 models.Connection + errMessage string + }{ + "empty_connections": { + errMessage: "no connection to pick from", + }, + "openvpn_cycles": { + connections: []models.Connection{ + {Type: vpn.OpenVPN, Port: 1, Hostname: "one"}, + {Type: vpn.OpenVPN, Port: 2, Hostname: "two"}, + }, + selection: settings.ServerSelection{VPN: vpn.OpenVPN}, + connection1: models.Connection{ + Type: vpn.OpenVPN, Port: 1, + Hostname: "one", + }, + connection2: models.Connection{ + Type: vpn.OpenVPN, Port: 2, + Hostname: "two", + }, + }, + "openvpn_endpoint_ip_overrides_cycle_pick": { + connections: []models.Connection{ + {Type: vpn.OpenVPN, Hostname: "one", IP: netip.AddrFrom4([4]byte{1, 1, 1, 1})}, + {Type: vpn.OpenVPN, Hostname: "two", IP: netip.AddrFrom4([4]byte{2, 2, 2, 2})}, + }, + selection: settings.ServerSelection{ + VPN: vpn.OpenVPN, + OpenVPN: settings.OpenVPNSelection{ + EndpointIP: netip.AddrFrom4([4]byte{9, 9, 9, 9}), + }, + }, + connection1: models.Connection{ + Type: vpn.OpenVPN, Hostname: "one", + IP: netip.AddrFrom4([4]byte{9, 9, 9, 9}), + }, + connection2: models.Connection{ + Type: vpn.OpenVPN, Hostname: "two", + IP: netip.AddrFrom4([4]byte{9, 9, 9, 9}), + }, + }, + "wireguard_endpoint_ip_picks_target": { + connections: []models.Connection{ + {Type: vpn.Wireguard, Hostname: "one", IP: netip.AddrFrom4([4]byte{1, 1, 1, 1})}, + {Type: vpn.Wireguard, Hostname: "two", IP: netip.AddrFrom4([4]byte{2, 2, 2, 2})}, + }, + selection: settings.ServerSelection{ + VPN: vpn.Wireguard, + Wireguard: settings.WireguardSelection{ + EndpointIP: netip.AddrFrom4([4]byte{2, 2, 2, 2}), + }, + }, + connection1: models.Connection{ + Type: vpn.Wireguard, Hostname: "two", + IP: netip.AddrFrom4([4]byte{2, 2, 2, 2}), + }, + connection2: models.Connection{ + Type: vpn.Wireguard, Hostname: "two", + IP: netip.AddrFrom4([4]byte{2, 2, 2, 2}), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + connPicker := NewConnectionPicker() + + connection, err := pickConnection(testCase.connections, + testCase.selection, connPicker) + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + assert.Equal(t, models.Connection{}, connection) + return + } + assert.NoError(t, err) + assert.Equal(t, testCase.connection1, connection) + + connection, err = pickConnection(testCase.connections, + testCase.selection, connPicker) + assert.NoError(t, err) + assert.Equal(t, testCase.connection2, connection) + }) + } } diff --git a/internal/provider/vpnsecure/connection.go b/internal/provider/vpnsecure/connection.go index faa24ad4..a9d1cb1f 100644 --- a/internal/provider/vpnsecure/connection.go +++ b/internal/provider/vpnsecure/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(110, 1282, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/vpnsecure/provider.go b/internal/provider/vpnsecure/provider.go index 89cfeb00..ea203e95 100644 --- a/internal/provider/vpnsecure/provider.go +++ b/internal/provider/vpnsecure/provider.go @@ -1,27 +1,26 @@ package vpnsecure import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/vpnsecure/updater" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner, parallelResolver), } } diff --git a/internal/provider/vpnunlimited/connection.go b/internal/provider/vpnunlimited/connection.go index 43cd3405..e2cde761 100644 --- a/internal/provider/vpnunlimited/connection.go +++ b/internal/provider/vpnunlimited/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(1197, 1197, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/vpnunlimited/provider.go b/internal/provider/vpnunlimited/provider.go index 1f8b1522..372cd918 100644 --- a/internal/provider/vpnunlimited/provider.go +++ b/internal/provider/vpnunlimited/provider.go @@ -1,26 +1,24 @@ package vpnunlimited import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/vpnunlimited/updater" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/vyprvpn/connection.go b/internal/provider/vyprvpn/connection.go index 33636dbb..3107b252 100644 --- a/internal/provider/vyprvpn/connection.go +++ b/internal/provider/vyprvpn/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/vyprvpn/provider.go b/internal/provider/vyprvpn/provider.go index f9977f8a..e6055e8b 100644 --- a/internal/provider/vyprvpn/provider.go +++ b/internal/provider/vyprvpn/provider.go @@ -1,26 +1,24 @@ package vyprvpn import ( - "math/rand" - "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/vyprvpn/updater" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, +func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner, parallelResolver common.ParallelResolver, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), } } diff --git a/internal/provider/windscribe/connection.go b/internal/provider/windscribe/connection.go index 9698b728..4d8a735c 100644 --- a/internal/provider/windscribe/connection.go +++ b/internal/provider/windscribe/connection.go @@ -11,5 +11,5 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support ) { defaults := utils.NewConnectionDefaults(443, 1194, 1194) //nolint:mnd return utils.GetConnection(p.Name(), - p.storage, selection, defaults, ipv6Supported, p.randSource) + p.storage, selection, defaults, ipv6Supported, p.connPicker) } diff --git a/internal/provider/windscribe/connection_test.go b/internal/provider/windscribe/connection_test.go index bff15edc..ce4f8cc4 100644 --- a/internal/provider/windscribe/connection_test.go +++ b/internal/provider/windscribe/connection_test.go @@ -2,7 +2,6 @@ package windscribe import ( "errors" - "math/rand" "net/http" "net/netip" "testing" @@ -92,11 +91,10 @@ func Test_Provider_GetConnection(t *testing.T) { storage := common.NewMockStorage(ctrl) storage.EXPECT().FilterServers(provider, testCase.selection). Return(testCase.filteredServers, testCase.storageErr) - randSource := rand.NewSource(0) client := (*http.Client)(nil) warner := (common.Warner)(nil) - provider := New(storage, randSource, client, warner) + provider := New(storage, client, warner) if testCase.panicMessage != "" { assert.PanicsWithValue(t, testCase.panicMessage, func() { diff --git a/internal/provider/windscribe/provider.go b/internal/provider/windscribe/provider.go index 66b284fb..df4d1441 100644 --- a/internal/provider/windscribe/provider.go +++ b/internal/provider/windscribe/provider.go @@ -1,26 +1,25 @@ package windscribe import ( - "math/rand" "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/windscribe/updater" ) type Provider struct { storage common.Storage - randSource rand.Source + connPicker *utils.ConnectionPicker common.Fetcher } -func New(storage common.Storage, randSource rand.Source, - client *http.Client, updaterWarner common.Warner, +func New(storage common.Storage, client *http.Client, updaterWarner common.Warner, ) *Provider { return &Provider{ storage: storage, - randSource: randSource, + connPicker: utils.NewConnectionPicker(), Fetcher: updater.New(client, updaterWarner), } }