Compare commits
6 Commits
remove-kee
...
ovpn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6476cedae9 | ||
|
|
8f386dd91e | ||
|
|
9c514bf661 | ||
|
|
355cb950c3 | ||
|
|
ff93ea6bac | ||
|
|
231f5d9789 |
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -56,6 +56,7 @@ body:
|
|||||||
- IVPN
|
- IVPN
|
||||||
- Mullvad
|
- Mullvad
|
||||||
- NordVPN
|
- NordVPN
|
||||||
|
- OVPN
|
||||||
- Privado
|
- Privado
|
||||||
- Private Internet Access
|
- Private Internet Access
|
||||||
- PrivateVPN
|
- PrivateVPN
|
||||||
|
|||||||
2
.github/labels.yml
vendored
2
.github/labels.yml
vendored
@@ -62,6 +62,8 @@
|
|||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ NordVPN"
|
- name: "☁️ NordVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ OVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
- name: "☁️ Perfect Privacy"
|
- name: "☁️ Perfect Privacy"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ PIA"
|
- name: "☁️ PIA"
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# # ProtonVPN only:
|
# # ProtonVPN only:
|
||||||
SECURE_CORE_ONLY= \
|
SECURE_CORE_ONLY= \
|
||||||
TOR_ONLY= \
|
TOR_ONLY= \
|
||||||
# # Surfshark only:
|
# # Surfshark and ovpn only:
|
||||||
MULTIHOP_ONLY= \
|
MULTIHOP_ONLY= \
|
||||||
# # VPN Secure only:
|
# # VPN Secure only:
|
||||||
PREMIUM_ONLY= \
|
PREMIUM_ONLY= \
|
||||||
|
|||||||
@@ -57,10 +57,10 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
||||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Ovpn**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||||
- Supports OpenVPN for all providers listed
|
- Supports OpenVPN for all providers listed
|
||||||
- Supports Wireguard both kernelspace and userspace
|
- 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**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** 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)
|
- 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/qdm12/gluetun/issues/134)
|
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
// no restriction on port
|
// no restriction on port
|
||||||
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
||||||
providers.Privatevpn, providers.Torguard:
|
providers.Ovpn, providers.Privatevpn, providers.Torguard:
|
||||||
// no custom port allowed
|
// no custom port allowed
|
||||||
case providers.Expressvpn, providers.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
|
|||||||
providers.Ivpn,
|
providers.Ivpn,
|
||||||
providers.Mullvad,
|
providers.Mullvad,
|
||||||
providers.Nordvpn,
|
providers.Nordvpn,
|
||||||
|
providers.Ovpn,
|
||||||
providers.Protonvpn,
|
providers.Protonvpn,
|
||||||
providers.Surfshark,
|
providers.Surfshark,
|
||||||
providers.Windscribe,
|
providers.Windscribe,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
|
|||||||
providers.Ivpn,
|
providers.Ivpn,
|
||||||
providers.Mullvad,
|
providers.Mullvad,
|
||||||
providers.Nordvpn,
|
providers.Nordvpn,
|
||||||
|
providers.Ovpn,
|
||||||
providers.Protonvpn,
|
providers.Protonvpn,
|
||||||
providers.Surfshark,
|
providers.Surfshark,
|
||||||
providers.Windscribe,
|
providers.Windscribe,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
@@ -21,7 +22,7 @@ type WireguardSelection struct {
|
|||||||
// in the internal state.
|
// in the internal state.
|
||||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||||
// EndpointPort is a the server port to use for the VPN server.
|
// 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.
|
// and Windscribe, and compulsory for the others.
|
||||||
// When optional, it can be set to 0 to indicate not use
|
// When optional, it can be set to 0 to indicate not use
|
||||||
// a custom endpoint port. It cannot be nil in the internal
|
// a custom endpoint port. It cannot be nil in the internal
|
||||||
@@ -39,8 +40,9 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate EndpointIP
|
// Validate EndpointIP
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
|
case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
|
||||||
providers.Mullvad, providers.Nordvpn, providers.Protonvpn,
|
providers.Mullvad, providers.Nordvpn, providers.Ovpn,
|
||||||
providers.Surfshark, providers.Windscribe:
|
providers.Protonvpn, providers.Surfshark,
|
||||||
|
providers.Windscribe:
|
||||||
// endpoint IP addresses are baked in
|
// endpoint IP addresses are baked in
|
||||||
case providers.Custom:
|
case providers.Custom:
|
||||||
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
|
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
|
||||||
@@ -62,12 +64,16 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
if *w.EndpointPort != 0 {
|
if *w.EndpointPort != 0 {
|
||||||
return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
|
return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
|
||||||
}
|
}
|
||||||
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
|
// EndpointPort is optional and can be 0
|
||||||
if *w.EndpointPort == 0 {
|
if *w.EndpointPort == 0 {
|
||||||
break // no custom endpoint port set
|
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
|
break // no restriction on custom endpoint port value
|
||||||
}
|
}
|
||||||
var allowed []uint16
|
var allowed []uint16
|
||||||
@@ -92,7 +98,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate PublicKey
|
// Validate PublicKey
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
|
case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
|
||||||
providers.Surfshark, providers.Windscribe:
|
providers.Ovpn, providers.Surfshark, providers.Windscribe:
|
||||||
// public keys are baked in
|
// public keys are baked in
|
||||||
case providers.Custom:
|
case providers.Custom:
|
||||||
if w.PublicKey == "" {
|
if w.PublicKey == "" {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const (
|
|||||||
Ivpn = "ivpn"
|
Ivpn = "ivpn"
|
||||||
Mullvad = "mullvad"
|
Mullvad = "mullvad"
|
||||||
Nordvpn = "nordvpn"
|
Nordvpn = "nordvpn"
|
||||||
|
Ovpn = "ovpn"
|
||||||
Perfectprivacy = "perfect privacy"
|
Perfectprivacy = "perfect privacy"
|
||||||
Privado = "privado"
|
Privado = "privado"
|
||||||
PrivateInternetAccess = "private internet access"
|
PrivateInternetAccess = "private internet access"
|
||||||
@@ -44,6 +45,7 @@ func All() []string {
|
|||||||
Ivpn,
|
Ivpn,
|
||||||
Mullvad,
|
Mullvad,
|
||||||
Nordvpn,
|
Nordvpn,
|
||||||
|
Ovpn,
|
||||||
Perfectprivacy,
|
Perfectprivacy,
|
||||||
Privado,
|
Privado,
|
||||||
PrivateInternetAccess,
|
PrivateInternetAccess,
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type Server struct {
|
|||||||
PortForward bool `json:"port_forward,omitempty"`
|
PortForward bool `json:"port_forward,omitempty"`
|
||||||
Keep bool `json:"keep,omitempty"`
|
Keep bool `json:"keep,omitempty"`
|
||||||
IPs []netip.Addr `json:"ips,omitempty"`
|
IPs []netip.Addr `json:"ips,omitempty"`
|
||||||
|
PortsTCP []uint16 `json:"ports_tcp,omitempty"`
|
||||||
|
PortsUDP []uint16 `json:"ports_udp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
15
internal/provider/ovpn/connection.go
Normal file
15
internal/provider/ovpn/connection.go
Normal file
@@ -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.randSource)
|
||||||
|
}
|
||||||
128
internal/provider/ovpn/connection_test.go
Normal file
128
internal/provider/ovpn/connection_test.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package ovpn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"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)
|
||||||
|
randSource := rand.NewSource(0)
|
||||||
|
|
||||||
|
client := (*http.Client)(nil)
|
||||||
|
provider := New(storage, randSource, 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
internal/provider/ovpn/openvpnconf.go
Normal file
41
internal/provider/ovpn/openvpnconf.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package ovpn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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,
|
||||||
|
RemoteCertTLS: true,
|
||||||
|
Ciphers: []string{
|
||||||
|
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
|
||||||
|
KeyDirection: "1",
|
||||||
|
ExtraLines: []string{
|
||||||
|
"replay-window 256",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(connection.Hostname, "singapore.ovpn.com") {
|
||||||
|
providerSettings.TLSCrypt = providerSettings.TLSAuth
|
||||||
|
providerSettings.TLSAuth = ""
|
||||||
|
providerSettings.KeyDirection = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
||||||
|
}
|
||||||
30
internal/provider/ovpn/provider.go
Normal file
30
internal/provider/ovpn/provider.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package ovpn
|
||||||
|
|
||||||
|
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/ovpn/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
storage common.Storage
|
||||||
|
randSource rand.Source
|
||||||
|
common.Fetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
|
client *http.Client,
|
||||||
|
) *Provider {
|
||||||
|
return &Provider{
|
||||||
|
storage: storage,
|
||||||
|
randSource: randSource,
|
||||||
|
Fetcher: updater.New(client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Name() string {
|
||||||
|
return providers.Ovpn
|
||||||
|
}
|
||||||
179
internal/provider/ovpn/updater/api.go
Normal file
179
internal/provider/ovpn/updater/api.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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("%w: %d %s", common.ErrHTTPStatusCodeNotOK,
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
115
internal/provider/ovpn/updater/api_test.go
Normal file
115
internal/provider/ovpn/updater/api_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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 status code 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},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
9
internal/provider/ovpn/updater/roundtrip_test.go
Normal file
9
internal/provider/ovpn/updater/roundtrip_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
66
internal/provider/ovpn/updater/servers.go
Normal file
66
internal/provider/ovpn/updater/servers.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrResponseSuccessFalse = errors.New("response success field is false")
|
||||||
|
|
||||||
|
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, fmt.Errorf("%w", ErrResponseSuccessFalse)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
servers = append(servers, openVPNServer)
|
||||||
|
|
||||||
|
wireguardServer := baseServer
|
||||||
|
wireguardServer.VPN = vpn.Wireguard
|
||||||
|
wireguardServer.WgPubKey = apiServer.PublicKey
|
||||||
|
servers = append(servers, wireguardServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
181
internal/provider/ovpn/updater/servers_test.go
Normal file
181
internal/provider/ovpn/updater/servers_test.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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,
|
||||||
|
errWrapped: common.ErrHTTPStatusCodeNotOK,
|
||||||
|
errMessage: "fetching API: HTTP status code not OK: 204 No Content",
|
||||||
|
},
|
||||||
|
"success_field_false": {
|
||||||
|
responseStatus: http.StatusOK,
|
||||||
|
responseBody: `{"success": false}`,
|
||||||
|
errWrapped: ErrResponseSuccessFalse,
|
||||||
|
errMessage: "response success field is false",
|
||||||
|
},
|
||||||
|
"validation_failed": {
|
||||||
|
responseStatus: http.StatusOK,
|
||||||
|
responseBody: `{
|
||||||
|
"success": true,
|
||||||
|
"datacenters": [
|
||||||
|
{
|
||||||
|
"city": "Vienna",
|
||||||
|
"servers": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
errWrapped: ErrCountryNameNotSet,
|
||||||
|
errMessage: "validating data center 1 of 1: data center Vienna: country name is not set",
|
||||||
|
},
|
||||||
|
"not_enough_servers": {
|
||||||
|
minServers: 3,
|
||||||
|
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: 2 and expected at least 3",
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
minServers: 2,
|
||||||
|
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.Wireguard,
|
||||||
|
WgPubKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
15
internal/provider/ovpn/updater/updater.go
Normal file
15
internal/provider/ovpn/updater/updater.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/provider/ivpn"
|
"github.com/qdm12/gluetun/internal/provider/ivpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider/mullvad"
|
"github.com/qdm12/gluetun/internal/provider/mullvad"
|
||||||
"github.com/qdm12/gluetun/internal/provider/nordvpn"
|
"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/perfectprivacy"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privado"
|
"github.com/qdm12/gluetun/internal/provider/privado"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess"
|
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess"
|
||||||
@@ -71,6 +72,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providers.Ivpn: ivpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.Ivpn: ivpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Mullvad: mullvad.New(storage, randSource, client),
|
providers.Mullvad: mullvad.New(storage, randSource, client),
|
||||||
providers.Nordvpn: nordvpn.New(storage, randSource, client, updaterWarner),
|
providers.Nordvpn: nordvpn.New(storage, randSource, client, updaterWarner),
|
||||||
|
providers.Ovpn: ovpn.New(storage, randSource, client),
|
||||||
providers.Perfectprivacy: perfectprivacy.New(storage, randSource, unzipper, updaterWarner),
|
providers.Perfectprivacy: perfectprivacy.New(storage, randSource, unzipper, updaterWarner),
|
||||||
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ func GetConnection(provider string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol := getProtocol(selection)
|
protocol := getProtocol(selection)
|
||||||
port := getPort(selection, defaults.OpenVPNTCPPort,
|
|
||||||
defaults.OpenVPNUDPPort, defaults.WireguardPort)
|
|
||||||
|
|
||||||
connections := make([]models.Connection, 0, len(servers))
|
connections := make([]models.Connection, 0, len(servers))
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
@@ -61,6 +59,9 @@ func GetConnection(provider string,
|
|||||||
hostname = server.OvpnX509
|
hostname = server.OvpnX509
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port := getPort(selection, server, defaults.OpenVPNTCPPort,
|
||||||
|
defaults.OpenVPNUDPPort, defaults.WireguardPort)
|
||||||
|
|
||||||
connection := models.Connection{
|
connection := models.Connection{
|
||||||
Type: selection.VPN,
|
Type: selection.VPN,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
|
|||||||
@@ -6,29 +6,44 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"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,
|
defaultOpenVPNTCP, defaultOpenVPNUDP, defaultWireguard uint16,
|
||||||
) (port uint16) {
|
) (port uint16) {
|
||||||
switch selection.VPN {
|
switch selection.VPN {
|
||||||
case vpn.Wireguard:
|
case vpn.Wireguard:
|
||||||
customPort := *selection.Wireguard.EndpointPort
|
customPort := *selection.Wireguard.EndpointPort
|
||||||
if customPort > 0 {
|
if customPort > 0 {
|
||||||
|
// Note: servers filtering ensures the custom port is within the
|
||||||
|
// server ports defined if any is set.
|
||||||
return customPort
|
return customPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(server.PortsUDP) > 0 {
|
||||||
|
defaultWireguard = server.PortsUDP[0]
|
||||||
|
}
|
||||||
checkDefined("Wireguard", defaultWireguard)
|
checkDefined("Wireguard", defaultWireguard)
|
||||||
return defaultWireguard
|
return defaultWireguard
|
||||||
default: // OpenVPN
|
default: // OpenVPN
|
||||||
customPort := *selection.OpenVPN.CustomPort
|
customPort := *selection.OpenVPN.CustomPort
|
||||||
if customPort > 0 {
|
if customPort > 0 {
|
||||||
|
// Note: servers filtering ensures the custom port is within the
|
||||||
|
// server ports defined if any is set.
|
||||||
return customPort
|
return customPort
|
||||||
}
|
}
|
||||||
if selection.OpenVPN.Protocol == constants.TCP {
|
if selection.OpenVPN.Protocol == constants.TCP {
|
||||||
|
if len(server.PortsTCP) > 0 {
|
||||||
|
defaultOpenVPNTCP = server.PortsTCP[0]
|
||||||
|
}
|
||||||
checkDefined("OpenVPN TCP", defaultOpenVPNTCP)
|
checkDefined("OpenVPN TCP", defaultOpenVPNTCP)
|
||||||
return defaultOpenVPNTCP
|
return defaultOpenVPNTCP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(server.PortsUDP) > 0 {
|
||||||
|
defaultOpenVPNUDP = server.PortsUDP[0]
|
||||||
|
}
|
||||||
checkDefined("OpenVPN UDP", defaultOpenVPNUDP)
|
checkDefined("OpenVPN UDP", defaultOpenVPNUDP)
|
||||||
return defaultOpenVPNUDP
|
return defaultOpenVPNUDP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ func Test_GetPort(t *testing.T) {
|
|||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
selection settings.ServerSelection
|
selection settings.ServerSelection
|
||||||
|
server models.Server
|
||||||
defaultOpenVPNTCP uint16
|
defaultOpenVPNTCP uint16
|
||||||
defaultOpenVPNUDP uint16
|
defaultOpenVPNUDP uint16
|
||||||
defaultWireguard uint16
|
defaultWireguard uint16
|
||||||
@@ -49,6 +51,20 @@ func Test_GetPort(t *testing.T) {
|
|||||||
defaultWireguard: defaultWireguard,
|
defaultWireguard: defaultWireguard,
|
||||||
port: defaultOpenVPNUDP,
|
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": {
|
"OpenVPN UDP no default port defined": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.OpenVPN,
|
VPN: vpn.OpenVPN,
|
||||||
@@ -89,6 +105,20 @@ func Test_GetPort(t *testing.T) {
|
|||||||
},
|
},
|
||||||
port: 1234,
|
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": {
|
"Wireguard": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.Wireguard,
|
VPN: vpn.Wireguard,
|
||||||
@@ -106,6 +136,19 @@ func Test_GetPort(t *testing.T) {
|
|||||||
defaultWireguard: defaultWireguard,
|
defaultWireguard: defaultWireguard,
|
||||||
port: 1234,
|
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": {
|
"Wireguard no default port defined": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.Wireguard,
|
VPN: vpn.Wireguard,
|
||||||
@@ -121,6 +164,7 @@ func Test_GetPort(t *testing.T) {
|
|||||||
if testCase.panics != "" {
|
if testCase.panics != "" {
|
||||||
assert.PanicsWithValue(t, testCase.panics, func() {
|
assert.PanicsWithValue(t, testCase.panics, func() {
|
||||||
_ = getPort(testCase.selection,
|
_ = getPort(testCase.selection,
|
||||||
|
testCase.server,
|
||||||
testCase.defaultOpenVPNTCP,
|
testCase.defaultOpenVPNTCP,
|
||||||
testCase.defaultOpenVPNUDP,
|
testCase.defaultOpenVPNUDP,
|
||||||
testCase.defaultWireguard)
|
testCase.defaultWireguard)
|
||||||
@@ -129,6 +173,7 @@ func Test_GetPort(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
port := getPort(testCase.selection,
|
port := getPort(testCase.selection,
|
||||||
|
testCase.server,
|
||||||
testCase.defaultOpenVPNTCP,
|
testCase.defaultOpenVPNTCP,
|
||||||
testCase.defaultOpenVPNUDP,
|
testCase.defaultOpenVPNUDP,
|
||||||
testCase.defaultWireguard)
|
testCase.defaultWireguard)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
@@ -121,6 +122,10 @@ func filterServer(server models.Server,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filterByPorts(selection, server.PortsTCP) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// TODO filter port forward server for PIA
|
// TODO filter port forward server for PIA
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -164,3 +169,21 @@ func filterByProtocol(selection settings.ServerSelection,
|
|||||||
return (wantTCP && !serverTCP) || (wantUDP && !serverUDP)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commaJoin(slice []string) string {
|
func commaJoin(slice []string) string {
|
||||||
@@ -16,7 +17,7 @@ func commaJoin(slice []string) string {
|
|||||||
|
|
||||||
var ErrNoServerFound = errors.New("no server found")
|
var ErrNoServerFound = errors.New("no server found")
|
||||||
|
|
||||||
func noServerFoundError(selection settings.ServerSelection) (err error) {
|
func noServerFoundError(selection settings.ServerSelection) (err error) { //nolint:gocyclo
|
||||||
var messageParts []string
|
var messageParts []string
|
||||||
|
|
||||||
messageParts = append(messageParts, "VPN "+selection.VPN)
|
messageParts = append(messageParts, "VPN "+selection.VPN)
|
||||||
@@ -153,6 +154,15 @@ func noServerFoundError(selection settings.ServerSelection) (err error) {
|
|||||||
"target ip address "+selection.TargetIP.String())
|
"target ip address "+selection.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, "; ")
|
message := "for " + strings.Join(messageParts, "; ")
|
||||||
|
|
||||||
return fmt.Errorf("%w: %s", ErrNoServerFound, message)
|
return fmt.Errorf("%w: %s", ErrNoServerFound, message)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user