From 1b12dcb5b4443a11f3dc9fa2c5637dbbcf0bae92 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Wed, 23 Oct 2024 09:05:32 +0000 Subject: [PATCH 1/8] initial code --- .github/ISSUE_TEMPLATE/bug.yml | 1 + .github/labels.yml | 2 + Dockerfile | 2 +- README.md | 9 +- go.mod | 4 +- go.sum | 4 +- .../settings/openvpnselection.go | 2 +- internal/configuration/settings/provider.go | 1 + .../settings/wireguardselection.go | 18 +- internal/constants/providers/providers.go | 2 + internal/models/server.go | 2 + internal/provider/ovpn/connection.go | 15 ++ internal/provider/ovpn/connection_test.go | 126 +++++++++++ internal/provider/ovpn/openvpnconf.go | 23 ++ internal/provider/ovpn/provider.go | 28 +++ internal/provider/ovpn/updater/api.go | 183 ++++++++++++++++ internal/provider/ovpn/updater/api_test.go | 117 ++++++++++ .../provider/ovpn/updater/roundtrip_test.go | 9 + internal/provider/ovpn/updater/servers.go | 71 ++++++ .../provider/ovpn/updater/servers_test.go | 204 ++++++++++++++++++ internal/provider/ovpn/updater/updater.go | 15 ++ internal/provider/providers.go | 2 + internal/provider/utils/connection.go | 5 +- internal/provider/utils/port.go | 17 +- internal/provider/utils/port_test.go | 45 ++++ internal/storage/filter.go | 23 ++ internal/storage/formatting.go | 11 +- internal/storage/hardcoded.go | 3 +- internal/storage/hardcoded_test.go | 5 +- 29 files changed, 927 insertions(+), 22 deletions(-) create mode 100644 internal/provider/ovpn/connection.go create mode 100644 internal/provider/ovpn/connection_test.go create mode 100644 internal/provider/ovpn/openvpnconf.go create mode 100644 internal/provider/ovpn/provider.go create mode 100644 internal/provider/ovpn/updater/api.go create mode 100644 internal/provider/ovpn/updater/api_test.go create mode 100644 internal/provider/ovpn/updater/roundtrip_test.go create mode 100644 internal/provider/ovpn/updater/servers.go create mode 100644 internal/provider/ovpn/updater/servers_test.go create mode 100644 internal/provider/ovpn/updater/updater.go diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9e4f95b6d..3c3963ca7 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -56,6 +56,7 @@ body: - IVPN - Mullvad - NordVPN + - OVPN - Privado - Private Internet Access - PrivateVPN diff --git a/.github/labels.yml b/.github/labels.yml index 51486f96b..647d6ee89 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -64,6 +64,8 @@ color: "cfe8d4" - name: "☁️ NordVPN" color: "cfe8d4" +- name: "☁️ OVPN" + color: "cfe8d4" - name: "☁️ Perfect Privacy" color: "cfe8d4" - name: "☁️ PIA" diff --git a/Dockerfile b/Dockerfile index 3b7d68e82..9160c4a98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -186,7 +186,7 @@ ENV VPN_SERVICE_PROVIDER=pia \ # # ProtonVPN only: SECURE_CORE_ONLY= \ TOR_ONLY= \ - # # Surfshark only: + # # Surfshark and ovpn only: MULTIHOP_ONLY= \ # # VPN Secure only: PREMIUM_ONLY= \ diff --git a/README.md b/README.md index 8e446faa2..2ace59580 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,14 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers ## Features - Based on Alpine 3.23 for a small Docker image of 43.1MB -- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad** (Wireguard only), **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers +- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad** (Wireguard only), **NordVPN**, **Ovpn**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers - Supports OpenVPN for all providers listed - Supports Wireguard both kernelspace and userspace - - For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe** + - For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Ovpn**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe** - For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - - More in progress, see [#134](https://github.com/passteque/gluetun/issues/134) + +- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) +- More in progress, see [#134](https://github.com/passteque/gluetun/issues/134) - Supports AmneziaWG only with the custom provider for now - DNS over TLS baked in with service provider(s) of your choice - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours diff --git a/go.mod b/go.mod index f7a570e99..e07fffb07 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,10 @@ require ( github.com/klauspost/pgzip v1.2.6 github.com/mdlayher/genetlink v1.4.0 github.com/mdlayher/netlink v1.9.0 + github.com/miekg/dns v1.1.62 github.com/pelletier/go-toml/v2 v2.2.4 github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a - github.com/qdm12/gluetun-servers v0.1.0 + github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 github.com/qdm12/gosettings v0.4.4 github.com/qdm12/goshutdown v0.3.0 @@ -47,7 +48,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mdlayher/socket v0.6.0 // indirect - github.com/miekg/dns v1.1.62 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index e1b8807b3..805ed4a92 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE= -github.com/qdm12/gluetun-servers v0.1.0 h1:w9JLghKZwI0Gzpp9p5rNANgEYUUZ1dxdxsG6NKIojaY= -github.com/qdm12/gluetun-servers v0.1.0/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8= +github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c h1:rPgmhMt9uOq6pdPr8k0sXUnZ0fjTBw+yUOaIl0ttFu4= +github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg= github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4= diff --git a/internal/configuration/settings/openvpnselection.go b/internal/configuration/settings/openvpnselection.go index 268e2032f..c0dacb224 100644 --- a/internal/configuration/settings/openvpnselection.go +++ b/internal/configuration/settings/openvpnselection.go @@ -70,7 +70,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) { switch vpnProvider { // no restriction on port case providers.Custom, providers.Cyberghost, providers.HideMyAss, - providers.Privatevpn, providers.Torguard: + providers.Ovpn, providers.Privatevpn, providers.Torguard: // no custom port allowed case providers.Expressvpn, providers.Fastestvpn, providers.Giganews, providers.Ipvanish, diff --git a/internal/configuration/settings/provider.go b/internal/configuration/settings/provider.go index ccc3acac3..8bcc9e1a4 100644 --- a/internal/configuration/settings/provider.go +++ b/internal/configuration/settings/provider.go @@ -49,6 +49,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet providers.Ivpn, providers.Mullvad, providers.Nordvpn, + providers.Ovpn, providers.Protonvpn, providers.Surfshark, providers.Windscribe, diff --git a/internal/configuration/settings/wireguardselection.go b/internal/configuration/settings/wireguardselection.go index 301893cf1..c7afea61c 100644 --- a/internal/configuration/settings/wireguardselection.go +++ b/internal/configuration/settings/wireguardselection.go @@ -5,6 +5,7 @@ import ( "fmt" "net/netip" + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gosettings" "github.com/qdm12/gosettings/reader" @@ -22,7 +23,7 @@ type WireguardSelection struct { // It can never be the zero value in the internal state. EndpointIP netip.Addr `json:"endpoint_ip"` // EndpointPort is a the server port to use for the VPN server. - // It is optional for VPN providers IVPN, Mullvad, Surfshark + // It is optional for VPN providers IVPN, Mullvad, Ovpn, Surfshark // and Windscribe, and compulsory for the others. // When optional, it can be set to 0 to indicate not use // a custom endpoint port. It cannot be nil in the internal @@ -40,8 +41,9 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) { // Validate EndpointIP switch vpnProvider { case providers.Airvpn, providers.Fastestvpn, providers.Ivpn, - providers.Mullvad, providers.Nordvpn, providers.Protonvpn, - providers.Surfshark, providers.Windscribe: + providers.Mullvad, providers.Nordvpn, providers.Ovpn, + providers.Protonvpn, providers.Surfshark, + providers.Windscribe: // endpoint IP addresses are baked in case providers.Custom: if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() { @@ -63,12 +65,16 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) { if *w.EndpointPort != 0 { return errors.New("endpoint port is set") } - case providers.Airvpn, providers.Ivpn, providers.Mullvad, providers.Windscribe: + case providers.Airvpn, providers.Ivpn, providers.Mullvad, + providers.Ovpn, providers.Windscribe: // EndpointPort is optional and can be 0 if *w.EndpointPort == 0 { break // no custom endpoint port set } - if vpnProvider == providers.Mullvad { + if helpers.IsOneOf(vpnProvider, + providers.Mullvad, + providers.Ovpn, + ) { break // no restriction on custom endpoint port value } var allowed []uint16 @@ -92,7 +98,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) { // Validate PublicKey switch vpnProvider { case providers.Fastestvpn, providers.Ivpn, providers.Mullvad, - providers.Surfshark, providers.Windscribe: + providers.Ovpn, providers.Surfshark, providers.Windscribe: // public keys are baked in case providers.Custom: if w.PublicKey == "" { diff --git a/internal/constants/providers/providers.go b/internal/constants/providers/providers.go index b8fdb51f4..71cf64a72 100644 --- a/internal/constants/providers/providers.go +++ b/internal/constants/providers/providers.go @@ -15,6 +15,7 @@ const ( Ivpn = "ivpn" Mullvad = "mullvad" Nordvpn = "nordvpn" + Ovpn = "ovpn" Perfectprivacy = "perfect privacy" Privado = "privado" PrivateInternetAccess = "private internet access" @@ -43,6 +44,7 @@ func All() []string { Ivpn, Mullvad, Nordvpn, + Ovpn, Perfectprivacy, Privado, PrivateInternetAccess, diff --git a/internal/models/server.go b/internal/models/server.go index 8c52ad663..2eb190ccb 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -36,6 +36,8 @@ type Server struct { PortForward bool `json:"port_forward,omitempty"` Keep bool `json:"keep,omitempty"` IPs []netip.Addr `json:"ips,omitempty"` + PortsTCP []uint16 `json:"ports_tcp,omitempty"` + PortsUDP []uint16 `json:"ports_udp,omitempty"` } func (s *Server) HasMinimumInformation() (err error) { diff --git a/internal/provider/ovpn/connection.go b/internal/provider/ovpn/connection.go new file mode 100644 index 000000000..f04c224ba --- /dev/null +++ b/internal/provider/ovpn/connection.go @@ -0,0 +1,15 @@ +package ovpn + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( + connection models.Connection, err error, +) { + defaults := utils.NewConnectionDefaults(443, 1194, 9929) //nolint:mnd + return utils.GetConnection(p.Name(), + p.storage, selection, defaults, ipv6Supported, p.connPicker) +} diff --git a/internal/provider/ovpn/connection_test.go b/internal/provider/ovpn/connection_test.go new file mode 100644 index 000000000..9ccae3f4b --- /dev/null +++ b/internal/provider/ovpn/connection_test.go @@ -0,0 +1,126 @@ +package ovpn + +import ( + "errors" + "net/http" + "net/netip" + "testing" + + "github.com/golang/mock/gomock" + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/constants/providers" + "github.com/qdm12/gluetun/internal/constants/vpn" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/common" + "github.com/stretchr/testify/assert" +) + +func Test_Provider_GetConnection(t *testing.T) { + t.Parallel() + + const provider = providers.Ovpn + + errTest := errors.New("test error") + + testCases := map[string]struct { + filteredServers []models.Server + storageErr error + selection settings.ServerSelection + ipv6Supported bool + connection models.Connection + errWrapped error + errMessage string + }{ + "error": { + storageErr: errTest, + errWrapped: errTest, + errMessage: "filtering servers: test error", + }, + "default_openvpn_tcp_port": { + filteredServers: []models.Server{ + {IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}}, + }, + selection: settings.ServerSelection{ + OpenVPN: settings.OpenVPNSelection{ + Protocol: constants.TCP, + }, + }.WithDefaults(provider), + connection: models.Connection{ + Type: vpn.OpenVPN, + IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}), + Port: 443, + Protocol: constants.TCP, + }, + }, + "default_openvpn_udp_port": { + filteredServers: []models.Server{ + {IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}}, + }, + selection: settings.ServerSelection{ + OpenVPN: settings.OpenVPNSelection{ + Protocol: constants.UDP, + }, + }.WithDefaults(provider), + connection: models.Connection{ + Type: vpn.OpenVPN, + IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}), + Port: 1194, + Protocol: constants.UDP, + }, + }, + "default_wireguard_port": { + filteredServers: []models.Server{ + {IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x"}, + }, + selection: settings.ServerSelection{ + VPN: vpn.Wireguard, + }.WithDefaults(provider), + connection: models.Connection{ + Type: vpn.Wireguard, + IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}), + Port: 9929, + Protocol: constants.UDP, + PubKey: "x", + }, + }, + "default_multihop_port": { + filteredServers: []models.Server{ + {IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x", PortsUDP: []uint16{30044}}, + }, + selection: settings.ServerSelection{ + VPN: vpn.Wireguard, + }.WithDefaults(provider), + connection: models.Connection{ + Type: vpn.Wireguard, + IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}), + Port: 30044, + Protocol: constants.UDP, + PubKey: "x", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + storage := common.NewMockStorage(ctrl) + storage.EXPECT().FilterServers(provider, testCase.selection). + Return(testCase.filteredServers, testCase.storageErr) + + client := (*http.Client)(nil) + provider := New(storage, client) + + connection, err := provider.GetConnection(testCase.selection, testCase.ipv6Supported) + + assert.ErrorIs(t, err, testCase.errWrapped) + if testCase.errWrapped != nil { + assert.EqualError(t, err, testCase.errMessage) + } + + assert.Equal(t, testCase.connection, connection) + }) + } +} diff --git a/internal/provider/ovpn/openvpnconf.go b/internal/provider/ovpn/openvpnconf.go new file mode 100644 index 000000000..3e09d54e0 --- /dev/null +++ b/internal/provider/ovpn/openvpnconf.go @@ -0,0 +1,23 @@ +package ovpn + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants/openvpn" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (p *Provider) OpenVPNConfig(connection models.Connection, + settings settings.OpenVPN, ipv6Supported bool, +) (lines []string) { + providerSettings := utils.OpenVPNProviderSettings{ + AuthUserPass: true, + Ciphers: []string{ + openvpn.Chacha20Poly1305, + openvpn.AES256gcm, + openvpn.AES256cbc, + openvpn.AES128gcm, + }, + } + return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported) +} diff --git a/internal/provider/ovpn/provider.go b/internal/provider/ovpn/provider.go new file mode 100644 index 000000000..dd42ee54b --- /dev/null +++ b/internal/provider/ovpn/provider.go @@ -0,0 +1,28 @@ +package ovpn + +import ( + "net/http" + + "github.com/qdm12/gluetun/internal/constants/providers" + "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/provider/ovpn/updater" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +type Provider struct { + storage common.Storage + connPicker *utils.ConnectionPicker + common.Fetcher +} + +func New(storage common.Storage, client *http.Client) *Provider { + return &Provider{ + storage: storage, + connPicker: utils.NewConnectionPicker(), + Fetcher: updater.New(client), + } +} + +func (p *Provider) Name() string { + return providers.Ovpn +} diff --git a/internal/provider/ovpn/updater/api.go b/internal/provider/ovpn/updater/api.go new file mode 100644 index 000000000..d838d2e01 --- /dev/null +++ b/internal/provider/ovpn/updater/api.go @@ -0,0 +1,183 @@ +package updater + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/netip" + "strings" +) + +type apiData struct { + Success bool `json:"success"` + DataCenters []apiDataCenter `json:"datacenters"` +} + +type apiDataCenter struct { + City string `json:"city"` + CountryName string `json:"country_name"` + Servers []apiServer `json:"servers"` +} + +type apiServer struct { + IP netip.Addr `json:"ip"` + Ptr string `json:"ptr"` // hostname + Online bool `json:"online"` + PublicKey string `json:"public_key"` + WireguardPorts []uint16 `json:"wireguard_ports"` + MultiHopOpenvpnPort uint16 `json:"multihop_openvpn_port"` + MultiHopWireguardPort uint16 `json:"multihop_wireguard_port"` +} + +func fetchAPI(ctx context.Context, client *http.Client) ( + data apiData, err error, +) { + const url = "https://www.ovpn.com/v2/api/client/entry" + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return data, err + } + + response, err := client.Do(request) + if err != nil { + return data, err + } + + if response.StatusCode != http.StatusOK { + _ = response.Body.Close() + return data, fmt.Errorf("HTTP response status code is not OK: %d %s", + response.StatusCode, response.Status) + } + + decoder := json.NewDecoder(response.Body) + err = decoder.Decode(&data) + if err != nil { + _ = response.Body.Close() + return data, fmt.Errorf("decoding response body: %w", err) + } + + err = response.Body.Close() + if err != nil { + return data, fmt.Errorf("closing response body: %w", err) + } + + return data, nil +} + +var ( + ErrCityNotSet = errors.New("city is not set") + ErrCountryNameNotSet = errors.New("country name is not set") + ErrServersNotSet = errors.New("servers array is not set") +) + +func (a *apiDataCenter) validate() (err error) { + conditionalErrors := []conditionalError{ + {err: ErrCityNotSet, condition: a.City == ""}, + {err: ErrCountryNameNotSet, condition: a.CountryName == ""}, + {err: ErrServersNotSet, condition: len(a.Servers) == 0}, + } + err = collectErrors(conditionalErrors) + if err != nil { + var dataCenterSetFields []string + if a.CountryName != "" { + dataCenterSetFields = append(dataCenterSetFields, a.CountryName) + } + if a.City != "" { + dataCenterSetFields = append(dataCenterSetFields, a.City) + } + if len(dataCenterSetFields) == 0 { + return err + } + return fmt.Errorf("data center %s: %w", + strings.Join(dataCenterSetFields, ", "), err) + } + + for i, server := range a.Servers { + err = server.validate() + if err != nil { + return fmt.Errorf("datacenter %s, %s: server %d of %d: %w", + a.CountryName, a.City, i+1, len(a.Servers), err) + } + } + + return nil +} + +var ( + ErrIPFieldNotValid = errors.New("ip address is not set") + ErrHostnameFieldNotSet = errors.New("hostname field is not set") + ErrPublicKeyFieldNotSet = errors.New("public key field is not set") + ErrWireguardPortsNotSet = errors.New("wireguard ports array is not set") + ErrWireguardPortNotDefault = errors.New("wireguard port is not the default 9929") + ErrMultiHopOpenVPNPortNotSet = errors.New("multihop OpenVPN port is not set") + ErrMultiHopWireguardPortNotSet = errors.New("multihop WireGuard port is not set") +) + +func (a *apiServer) validate() (err error) { + const defaultWireguardPort = 9929 + conditionalErrors := []conditionalError{ + {err: ErrIPFieldNotValid, condition: !a.IP.IsValid()}, + {err: ErrHostnameFieldNotSet, condition: a.Ptr == ""}, + {err: ErrPublicKeyFieldNotSet, condition: a.PublicKey == ""}, + {err: ErrWireguardPortsNotSet, condition: len(a.WireguardPorts) == 0}, + { + err: ErrWireguardPortNotDefault, + condition: len(a.WireguardPorts) != 1 || a.WireguardPorts[0] != defaultWireguardPort, + }, + {err: ErrMultiHopOpenVPNPortNotSet, condition: a.MultiHopOpenvpnPort == 0}, + {err: ErrMultiHopWireguardPortNotSet, condition: a.MultiHopWireguardPort == 0}, + } + err = collectErrors(conditionalErrors) + switch { + case err == nil: + return nil + case a.Ptr != "": + return fmt.Errorf("server %s: %w", a.Ptr, err) + case a.IP.IsValid(): + return fmt.Errorf("server %s: %w", a.IP.String(), err) + default: + return err + } +} + +type conditionalError struct { + err error + condition bool +} + +type joinedError struct { + errs []error +} + +func (e *joinedError) Unwrap() []error { + return e.errs +} + +func (e *joinedError) Error() string { + errStrings := make([]string, len(e.errs)) + for i, err := range e.errs { + errStrings[i] = err.Error() + } + return strings.Join(errStrings, "; ") +} + +func collectErrors(conditionalErrors []conditionalError) (err error) { + errs := make([]error, 0, len(conditionalErrors)) + for _, conditionalError := range conditionalErrors { + if !conditionalError.condition { + continue + } + errs = append(errs, conditionalError.err) + } + + if len(errs) == 0 { + return nil + } + + return &joinedError{ + errs: errs, + } +} diff --git a/internal/provider/ovpn/updater/api_test.go b/internal/provider/ovpn/updater/api_test.go new file mode 100644 index 000000000..4d7619bf3 --- /dev/null +++ b/internal/provider/ovpn/updater/api_test.go @@ -0,0 +1,117 @@ +package updater + +import ( + "context" + "errors" + "io" + "net/http" + "net/netip" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_fetchAPI(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + responseStatus int + responseBody io.ReadCloser + data apiData + err error + }{ + "http response status not ok": { + responseStatus: http.StatusNoContent, + err: errors.New("HTTP response status code is not OK: 204 No Content"), + }, + "nil body": { + responseStatus: http.StatusOK, + err: errors.New("decoding response body: EOF"), + }, + "no server": { + responseStatus: http.StatusOK, + responseBody: io.NopCloser(strings.NewReader(`{}`)), + }, + "success": { + responseStatus: http.StatusOK, + responseBody: io.NopCloser(strings.NewReader(`{ + "success": true, + "datacenters": [ + { + "slug": "vienna", + "city": "Vienna", + "country": "AT", + "country_name": "Austria", + "pools": [ + "pool-1.prd.at.vienna.ovpn.com" + ], + "ping_address": "37.120.212.227", + "servers": [ + { + "ip": "37.120.212.227", + "ptr": "vpn44.prd.vienna.ovpn.com", + "name": "VPN44 - Vienna", + "online": true, + "load": 8, + "public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", + "wireguard_ports": [ + 9929 + ], + "multihop_openvpn_port": 20044, + "multihop_wireguard_port": 30044 + } + ] + } + ] + }`)), + data: apiData{ + Success: true, + DataCenters: []apiDataCenter{ + {CountryName: "Austria", City: "Vienna", Servers: []apiServer{ + { + IP: netip.MustParseAddr("37.120.212.227"), + Ptr: "vpn44.prd.vienna.ovpn.com", + Online: true, + PublicKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + WireguardPorts: []uint16{9929}, + MultiHopOpenvpnPort: 20044, + MultiHopWireguardPort: 30044, + }, + }}, + }, + }, + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + client := &http.Client{ + Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, r.URL.String(), "https://www.ovpn.com/v2/api/client/entry") + return &http.Response{ + StatusCode: testCase.responseStatus, + Status: http.StatusText(testCase.responseStatus), + Body: testCase.responseBody, + }, nil + }), + } + + data, err := fetchAPI(ctx, client) + + assert.Equal(t, testCase.data, data) + if testCase.err != nil { + require.Error(t, err) + assert.Equal(t, testCase.err.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/provider/ovpn/updater/roundtrip_test.go b/internal/provider/ovpn/updater/roundtrip_test.go new file mode 100644 index 000000000..9df237887 --- /dev/null +++ b/internal/provider/ovpn/updater/roundtrip_test.go @@ -0,0 +1,9 @@ +package updater + +import "net/http" + +type roundTripFunc func(r *http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +} diff --git a/internal/provider/ovpn/updater/servers.go b/internal/provider/ovpn/updater/servers.go new file mode 100644 index 000000000..9815975ba --- /dev/null +++ b/internal/provider/ovpn/updater/servers.go @@ -0,0 +1,71 @@ +package updater + +import ( + "context" + "errors" + "fmt" + "net/netip" + "sort" + + "github.com/qdm12/gluetun/internal/constants/vpn" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/common" +) + +func (u *Updater) FetchServers(ctx context.Context, minServers int) ( + servers []models.Server, err error, +) { + data, err := fetchAPI(ctx, u.client) + if err != nil { + return nil, fmt.Errorf("fetching API: %w", err) + } else if !data.Success { + return nil, errors.New("response success field is false") + } + + for dataCenterIndex, dataCenter := range data.DataCenters { + err = dataCenter.validate() + if err != nil { + return nil, fmt.Errorf("validating data center %d of %d: %w", + dataCenterIndex+1, len(data.DataCenters), err) + } + + for _, apiServer := range dataCenter.Servers { + if !apiServer.Online { + continue + } + + baseServer := models.Server{ + Country: dataCenter.CountryName, + City: dataCenter.City, + Hostname: apiServer.Ptr, + IPs: []netip.Addr{apiServer.IP}, + } + openVPNServer := baseServer + openVPNServer.VPN = vpn.OpenVPN + openVPNServer.TCP = true + openVPNServer.UDP = true + multiHopOpenVPNServer := openVPNServer + multiHopOpenVPNServer.MultiHop = true + multiHopOpenVPNServer.PortsTCP = []uint16{apiServer.MultiHopOpenvpnPort} + multiHopOpenVPNServer.PortsUDP = []uint16{apiServer.MultiHopOpenvpnPort} + servers = append(servers, openVPNServer, multiHopOpenVPNServer) + + wireguardServer := baseServer + wireguardServer.VPN = vpn.Wireguard + wireguardServer.WgPubKey = apiServer.PublicKey + multiHopWireguardServer := wireguardServer + multiHopWireguardServer.MultiHop = true + multiHopWireguardServer.PortsUDP = []uint16{apiServer.MultiHopWireguardPort} + servers = append(servers, wireguardServer, multiHopWireguardServer) + } + } + + if len(servers) < minServers { + return nil, fmt.Errorf("%w: %d and expected at least %d", + common.ErrNotEnoughServers, len(servers), minServers) + } + + sort.Sort(models.SortableServers(servers)) + + return servers, nil +} diff --git a/internal/provider/ovpn/updater/servers_test.go b/internal/provider/ovpn/updater/servers_test.go new file mode 100644 index 000000000..eda9d60ad --- /dev/null +++ b/internal/provider/ovpn/updater/servers_test.go @@ -0,0 +1,204 @@ +package updater + +import ( + "context" + "io" + "net/http" + "net/netip" + "strings" + "testing" + + "github.com/qdm12/gluetun/internal/constants/vpn" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/common" + "github.com/stretchr/testify/assert" +) + +func Test_Updater_FetchServers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + // Inputs + minServers int + + // From API + responseStatus int + responseBody string + + // Output + servers []models.Server + errWrapped error + errMessage string + }{ + "http_response_error": { + responseStatus: http.StatusNoContent, + errMessage: "fetching API: HTTP response status code is not OK: 204 No Content", + }, + "success_field_false": { + responseStatus: http.StatusOK, + responseBody: `{"success": false}`, + errMessage: "response success field is false", + }, + "validation_failed": { + responseStatus: http.StatusOK, + responseBody: `{ + "success": true, + "datacenters": [ + { + "city": "Vienna", + "servers": [ + {} + ] + } + ] +}`, + errMessage: "validating data center 1 of 1: data center Vienna: country name is not set", + }, + "not_enough_servers": { + minServers: 5, + responseStatus: http.StatusOK, + responseBody: `{ + "success": true, + "datacenters": [ + { + "city": "Vienna", + "country_name": "Austria", + "servers": [ + { + "ip": "37.120.212.227", + "ptr": "vpn44.prd.vienna.ovpn.com", + "online": true, + "public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "wireguard_ports": [9929], + "multihop_openvpn_port": 20044, + "multihop_wireguard_port": 30044 + } + ] + } + ] +}`, + errWrapped: common.ErrNotEnoughServers, + errMessage: "not enough servers found: 4 and expected at least 5", + }, + "success": { + minServers: 4, + responseBody: `{ + "success": true, + "datacenters": [ + { + "slug": "vienna", + "city": "Vienna", + "country": "AT", + "country_name": "Austria", + "pools": [ + "pool-1.prd.at.vienna.ovpn.com" + ], + "ping_address": "37.120.212.227", + "servers": [ + { + "ip": "37.120.212.227", + "ptr": "vpn44.prd.vienna.ovpn.com", + "name": "VPN44 - Vienna", + "online": true, + "load": 8, + "public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", + "wireguard_ports": [ + 9929 + ], + "multihop_openvpn_port": 20044, + "multihop_wireguard_port": 30044 + }, + { + "ip": "37.120.212.228", + "ptr": "vpn45.prd.vienna.ovpn.com", + "online": false, + "public_key": "r93LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "wireguard_ports": [9929], + "multihop_openvpn_port": 20045, + "multihop_wireguard_port": 30045 + } + ] + } + ] +}`, + responseStatus: http.StatusOK, + servers: []models.Server{ + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.OpenVPN, + UDP: true, + TCP: true, + }, + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.OpenVPN, + UDP: true, + TCP: true, + MultiHop: true, + PortsTCP: []uint16{20044}, + PortsUDP: []uint16{20044}, + }, + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.Wireguard, + WgPubKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + }, + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.Wireguard, + WgPubKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + MultiHop: true, + PortsUDP: []uint16{30044}, + }, + }, + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + client := &http.Client{ + Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, r.URL.String(), "https://www.ovpn.com/v2/api/client/entry") + return &http.Response{ + StatusCode: testCase.responseStatus, + Status: http.StatusText(testCase.responseStatus), + Body: io.NopCloser(strings.NewReader(testCase.responseBody)), + }, nil + }), + } + + updater := &Updater{ + client: client, + } + + servers, err := updater.FetchServers(ctx, testCase.minServers) + + assert.Equal(t, testCase.servers, servers) + if testCase.errMessage == "" { + assert.NoError(t, err) + } else { + assert.Contains(t, err.Error(), testCase.errMessage) + } + if testCase.errWrapped != nil { + assert.ErrorIs(t, err, testCase.errWrapped) + } + }) + } +} diff --git a/internal/provider/ovpn/updater/updater.go b/internal/provider/ovpn/updater/updater.go new file mode 100644 index 000000000..a987c0fd3 --- /dev/null +++ b/internal/provider/ovpn/updater/updater.go @@ -0,0 +1,15 @@ +package updater + +import ( + "net/http" +) + +type Updater struct { + client *http.Client +} + +func New(client *http.Client) *Updater { + return &Updater{ + client: client, + } +} diff --git a/internal/provider/providers.go b/internal/provider/providers.go index 074020966..87e616045 100644 --- a/internal/provider/providers.go +++ b/internal/provider/providers.go @@ -20,6 +20,7 @@ import ( "github.com/qdm12/gluetun/internal/provider/ivpn" "github.com/qdm12/gluetun/internal/provider/mullvad" "github.com/qdm12/gluetun/internal/provider/nordvpn" + "github.com/qdm12/gluetun/internal/provider/ovpn" "github.com/qdm12/gluetun/internal/provider/perfectprivacy" "github.com/qdm12/gluetun/internal/provider/privado" "github.com/qdm12/gluetun/internal/provider/privateinternetaccess" @@ -67,6 +68,7 @@ func NewProviders(storage Storage, timeNow func() time.Time, providers.Ivpn: ivpn.New(storage, client, updaterWarner, parallelResolver), providers.Mullvad: mullvad.New(storage, client), providers.Nordvpn: nordvpn.New(storage, client, updaterWarner), + providers.Ovpn: ovpn.New(storage, client), providers.Perfectprivacy: perfectprivacy.New(storage, unzipper, updaterWarner), providers.Privado: privado.New(storage, client, updaterWarner), providers.PrivateInternetAccess: privateinternetaccess.New(storage, timeNow, client), diff --git a/internal/provider/utils/connection.go b/internal/provider/utils/connection.go index 65bcb3cf8..ea2725744 100644 --- a/internal/provider/utils/connection.go +++ b/internal/provider/utils/connection.go @@ -52,8 +52,6 @@ func GetConnection(provider string, }) protocol := getProtocol(selection) - port := getPort(selection, defaults.OpenVPNTCPPort, - defaults.OpenVPNUDPPort, defaults.WireguardPort) connections := make([]models.Connection, 0, len(servers)) for _, server := range servers { @@ -69,6 +67,9 @@ func GetConnection(provider string, hostname = server.OvpnX509 } + port := getPort(selection, server, defaults.OpenVPNTCPPort, + defaults.OpenVPNUDPPort, defaults.WireguardPort) + connection := models.Connection{ Type: selection.VPN, IP: ip, diff --git a/internal/provider/utils/port.go b/internal/provider/utils/port.go index 1f32ef4a7..bdbea7c9b 100644 --- a/internal/provider/utils/port.go +++ b/internal/provider/utils/port.go @@ -6,29 +6,44 @@ import ( "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants/vpn" + "github.com/qdm12/gluetun/internal/models" ) -func getPort(selection settings.ServerSelection, +func getPort(selection settings.ServerSelection, server models.Server, defaultOpenVPNTCP, defaultOpenVPNUDP, defaultWireguard uint16, ) (port uint16) { switch selection.VPN { case vpn.Wireguard: customPort := *selection.Wireguard.EndpointPort if customPort > 0 { + // Note: servers filtering ensures the custom port is within the + // server ports defined if any is set. return customPort } + + if len(server.PortsUDP) > 0 { + defaultWireguard = server.PortsUDP[0] + } checkDefined("Wireguard", defaultWireguard) return defaultWireguard default: // OpenVPN customPort := *selection.OpenVPN.CustomPort if customPort > 0 { + // Note: servers filtering ensures the custom port is within the + // server ports defined if any is set. return customPort } if selection.OpenVPN.Protocol == constants.TCP { + if len(server.PortsTCP) > 0 { + defaultOpenVPNTCP = server.PortsTCP[0] + } checkDefined("OpenVPN TCP", defaultOpenVPNTCP) return defaultOpenVPNTCP } + if len(server.PortsUDP) > 0 { + defaultOpenVPNUDP = server.PortsUDP[0] + } checkDefined("OpenVPN UDP", defaultOpenVPNUDP) return defaultOpenVPNUDP } diff --git a/internal/provider/utils/port_test.go b/internal/provider/utils/port_test.go index 1967b47bc..a6f866e43 100644 --- a/internal/provider/utils/port_test.go +++ b/internal/provider/utils/port_test.go @@ -6,6 +6,7 @@ import ( "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants/vpn" + "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" ) @@ -22,6 +23,7 @@ func Test_GetPort(t *testing.T) { testCases := map[string]struct { selection settings.ServerSelection + server models.Server defaultOpenVPNTCP uint16 defaultOpenVPNUDP uint16 defaultWireguard uint16 @@ -48,6 +50,20 @@ func Test_GetPort(t *testing.T) { defaultWireguard: defaultWireguard, port: defaultOpenVPNUDP, }, + "OpenVPN_server_port_udp": { + selection: settings.ServerSelection{ + VPN: vpn.OpenVPN, + OpenVPN: settings.OpenVPNSelection{ + CustomPort: uint16Ptr(0), + Protocol: constants.UDP, + }, + }, + server: models.Server{ + PortsUDP: []uint16{1234}, + }, + defaultOpenVPNUDP: defaultOpenVPNUDP, + port: 1234, + }, "OpenVPN UDP no default port defined": { selection: settings.ServerSelection{ VPN: vpn.OpenVPN, @@ -88,6 +104,20 @@ func Test_GetPort(t *testing.T) { }, port: 1234, }, + "OpenVPN_server_port_tcp": { + selection: settings.ServerSelection{ + VPN: vpn.OpenVPN, + OpenVPN: settings.OpenVPNSelection{ + CustomPort: uint16Ptr(0), + Protocol: constants.TCP, + }, + }, + server: models.Server{ + PortsTCP: []uint16{1234}, + }, + defaultOpenVPNTCP: defaultOpenVPNTCP, + port: 1234, + }, "Wireguard": { selection: settings.ServerSelection{ VPN: vpn.Wireguard, @@ -105,6 +135,19 @@ func Test_GetPort(t *testing.T) { defaultWireguard: defaultWireguard, port: 1234, }, + "Wireguard_server_port": { + selection: settings.ServerSelection{ + VPN: vpn.Wireguard, + Wireguard: settings.WireguardSelection{ + EndpointPort: uint16Ptr(0), + }, + }, + server: models.Server{ + PortsUDP: []uint16{1234}, + }, + defaultWireguard: defaultWireguard, + port: 1234, + }, "Wireguard no default port defined": { selection: settings.ServerSelection{ VPN: vpn.Wireguard, @@ -120,6 +163,7 @@ func Test_GetPort(t *testing.T) { if testCase.panics != "" { assert.PanicsWithValue(t, testCase.panics, func() { _ = getPort(testCase.selection, + testCase.server, testCase.defaultOpenVPNTCP, testCase.defaultOpenVPNUDP, testCase.defaultWireguard) @@ -128,6 +172,7 @@ func Test_GetPort(t *testing.T) { } port := getPort(testCase.selection, + testCase.server, testCase.defaultOpenVPNTCP, testCase.defaultOpenVPNUDP, testCase.defaultWireguard) diff --git a/internal/storage/filter.go b/internal/storage/filter.go index 81d6cdbc7..4ff4d609b 100644 --- a/internal/storage/filter.go +++ b/internal/storage/filter.go @@ -3,6 +3,7 @@ package storage import ( "errors" "fmt" + "slices" "strings" "github.com/qdm12/gluetun/internal/configuration/settings" @@ -122,6 +123,10 @@ func filterServer(server models.Server, return true } + if filterByPorts(selection, server.PortsTCP) { + return true + } + // TODO filter port forward server for PIA return false @@ -165,3 +170,21 @@ func filterByProtocol(selection settings.ServerSelection, return (wantTCP && !serverTCP) || (wantUDP && !serverUDP) } } + +func filterByPorts(selection settings.ServerSelection, + serverPorts []uint16, +) (filtered bool) { + if len(serverPorts) == 0 { + return false + } + + customPort := *selection.OpenVPN.CustomPort + if selection.VPN == vpn.Wireguard { + customPort = *selection.Wireguard.EndpointPort + } + if customPort == 0 { + return false + } + + return !slices.Contains(serverPorts, customPort) +} diff --git a/internal/storage/formatting.go b/internal/storage/formatting.go index 44865a5b1..bdc4eab33 100644 --- a/internal/storage/formatting.go +++ b/internal/storage/formatting.go @@ -14,7 +14,7 @@ func commaJoin(slice []string) string { return strings.Join(slice, ", ") } -func noServerFoundError(selection settings.ServerSelection) (err error) { +func noServerFoundError(selection settings.ServerSelection) (err error) { //nolint:gocyclo var messageParts []string messageParts = append(messageParts, "VPN "+selection.VPN) @@ -155,6 +155,15 @@ func noServerFoundError(selection settings.ServerSelection) (err error) { "target ip address "+targetIP.String()) } + customPort := *selection.OpenVPN.CustomPort + if selection.VPN == vpn.Wireguard { + customPort = *selection.Wireguard.EndpointPort + } + if customPort > 0 { + messageParts = append(messageParts, + fmt.Sprintf("%s endpoint port %d", selection.VPN, customPort)) + } + message := "for " + strings.Join(messageParts, "; ") return fmt.Errorf("no server found: %s", message) diff --git a/internal/storage/hardcoded.go b/internal/storage/hardcoded.go index af75c5496..c1f246488 100644 --- a/internal/storage/hardcoded.go +++ b/internal/storage/hardcoded.go @@ -20,7 +20,8 @@ func parseHardcodedServers() (allServers models.AllServers) { filename := provider + ".json" providerFile, err := serversmodule.Files.Open(filename) if err != nil { - panic(fmt.Sprintf("reading embedded provider file %s for %s: %s", filename, provider, err)) + const rootURL = "https://github.com/qdm12/gluetun-servers/blob/main/pkg/servers" + panic(fmt.Sprintf("reading embedded provider file defined at %s/%s.json: %s", rootURL, filename, err)) } defer providerFile.Close() // no-op diff --git a/internal/storage/hardcoded_test.go b/internal/storage/hardcoded_test.go index 04a9bc0a5..b95ae757c 100644 --- a/internal/storage/hardcoded_test.go +++ b/internal/storage/hardcoded_test.go @@ -33,7 +33,10 @@ func Test_parseHardcodedServers(t *testing.T) { func Test_parseHardcodedServers_filepathsAndEmbeddedProviderFiles(t *testing.T) { t.Parallel() - hardcodedServers := parseHardcodedServers() + var hardcodedServers models.AllServers + require.NotPanics(t, func() { + hardcodedServers = parseHardcodedServers() + }) allProviders := providers.All() for _, provider := range allProviders { From 83feb533de017ac238a579667b424a49d38691c4 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Sun, 27 Oct 2024 12:53:50 +0000 Subject: [PATCH 2/8] Add missing openvpn options - CA - TLS auth - TLS crypt (for singapore) - `allow-compression asym` - `replay-window 256` - remote-cert-tls server - move aes256gcm as preferred cipher --- internal/provider/ovpn/openvpnconf.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/provider/ovpn/openvpnconf.go b/internal/provider/ovpn/openvpnconf.go index 3e09d54e0..e86793c74 100644 --- a/internal/provider/ovpn/openvpnconf.go +++ b/internal/provider/ovpn/openvpnconf.go @@ -11,12 +11,22 @@ func (p *Provider) OpenVPNConfig(connection models.Connection, settings settings.OpenVPN, ipv6Supported bool, ) (lines []string) { providerSettings := utils.OpenVPNProviderSettings{ - AuthUserPass: true, + AuthUserPass: true, + RemoteCertTLS: true, Ciphers: []string{ - openvpn.Chacha20Poly1305, openvpn.AES256gcm, openvpn.AES256cbc, openvpn.AES128gcm, + openvpn.Chacha20Poly1305, + }, + CAs: []string{ + "MIIEfTCCA2WgAwIBAgIJAK2aIWqpLj1/MA0GCSqGSIb3DQEBBQUAMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTAeFw0xNDA4MTcxODIxMjlaFw0zNDA4MTIxODIxMjlaMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMR+aP4GTuZwurZuOA2NYzMfqKyZi/TJcLEPlGTB/b4CWA9bTd8f0pHPrDAZsXIEayxxB58BIFNDNiybnbO15JN/QwlsqmA+aZX6mCSkScs/rRwasM6LDo8iGx+KmYEqAgzziONGbCMnlO+OaarXte7LhZ9X6Z/bryu4xq/i1v3raak13kXsrogtu4iDzxqJE/QhbNOi0yhCdlm5RYQjmlKGdPB9pNTgcakVI4HcngRYMzBlrGin0YkvWCdpx5FrDNeld7BSWrJMNYyvd+buaid0Fu1T9/P/Srj/8AiabKoaDyiGFbZdTnGfK+04lWRvwAmvazpqbUt5Omw634jJDuMCAwEAAaOB7TCB6jAdBgNVHQ4EFgQUEvJcHHcTiDtu7bAyZw+xaqg+xdIwgboGA1UdIwSBsjCBr4AUEvJcHHcTiDtu7bAyZw+xaqg+xdKhgYukgYgwgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIEwlTdG9ja2hvbG0xEjAQBgNVBAcTCVN0b2NraG9sbTEcMBoGA1UECxMTRmlybWEgRGF2aWQgV2liZXJnaDETMBEGA1UEAxMKb3Zwbi5zZSBjYTEbMBkGCSqGSIb3DQEJARYMaW5mb0BvdnBuLnNlggkArZohaqkuPX8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAJmID6OyBJbV7ayPPgquojF+FICuDdOfGVKP828cyISxcbVA04VpD0QLYVb0k9pFUx0NbgX2SvRTiFhP7LcyS1HV9s+XLCb2WItPPsrdRTwtqU2n3TlCEzWA3WOcOCtT6JSkv1eelmx1JnP0gYJrDvDvRYBFctwWhtE0bineSQkZwN6980zkknADLAiHpeZSu/AMx7CGTwA6SmoFvpNBmHXDcfe/9ZqbbYfUfyPNe+0JbMrcv1elKi+6wlEkHFaEBphiZwGEbOX1CjUMcQFgW/cIp3n50Eiyx6ktuqimhyb59P4Nw8gqH452tTtE4MM/brA5y0Q0WFBRBojfZIbGWWQ==", //nolint:lll + }, + TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll + TLSCrypt: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll + ExtraLines: []string{ + "allow-compression asym", + "replay-window 256", }, } return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported) From b9421221dadbaa941b85fe816439d125e8fda67d Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 8 Nov 2024 16:19:57 +0000 Subject: [PATCH 3/8] Set TLS crypt for Singapore hostnames only --- internal/provider/ovpn/openvpnconf.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/provider/ovpn/openvpnconf.go b/internal/provider/ovpn/openvpnconf.go index e86793c74..1bcb1b93c 100644 --- a/internal/provider/ovpn/openvpnconf.go +++ b/internal/provider/ovpn/openvpnconf.go @@ -1,6 +1,8 @@ package ovpn import ( + "strings" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants/openvpn" "github.com/qdm12/gluetun/internal/models" @@ -22,12 +24,17 @@ func (p *Provider) OpenVPNConfig(connection models.Connection, CAs: []string{ "MIIEfTCCA2WgAwIBAgIJAK2aIWqpLj1/MA0GCSqGSIb3DQEBBQUAMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTAeFw0xNDA4MTcxODIxMjlaFw0zNDA4MTIxODIxMjlaMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMR+aP4GTuZwurZuOA2NYzMfqKyZi/TJcLEPlGTB/b4CWA9bTd8f0pHPrDAZsXIEayxxB58BIFNDNiybnbO15JN/QwlsqmA+aZX6mCSkScs/rRwasM6LDo8iGx+KmYEqAgzziONGbCMnlO+OaarXte7LhZ9X6Z/bryu4xq/i1v3raak13kXsrogtu4iDzxqJE/QhbNOi0yhCdlm5RYQjmlKGdPB9pNTgcakVI4HcngRYMzBlrGin0YkvWCdpx5FrDNeld7BSWrJMNYyvd+buaid0Fu1T9/P/Srj/8AiabKoaDyiGFbZdTnGfK+04lWRvwAmvazpqbUt5Omw634jJDuMCAwEAAaOB7TCB6jAdBgNVHQ4EFgQUEvJcHHcTiDtu7bAyZw+xaqg+xdIwgboGA1UdIwSBsjCBr4AUEvJcHHcTiDtu7bAyZw+xaqg+xdKhgYukgYgwgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIEwlTdG9ja2hvbG0xEjAQBgNVBAcTCVN0b2NraG9sbTEcMBoGA1UECxMTRmlybWEgRGF2aWQgV2liZXJnaDETMBEGA1UEAxMKb3Zwbi5zZSBjYTEbMBkGCSqGSIb3DQEJARYMaW5mb0BvdnBuLnNlggkArZohaqkuPX8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAJmID6OyBJbV7ayPPgquojF+FICuDdOfGVKP828cyISxcbVA04VpD0QLYVb0k9pFUx0NbgX2SvRTiFhP7LcyS1HV9s+XLCb2WItPPsrdRTwtqU2n3TlCEzWA3WOcOCtT6JSkv1eelmx1JnP0gYJrDvDvRYBFctwWhtE0bineSQkZwN6980zkknADLAiHpeZSu/AMx7CGTwA6SmoFvpNBmHXDcfe/9ZqbbYfUfyPNe+0JbMrcv1elKi+6wlEkHFaEBphiZwGEbOX1CjUMcQFgW/cIp3n50Eiyx6ktuqimhyb59P4Nw8gqH452tTtE4MM/brA5y0Q0WFBRBojfZIbGWWQ==", //nolint:lll }, - TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll - TLSCrypt: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll + TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll ExtraLines: []string{ "allow-compression asym", "replay-window 256", }, } + + if strings.HasSuffix(connection.Hostname, "singapore.ovpn.com") { + providerSettings.TLSCrypt = providerSettings.TLSAuth + providerSettings.TLSAuth = "" + } + return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported) } From cfb8fcf00ff879cd8ca6bdaf9bf1d6b31c3790a3 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Wed, 25 Dec 2024 12:49:22 +0000 Subject: [PATCH 4/8] Add missing "key-direction 1" --- internal/provider/ovpn/openvpnconf.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/provider/ovpn/openvpnconf.go b/internal/provider/ovpn/openvpnconf.go index 1bcb1b93c..4a723b08b 100644 --- a/internal/provider/ovpn/openvpnconf.go +++ b/internal/provider/ovpn/openvpnconf.go @@ -24,7 +24,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection, CAs: []string{ "MIIEfTCCA2WgAwIBAgIJAK2aIWqpLj1/MA0GCSqGSIb3DQEBBQUAMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTAeFw0xNDA4MTcxODIxMjlaFw0zNDA4MTIxODIxMjlaMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMR+aP4GTuZwurZuOA2NYzMfqKyZi/TJcLEPlGTB/b4CWA9bTd8f0pHPrDAZsXIEayxxB58BIFNDNiybnbO15JN/QwlsqmA+aZX6mCSkScs/rRwasM6LDo8iGx+KmYEqAgzziONGbCMnlO+OaarXte7LhZ9X6Z/bryu4xq/i1v3raak13kXsrogtu4iDzxqJE/QhbNOi0yhCdlm5RYQjmlKGdPB9pNTgcakVI4HcngRYMzBlrGin0YkvWCdpx5FrDNeld7BSWrJMNYyvd+buaid0Fu1T9/P/Srj/8AiabKoaDyiGFbZdTnGfK+04lWRvwAmvazpqbUt5Omw634jJDuMCAwEAAaOB7TCB6jAdBgNVHQ4EFgQUEvJcHHcTiDtu7bAyZw+xaqg+xdIwgboGA1UdIwSBsjCBr4AUEvJcHHcTiDtu7bAyZw+xaqg+xdKhgYukgYgwgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIEwlTdG9ja2hvbG0xEjAQBgNVBAcTCVN0b2NraG9sbTEcMBoGA1UECxMTRmlybWEgRGF2aWQgV2liZXJnaDETMBEGA1UEAxMKb3Zwbi5zZSBjYTEbMBkGCSqGSIb3DQEJARYMaW5mb0BvdnBuLnNlggkArZohaqkuPX8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAJmID6OyBJbV7ayPPgquojF+FICuDdOfGVKP828cyISxcbVA04VpD0QLYVb0k9pFUx0NbgX2SvRTiFhP7LcyS1HV9s+XLCb2WItPPsrdRTwtqU2n3TlCEzWA3WOcOCtT6JSkv1eelmx1JnP0gYJrDvDvRYBFctwWhtE0bineSQkZwN6980zkknADLAiHpeZSu/AMx7CGTwA6SmoFvpNBmHXDcfe/9ZqbbYfUfyPNe+0JbMrcv1elKi+6wlEkHFaEBphiZwGEbOX1CjUMcQFgW/cIp3n50Eiyx6ktuqimhyb59P4Nw8gqH452tTtE4MM/brA5y0Q0WFBRBojfZIbGWWQ==", //nolint:lll }, - TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll + TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll + KeyDirection: "1", ExtraLines: []string{ "allow-compression asym", "replay-window 256", @@ -34,6 +35,7 @@ func (p *Provider) OpenVPNConfig(connection models.Connection, if strings.HasSuffix(connection.Hostname, "singapore.ovpn.com") { providerSettings.TLSCrypt = providerSettings.TLSAuth providerSettings.TLSAuth = "" + providerSettings.KeyDirection = "" } return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported) From 80763145dcd9522099081bce4e8777d0ad0d7b8e Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 27 Dec 2024 20:06:34 +0000 Subject: [PATCH 5/8] Remove unneeded `allow-compression asym` --- internal/provider/ovpn/openvpnconf.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/provider/ovpn/openvpnconf.go b/internal/provider/ovpn/openvpnconf.go index 4a723b08b..e095e5acd 100644 --- a/internal/provider/ovpn/openvpnconf.go +++ b/internal/provider/ovpn/openvpnconf.go @@ -27,7 +27,6 @@ func (p *Provider) OpenVPNConfig(connection models.Connection, TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll KeyDirection: "1", ExtraLines: []string{ - "allow-compression asym", "replay-window 256", }, } From 506c9140baf8ef5939c8267e7af4ffbbfbe9e1b9 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 22 May 2026 00:29:35 +0000 Subject: [PATCH 6/8] PR feedback --- README.md | 5 +-- internal/provider/ovpn/openvpnconf.go | 3 -- internal/provider/ovpn/updater/api.go | 60 ++++++--------------------- internal/storage/copy.go | 16 ++----- internal/storage/copy_test.go | 44 +++----------------- internal/storage/filter.go | 6 ++- internal/storage/hardcoded.go | 2 +- 7 files changed, 30 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 2ace59580..59a13e70a 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,8 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers - Supports Wireguard both kernelspace and userspace - For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Ovpn**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe** - For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - -- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) -- More in progress, see [#134](https://github.com/passteque/gluetun/issues/134) + - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) + - More in progress, see [#134](https://github.com/passteque/gluetun/issues/134) - Supports AmneziaWG only with the custom provider for now - DNS over TLS baked in with service provider(s) of your choice - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours diff --git a/internal/provider/ovpn/openvpnconf.go b/internal/provider/ovpn/openvpnconf.go index e095e5acd..2ce929770 100644 --- a/internal/provider/ovpn/openvpnconf.go +++ b/internal/provider/ovpn/openvpnconf.go @@ -26,9 +26,6 @@ func (p *Provider) OpenVPNConfig(connection models.Connection, }, TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll KeyDirection: "1", - ExtraLines: []string{ - "replay-window 256", - }, } if strings.HasSuffix(connection.Hostname, "singapore.ovpn.com") { diff --git a/internal/provider/ovpn/updater/api.go b/internal/provider/ovpn/updater/api.go index d838d2e01..9a00287ef 100644 --- a/internal/provider/ovpn/updater/api.go +++ b/internal/provider/ovpn/updater/api.go @@ -67,17 +67,11 @@ func fetchAPI(ctx context.Context, client *http.Client) ( return data, nil } -var ( - ErrCityNotSet = errors.New("city is not set") - ErrCountryNameNotSet = errors.New("country name is not set") - ErrServersNotSet = errors.New("servers array is not set") -) - func (a *apiDataCenter) validate() (err error) { conditionalErrors := []conditionalError{ - {err: ErrCityNotSet, condition: a.City == ""}, - {err: ErrCountryNameNotSet, condition: a.CountryName == ""}, - {err: ErrServersNotSet, condition: len(a.Servers) == 0}, + {err: "city is not set", condition: a.City == ""}, + {err: "country name is not set", condition: a.CountryName == ""}, + {err: "servers array is not set", condition: len(a.Servers) == 0}, } err = collectErrors(conditionalErrors) if err != nil { @@ -106,29 +100,19 @@ func (a *apiDataCenter) validate() (err error) { return nil } -var ( - ErrIPFieldNotValid = errors.New("ip address is not set") - ErrHostnameFieldNotSet = errors.New("hostname field is not set") - ErrPublicKeyFieldNotSet = errors.New("public key field is not set") - ErrWireguardPortsNotSet = errors.New("wireguard ports array is not set") - ErrWireguardPortNotDefault = errors.New("wireguard port is not the default 9929") - ErrMultiHopOpenVPNPortNotSet = errors.New("multihop OpenVPN port is not set") - ErrMultiHopWireguardPortNotSet = errors.New("multihop WireGuard port is not set") -) - func (a *apiServer) validate() (err error) { const defaultWireguardPort = 9929 conditionalErrors := []conditionalError{ - {err: ErrIPFieldNotValid, condition: !a.IP.IsValid()}, - {err: ErrHostnameFieldNotSet, condition: a.Ptr == ""}, - {err: ErrPublicKeyFieldNotSet, condition: a.PublicKey == ""}, - {err: ErrWireguardPortsNotSet, condition: len(a.WireguardPorts) == 0}, + {err: "ip address is not set", condition: !a.IP.IsValid()}, + {err: "hostname field is not set", condition: a.Ptr == ""}, + {err: "public key field is not set", condition: a.PublicKey == ""}, + {err: "wireguard ports array is not set", condition: len(a.WireguardPorts) == 0}, { - err: ErrWireguardPortNotDefault, + err: "wireguard port is not the default 9929", condition: len(a.WireguardPorts) != 1 || a.WireguardPorts[0] != defaultWireguardPort, }, - {err: ErrMultiHopOpenVPNPortNotSet, condition: a.MultiHopOpenvpnPort == 0}, - {err: ErrMultiHopWireguardPortNotSet, condition: a.MultiHopWireguardPort == 0}, + {err: "multihop OpenVPN port is not set", condition: a.MultiHopOpenvpnPort == 0}, + {err: "multihop WireGuard port is not set", condition: a.MultiHopWireguardPort == 0}, } err = collectErrors(conditionalErrors) switch { @@ -144,28 +128,12 @@ func (a *apiServer) validate() (err error) { } type conditionalError struct { - err error + err string condition bool } -type joinedError struct { - errs []error -} - -func (e *joinedError) Unwrap() []error { - return e.errs -} - -func (e *joinedError) Error() string { - errStrings := make([]string, len(e.errs)) - for i, err := range e.errs { - errStrings[i] = err.Error() - } - return strings.Join(errStrings, "; ") -} - func collectErrors(conditionalErrors []conditionalError) (err error) { - errs := make([]error, 0, len(conditionalErrors)) + errs := make([]string, 0, len(conditionalErrors)) for _, conditionalError := range conditionalErrors { if !conditionalError.condition { continue @@ -177,7 +145,5 @@ func collectErrors(conditionalErrors []conditionalError) (err error) { return nil } - return &joinedError{ - errs: errs, - } + return errors.New(strings.Join(errs, "; ")) } diff --git a/internal/storage/copy.go b/internal/storage/copy.go index 113478f9f..3bc53ebd4 100644 --- a/internal/storage/copy.go +++ b/internal/storage/copy.go @@ -1,23 +1,15 @@ package storage import ( - "net/netip" + "slices" "github.com/qdm12/gluetun/internal/models" ) func copyServer(server models.Server) (serverCopy models.Server) { serverCopy = server - serverCopy.IPs = copyIPs(server.IPs) + serverCopy.IPs = slices.Clone(server.IPs) + serverCopy.PortsTCP = slices.Clone(server.PortsTCP) + serverCopy.PortsUDP = slices.Clone(server.PortsUDP) return serverCopy } - -func copyIPs(toCopy []netip.Addr) (copied []netip.Addr) { - if toCopy == nil { - return nil - } - - copied = make([]netip.Addr, len(toCopy)) - copy(copied, toCopy) - return copied -} diff --git a/internal/storage/copy_test.go b/internal/storage/copy_test.go index bea568d22..ea16b53c6 100644 --- a/internal/storage/copy_test.go +++ b/internal/storage/copy_test.go @@ -21,43 +21,9 @@ func Test_copyServer(t *testing.T) { assert.Equal(t, server, serverCopy) // Check for mutation serverCopy.IPs[0] = netip.AddrFrom4([4]byte{9, 9, 9, 9}) - assert.NotEqual(t, server, serverCopy) -} - -func Test_copyIPs(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - toCopy []netip.Addr - copied []netip.Addr - }{ - "nil": {}, - "empty": { - toCopy: []netip.Addr{}, - copied: []netip.Addr{}, - }, - "single IP": { - toCopy: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, - copied: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, - }, - "two IPs": { - toCopy: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})}, - copied: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})}, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - copied := copyIPs(testCase.toCopy) - - assert.Equal(t, testCase.copied, copied) - - if len(copied) > 0 { - testCase.toCopy[0] = netip.AddrFrom4([4]byte{9, 9, 9, 9}) - assert.NotEqual(t, testCase.toCopy[0], testCase.copied[0]) - } - }) - } + serverCopy.PortsTCP = []uint16{80} + serverCopy.PortsUDP = []uint16{53} + assert.NotEqual(t, server.IPs, serverCopy.IPs) + assert.NotEqual(t, server.PortsTCP, serverCopy.PortsTCP) + assert.NotEqual(t, server.PortsUDP, serverCopy.PortsUDP) } diff --git a/internal/storage/filter.go b/internal/storage/filter.go index 4ff4d609b..c56d0ece4 100644 --- a/internal/storage/filter.go +++ b/internal/storage/filter.go @@ -123,7 +123,11 @@ func filterServer(server models.Server, return true } - if filterByPorts(selection, server.PortsTCP) { + serverPorts := server.PortsUDP + if server.VPN == vpn.OpenVPN && server.TCP { + serverPorts = server.PortsTCP + } + if filterByPorts(selection, serverPorts) { return true } diff --git a/internal/storage/hardcoded.go b/internal/storage/hardcoded.go index c1f246488..5655281d0 100644 --- a/internal/storage/hardcoded.go +++ b/internal/storage/hardcoded.go @@ -21,7 +21,7 @@ func parseHardcodedServers() (allServers models.AllServers) { providerFile, err := serversmodule.Files.Open(filename) if err != nil { const rootURL = "https://github.com/qdm12/gluetun-servers/blob/main/pkg/servers" - panic(fmt.Sprintf("reading embedded provider file defined at %s/%s.json: %s", rootURL, filename, err)) + panic(fmt.Sprintf("reading embedded provider file defined at %s/%s: %s", rootURL, filename, err)) } defer providerFile.Close() // no-op From b5f815640efeed05046e50748a4a2dd1fbdea4b2 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 22 May 2026 01:12:24 +0000 Subject: [PATCH 7/8] SERVER_DEDICATED option --- Dockerfile | 2 ++ go.mod | 2 +- go.sum | 4 +-- .../configuration/settings/serverselection.go | 18 ++++++++++++ internal/models/server.go | 1 + internal/provider/ovpn/updater/api.go | 18 +++++++----- internal/provider/ovpn/updater/api_test.go | 1 + internal/provider/ovpn/updater/servers.go | 13 ++++++++- .../provider/ovpn/updater/servers_test.go | 28 +++++++++++++++++-- internal/storage/filter.go | 6 ++++ 10 files changed, 80 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9160c4a98..96a286176 100644 --- a/Dockerfile +++ b/Dockerfile @@ -192,6 +192,8 @@ ENV VPN_SERVICE_PROVIDER=pia \ PREMIUM_ONLY= \ # # PIA and ProtonVPN only: PORT_FORWARD_ONLY= \ + # # Ovpn only: + SERVER_DEDICATED=no \ # Firewall FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \ FIREWALL_VPN_INPUT_PORTS= \ diff --git a/go.mod b/go.mod index e07fffb07..c520cf958 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/miekg/dns v1.1.62 github.com/pelletier/go-toml/v2 v2.2.4 github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a - github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c + github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82 github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 github.com/qdm12/gosettings v0.4.4 github.com/qdm12/goshutdown v0.3.0 diff --git a/go.sum b/go.sum index 805ed4a92..004938841 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE= -github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c h1:rPgmhMt9uOq6pdPr8k0sXUnZ0fjTBw+yUOaIl0ttFu4= -github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8= +github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82 h1:tE44IEW7o9yPQaO8HBeoO9RxtTTxqhboIypegrQlVt8= +github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg= github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4= diff --git a/internal/configuration/settings/serverselection.go b/internal/configuration/settings/serverselection.go index c3e6a6d6c..f3321b30f 100644 --- a/internal/configuration/settings/serverselection.go +++ b/internal/configuration/settings/serverselection.go @@ -63,6 +63,9 @@ type ServerSelection struct { // TorOnly is true if VPN servers without tor should // be filtered. This is used with ProtonVPN. TorOnly *bool `json:"tor_only"` + // Dedicated is true if dedicated VPN servers should be chosen only. + // This is used with OVPN. + Dedicated *bool `json:"dedicated"` // OpenVPN contains settings to select OpenVPN servers // and the final connection. OpenVPN OpenVPNSelection `json:"openvpn"` @@ -272,6 +275,8 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) return errors.New("secure core only filter is not supported") case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn: return errors.New("tor only filter is not supported") + case *settings.Dedicated && vpnServiceProvider != providers.Ovpn: + return errors.New("dedicated filter is not supported") default: return nil } @@ -296,6 +301,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) { TorOnly: gosettings.CopyPointer(ss.TorOnly), PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly), MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly), + Dedicated: gosettings.CopyPointer(ss.Dedicated), OpenVPN: ss.OpenVPN.copy(), Wireguard: ss.Wireguard.copy(), } @@ -319,6 +325,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) { ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly) ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly) ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly) + ss.Dedicated = gosettings.OverrideWithPointer(ss.Dedicated, other.Dedicated) ss.OpenVPN.overrideWith(other.OpenVPN) ss.Wireguard.overrideWith(other.Wireguard) } @@ -335,6 +342,7 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled defaultPortForwardOnly := portForwardingEnabled && helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn) ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly) + ss.Dedicated = gosettings.DefaultPointer(ss.Dedicated, false) ss.OpenVPN.setDefaults(vpnProvider) ss.Wireguard.setDefaults() } @@ -410,6 +418,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) { node.Appendf("Multi-hop only servers: yes") } + if *ss.Dedicated { + node.Appendf("Dedicated servers: yes") + } + if *ss.PortForwardOnly { node.Appendf("Port forwarding only servers: yes") } @@ -501,6 +513,12 @@ func (ss *ServerSelection) read(r *reader.Reader, return err } + // Ovpn only + ss.Dedicated, err = r.BoolPtr("SERVER_DEDICATED") + if err != nil { + return err + } + err = ss.OpenVPN.read(r) if err != nil { return err diff --git a/internal/models/server.go b/internal/models/server.go index 2eb190ccb..33656e21c 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -34,6 +34,7 @@ type Server struct { SecureCore bool `json:"secure_core,omitempty"` Tor bool `json:"tor,omitempty"` PortForward bool `json:"port_forward,omitempty"` + Dedicated bool `json:"dedicated,omitempty"` Keep bool `json:"keep,omitempty"` IPs []netip.Addr `json:"ips,omitempty"` PortsTCP []uint16 `json:"ports_tcp,omitempty"` diff --git a/internal/provider/ovpn/updater/api.go b/internal/provider/ovpn/updater/api.go index 9a00287ef..27d3d6611 100644 --- a/internal/provider/ovpn/updater/api.go +++ b/internal/provider/ovpn/updater/api.go @@ -22,13 +22,16 @@ type apiDataCenter struct { } type apiServer struct { - IP netip.Addr `json:"ip"` - Ptr string `json:"ptr"` // hostname - Online bool `json:"online"` - PublicKey string `json:"public_key"` - WireguardPorts []uint16 `json:"wireguard_ports"` - MultiHopOpenvpnPort uint16 `json:"multihop_openvpn_port"` - MultiHopWireguardPort uint16 `json:"multihop_wireguard_port"` + IP netip.Addr `json:"ip"` + Ptr string `json:"ptr"` // hostname + Online bool `json:"online"` + // PublicKey is for the Standard Shared Entry Point + PublicKey string `json:"public_key"` + // PublicKeyIPv4 is for the Public / Dedicated IP Entry Point + PublicKeyIPv4 string `json:"public_key_ipv4"` + WireguardPorts []uint16 `json:"wireguard_ports"` + MultiHopOpenvpnPort uint16 `json:"multihop_openvpn_port"` + MultiHopWireguardPort uint16 `json:"multihop_wireguard_port"` } func fetchAPI(ctx context.Context, client *http.Client) ( @@ -106,6 +109,7 @@ func (a *apiServer) validate() (err error) { {err: "ip address is not set", condition: !a.IP.IsValid()}, {err: "hostname field is not set", condition: a.Ptr == ""}, {err: "public key field is not set", condition: a.PublicKey == ""}, + {err: "public key IPv4 field is not set", condition: a.PublicKeyIPv4 == ""}, {err: "wireguard ports array is not set", condition: len(a.WireguardPorts) == 0}, { err: "wireguard port is not the default 9929", diff --git a/internal/provider/ovpn/updater/api_test.go b/internal/provider/ovpn/updater/api_test.go index 4d7619bf3..7a92afcf2 100644 --- a/internal/provider/ovpn/updater/api_test.go +++ b/internal/provider/ovpn/updater/api_test.go @@ -76,6 +76,7 @@ func Test_fetchAPI(t *testing.T) { Ptr: "vpn44.prd.vienna.ovpn.com", Online: true, PublicKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + PublicKeyIPv4: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", WireguardPorts: []uint16{9929}, MultiHopOpenvpnPort: 20044, MultiHopWireguardPort: 30044, diff --git a/internal/provider/ovpn/updater/servers.go b/internal/provider/ovpn/updater/servers.go index 9815975ba..f7e16d2d0 100644 --- a/internal/provider/ovpn/updater/servers.go +++ b/internal/provider/ovpn/updater/servers.go @@ -56,7 +56,18 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) ( multiHopWireguardServer := wireguardServer multiHopWireguardServer.MultiHop = true multiHopWireguardServer.PortsUDP = []uint16{apiServer.MultiHopWireguardPort} - servers = append(servers, wireguardServer, multiHopWireguardServer) + dedicatedWireguardServer := wireguardServer + dedicatedWireguardServer.WgPubKey = apiServer.PublicKeyIPv4 + dedicatedWireguardServer.Dedicated = true + dedicatedMultiHopWireguardServer := multiHopWireguardServer + dedicatedMultiHopWireguardServer.WgPubKey = apiServer.PublicKeyIPv4 + dedicatedMultiHopWireguardServer.Dedicated = true + servers = append(servers, + wireguardServer, + multiHopWireguardServer, + dedicatedWireguardServer, + dedicatedMultiHopWireguardServer, + ) } } diff --git a/internal/provider/ovpn/updater/servers_test.go b/internal/provider/ovpn/updater/servers_test.go index eda9d60ad..20b1de953 100644 --- a/internal/provider/ovpn/updater/servers_test.go +++ b/internal/provider/ovpn/updater/servers_test.go @@ -55,7 +55,7 @@ func Test_Updater_FetchServers(t *testing.T) { errMessage: "validating data center 1 of 1: data center Vienna: country name is not set", }, "not_enough_servers": { - minServers: 5, + minServers: 7, responseStatus: http.StatusOK, responseBody: `{ "success": true, @@ -69,6 +69,7 @@ func Test_Updater_FetchServers(t *testing.T) { "ptr": "vpn44.prd.vienna.ovpn.com", "online": true, "public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", "wireguard_ports": [9929], "multihop_openvpn_port": 20044, "multihop_wireguard_port": 30044 @@ -78,7 +79,9 @@ func Test_Updater_FetchServers(t *testing.T) { ] }`, errWrapped: common.ErrNotEnoughServers, - errMessage: "not enough servers found: 4 and expected at least 5", + // Wireguard + dedicated Wireguard + Wireguard multi-hop + + // dedicated Wireguard multi-hop + OpenVPN + OpenVPN multi-hop + errMessage: "not enough servers found: 6 and expected at least 7", }, "success": { minServers: 4, @@ -114,6 +117,7 @@ func Test_Updater_FetchServers(t *testing.T) { "ptr": "vpn45.prd.vienna.ovpn.com", "online": false, "public_key": "r93LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "public_key_ipv4": "wGbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", "wireguard_ports": [9929], "multihop_openvpn_port": 20045, "multihop_wireguard_port": 30045 @@ -163,6 +167,26 @@ func Test_Updater_FetchServers(t *testing.T) { MultiHop: true, PortsUDP: []uint16{30044}, }, + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.Wireguard, + WgPubKey: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", + Dedicated: true, + }, + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.Wireguard, + WgPubKey: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", + MultiHop: true, + Dedicated: true, + PortsUDP: []uint16{30044}, + }, }, }, } diff --git a/internal/storage/filter.go b/internal/storage/filter.go index c56d0ece4..00a99c986 100644 --- a/internal/storage/filter.go +++ b/internal/storage/filter.go @@ -49,6 +49,7 @@ func (s *Storage) FilterServers(provider string, selection settings.ServerSelect return servers, nil } +//nolint:gocognit,gocyclo func filterServer(server models.Server, selection settings.ServerSelection, ) (filtered bool) { @@ -91,6 +92,11 @@ func filterServer(server models.Server, return true } + if (*selection.Dedicated && !server.Dedicated) || + (!*selection.Dedicated && server.Dedicated) { + return false + } + if filterByPossibilities(server.Country, selection.Countries) { return true } From 4a670635c433f19670e4476eb992028c267a77a6 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Wed, 24 Jun 2026 22:25:32 +0000 Subject: [PATCH 8/8] log out json from dns leak test --- internal/dns/leak.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/dns/leak.go b/internal/dns/leak.go index 8b9dbb02f..8f377f5db 100644 --- a/internal/dns/leak.go +++ b/internal/dns/leak.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "math" "math/rand/v2" "net/http" @@ -84,11 +85,14 @@ func triggerDNSQuery(ctx context.Context, client *http.Client, session string) ( IP map[string]uint `json:"ip"` } - decoder := json.NewDecoder(response.Body) + rawData, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("reading response body: %w", err) + } var data ipLeakData - err = decoder.Decode(&data) + err = json.Unmarshal(rawData, &data) if err != nil { - return nil, fmt.Errorf("decoding response: %w", err) + return nil, fmt.Errorf("decoding response %q: %w", rawData, err) } else if data.Session != session { return nil, fmt.Errorf("ipleak.net session mismatch: expected %s, got %s", session, data.Session) }