mirror of
https://github.com/qdm12/gluetun.git
synced 2025-12-11 04:38:54 -06:00
feat: add VPNsecure.me support (#848)
- `OPENVPN_ENCRYPTED_KEY` environment variable - `OPENVPN_ENCRYPTED_KEY_SECRETFILE` environment variable - `OPENVPN_KEY_PASSPHRASE` environment variable - `OPENVPN_KEY_PASSPHRASE_SECRETFILE` environment variable - `PREMIUM_ONLY` environment variable - OpenVPN user and password not required for vpnsecure provider
This commit is contained in:
parent
991cfb8659
commit
a182e3503b
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -57,6 +57,7 @@ body:
|
|||||||
- SlickVPN
|
- SlickVPN
|
||||||
- Surfshark
|
- Surfshark
|
||||||
- TorGuard
|
- TorGuard
|
||||||
|
- VPNSecure.me
|
||||||
- VPNUnlimited
|
- VPNUnlimited
|
||||||
- VyprVPN
|
- VyprVPN
|
||||||
- WeVPN
|
- WeVPN
|
||||||
|
|||||||
2
.github/labels.yml
vendored
2
.github/labels.yml
vendored
@ -73,6 +73,8 @@
|
|||||||
- name: ":cloud: Torguard"
|
- name: ":cloud: Torguard"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
description: ""
|
||||||
|
- name: ":cloud: VPNSecure.me"
|
||||||
|
color: "cfe8d4"
|
||||||
- name: ":cloud: VPNUnlimited"
|
- name: ":cloud: VPNUnlimited"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
description: ""
|
||||||
|
|||||||
@ -115,6 +115,11 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
OPENVPN_KEY= \
|
OPENVPN_KEY= \
|
||||||
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
||||||
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
|
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
|
||||||
|
# # VPNSecure only:
|
||||||
|
OPENVPN_ENCRYPTED_KEY= \
|
||||||
|
OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key \
|
||||||
|
OPENVPN_KEY_PASSPHRASE= \
|
||||||
|
OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase \
|
||||||
# # Nordvpn only:
|
# # Nordvpn only:
|
||||||
SERVER_NUMBER= \
|
SERVER_NUMBER= \
|
||||||
# # PIA only:
|
# # PIA only:
|
||||||
@ -123,6 +128,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
FREE_ONLY= \
|
FREE_ONLY= \
|
||||||
# # Surfshark only:
|
# # Surfshark only:
|
||||||
MULTIHOP_ONLY= \
|
MULTIHOP_ONLY= \
|
||||||
|
# # VPN Secure only:
|
||||||
|
PREMIUM_ONLY= \
|
||||||
# Firewall
|
# Firewall
|
||||||
FIREWALL=on \
|
FIREWALL=on \
|
||||||
FIREWALL_VPN_INPUT_PORTS= \
|
FIREWALL_VPN_INPUT_PORTS= \
|
||||||
|
|||||||
@ -58,7 +58,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Alpine 3.16 for a small Docker image of 29MB
|
- Based on Alpine 3.16 for a small Docker image of 29MB
|
||||||
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **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 **Mullvad**, **Ivpn** and **Windscribe**
|
- For **Mullvad**, **Ivpn** and **Windscribe**
|
||||||
|
|||||||
@ -17,6 +17,7 @@ var (
|
|||||||
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
|
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
|
||||||
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
|
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
|
||||||
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
|
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
|
||||||
|
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
|
||||||
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
|
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
|
||||||
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
|
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
|
||||||
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
|
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
|
||||||
|
|||||||
@ -42,7 +42,7 @@ type OpenVPN struct {
|
|||||||
// It is ignored if it is set to the empty string.
|
// It is ignored if it is set to the empty string.
|
||||||
Auth *string
|
Auth *string
|
||||||
// Cert is the OpenVPN certificate for the <cert> block.
|
// Cert is the OpenVPN certificate for the <cert> block.
|
||||||
// This is notably used by Cyberghost.
|
// This is notably used by Cyberghost and VPN secure.
|
||||||
// It can be set to the empty string to be ignored.
|
// It can be set to the empty string to be ignored.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
Cert *string
|
Cert *string
|
||||||
@ -51,6 +51,15 @@ type OpenVPN struct {
|
|||||||
// It can be set to the empty string to be ignored.
|
// It can be set to the empty string to be ignored.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
Key *string
|
Key *string
|
||||||
|
// EncryptedKey is the content of an encrypted
|
||||||
|
// key for OpenVPN. It is used by VPN secure.
|
||||||
|
// It defaults to the empty string meaning it is not
|
||||||
|
// to be used. KeyPassphrase must be set if this one is set.
|
||||||
|
EncryptedKey *string
|
||||||
|
// KeyPassphrase is the key passphrase to be used by OpenVPN
|
||||||
|
// to decrypt the EncryptedPrivateKey. It defaults to the
|
||||||
|
// empty string and must be set if EncryptedPrivateKey is set.
|
||||||
|
KeyPassphrase *string
|
||||||
// PIAEncPreset is the encryption preset for
|
// PIAEncPreset is the encryption preset for
|
||||||
// Private Internet Access. It can be set to an
|
// Private Internet Access. It can be set to an
|
||||||
// empty string for other providers.
|
// empty string for other providers.
|
||||||
@ -116,6 +125,15 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
|||||||
return fmt.Errorf("client key: %w", err)
|
return fmt.Errorf("client key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypted key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
|
||||||
|
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
const maxMSSFix = 10000
|
const maxMSSFix = 10000
|
||||||
if *o.MSSFix > maxMSSFix {
|
if *o.MSSFix > maxMSSFix {
|
||||||
return fmt.Errorf("%w: %d is over the maximum value of %d",
|
return fmt.Errorf("%w: %d is over the maximum value of %d",
|
||||||
@ -164,6 +182,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
|
|||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case
|
case
|
||||||
providers.Cyberghost,
|
providers.Cyberghost,
|
||||||
|
providers.VPNSecure,
|
||||||
providers.VPNUnlimited:
|
providers.VPNUnlimited:
|
||||||
if clientCert == "" {
|
if clientCert == "" {
|
||||||
return ErrMissingValue
|
return ErrMissingValue
|
||||||
@ -203,23 +222,42 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateOpenVPNEncryptedKey(vpnProvider,
|
||||||
|
encryptedPrivateKey string) (err error) {
|
||||||
|
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
|
||||||
|
return ErrMissingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if encryptedPrivateKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = extract.PEM([]byte(encryptedPrivateKey))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("extracting encrypted key: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenVPN) copy() (copied OpenVPN) {
|
func (o *OpenVPN) copy() (copied OpenVPN) {
|
||||||
return OpenVPN{
|
return OpenVPN{
|
||||||
Version: o.Version,
|
Version: o.Version,
|
||||||
User: helpers.CopyStringPtr(o.User),
|
User: helpers.CopyStringPtr(o.User),
|
||||||
Password: helpers.CopyStringPtr(o.Password),
|
Password: helpers.CopyStringPtr(o.Password),
|
||||||
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
||||||
Ciphers: helpers.CopyStringSlice(o.Ciphers),
|
Ciphers: helpers.CopyStringSlice(o.Ciphers),
|
||||||
Auth: helpers.CopyStringPtr(o.Auth),
|
Auth: helpers.CopyStringPtr(o.Auth),
|
||||||
Cert: helpers.CopyStringPtr(o.Cert),
|
Cert: helpers.CopyStringPtr(o.Cert),
|
||||||
Key: helpers.CopyStringPtr(o.Key),
|
Key: helpers.CopyStringPtr(o.Key),
|
||||||
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
EncryptedKey: helpers.CopyStringPtr(o.EncryptedKey),
|
||||||
IPv6: helpers.CopyBoolPtr(o.IPv6),
|
KeyPassphrase: helpers.CopyStringPtr(o.KeyPassphrase),
|
||||||
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
|
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
||||||
Interface: o.Interface,
|
IPv6: helpers.CopyBoolPtr(o.IPv6),
|
||||||
ProcessUser: o.ProcessUser,
|
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
|
||||||
Verbosity: helpers.CopyIntPtr(o.Verbosity),
|
Interface: o.Interface,
|
||||||
Flags: helpers.CopyStringSlice(o.Flags),
|
ProcessUser: o.ProcessUser,
|
||||||
|
Verbosity: helpers.CopyIntPtr(o.Verbosity),
|
||||||
|
Flags: helpers.CopyStringSlice(o.Flags),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,6 +272,8 @@ func (o *OpenVPN) mergeWith(other OpenVPN) {
|
|||||||
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
|
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
|
||||||
o.Cert = helpers.MergeWithStringPtr(o.Cert, other.Cert)
|
o.Cert = helpers.MergeWithStringPtr(o.Cert, other.Cert)
|
||||||
o.Key = helpers.MergeWithStringPtr(o.Key, other.Key)
|
o.Key = helpers.MergeWithStringPtr(o.Key, other.Key)
|
||||||
|
o.EncryptedKey = helpers.MergeWithStringPtr(o.EncryptedKey, other.EncryptedKey)
|
||||||
|
o.KeyPassphrase = helpers.MergeWithStringPtr(o.KeyPassphrase, other.KeyPassphrase)
|
||||||
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
|
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
|
||||||
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
|
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
|
||||||
@ -255,6 +295,8 @@ func (o *OpenVPN) overrideWith(other OpenVPN) {
|
|||||||
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
|
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
|
||||||
o.Cert = helpers.OverrideWithStringPtr(o.Cert, other.Cert)
|
o.Cert = helpers.OverrideWithStringPtr(o.Cert, other.Cert)
|
||||||
o.Key = helpers.OverrideWithStringPtr(o.Key, other.Key)
|
o.Key = helpers.OverrideWithStringPtr(o.Key, other.Key)
|
||||||
|
o.EncryptedKey = helpers.OverrideWithStringPtr(o.EncryptedKey, other.EncryptedKey)
|
||||||
|
o.KeyPassphrase = helpers.OverrideWithStringPtr(o.KeyPassphrase, other.KeyPassphrase)
|
||||||
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
|
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
|
||||||
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
|
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
|
||||||
@ -277,6 +319,8 @@ func (o *OpenVPN) setDefaults(vpnProvider string) {
|
|||||||
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
|
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
|
||||||
o.Cert = helpers.DefaultStringPtr(o.Cert, "")
|
o.Cert = helpers.DefaultStringPtr(o.Cert, "")
|
||||||
o.Key = helpers.DefaultStringPtr(o.Key, "")
|
o.Key = helpers.DefaultStringPtr(o.Key, "")
|
||||||
|
o.EncryptedKey = helpers.DefaultStringPtr(o.EncryptedKey, "")
|
||||||
|
o.KeyPassphrase = helpers.DefaultStringPtr(o.KeyPassphrase, "")
|
||||||
|
|
||||||
var defaultEncPreset string
|
var defaultEncPreset string
|
||||||
if vpnProvider == providers.PrivateInternetAccess {
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
@ -321,6 +365,11 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.Key))
|
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.Key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *o.EncryptedKey != "" {
|
||||||
|
node.Appendf("Encrypted key: %s (key passhrapse %s)",
|
||||||
|
helpers.ObfuscateData(*o.EncryptedKey), helpers.ObfuscatePassword(*o.KeyPassphrase))
|
||||||
|
}
|
||||||
|
|
||||||
if *o.PIAEncPreset != "" {
|
if *o.PIAEncPreset != "" {
|
||||||
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,8 +60,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
case providers.Expressvpn, providers.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
providers.Ipvanish, providers.Nordvpn,
|
providers.Ipvanish, providers.Nordvpn,
|
||||||
providers.Privado, providers.Purevpn,
|
providers.Privado, providers.Purevpn,
|
||||||
providers.Surfshark, providers.VPNUnlimited,
|
providers.Surfshark, providers.VPNSecure,
|
||||||
providers.Vyprvpn:
|
providers.VPNUnlimited, providers.Vyprvpn:
|
||||||
return fmt.Errorf("%w: for VPN service provider %s",
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
|
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -45,6 +45,10 @@ type ServerSelection struct { //nolint:maligned
|
|||||||
// FreeOnly is true if VPN servers that are not free should
|
// FreeOnly is true if VPN servers that are not free should
|
||||||
// be filtered. This is used with ProtonVPN and VPN Unlimited.
|
// be filtered. This is used with ProtonVPN and VPN Unlimited.
|
||||||
FreeOnly *bool
|
FreeOnly *bool
|
||||||
|
// PremiumOnly is true if VPN servers that are not premium should
|
||||||
|
// be filtered. This is used with VPN Secure.
|
||||||
|
// TODO extend to providers using FreeOnly.
|
||||||
|
PremiumOnly *bool
|
||||||
// StreamOnly is true if VPN servers not for streaming should
|
// StreamOnly is true if VPN servers not for streaming should
|
||||||
// be filtered. This is used with VPNUnlimited.
|
// be filtered. This is used with VPNUnlimited.
|
||||||
StreamOnly *bool
|
StreamOnly *bool
|
||||||
@ -63,8 +67,10 @@ type ServerSelection struct { //nolint:maligned
|
|||||||
var (
|
var (
|
||||||
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
|
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
|
||||||
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
|
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
|
||||||
|
ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported")
|
||||||
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
|
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
|
||||||
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
|
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
|
||||||
|
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||||
@ -103,6 +109,18 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
|||||||
ErrFreeOnlyNotSupported, vpnServiceProvider)
|
ErrFreeOnlyNotSupported, vpnServiceProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *ss.PremiumOnly &&
|
||||||
|
!helpers.IsOneOf(vpnServiceProvider,
|
||||||
|
providers.VPNSecure,
|
||||||
|
) {
|
||||||
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
|
ErrPremiumOnlyNotSupported, vpnServiceProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ss.FreeOnly && *ss.PremiumOnly {
|
||||||
|
return ErrFreePremiumBothSet
|
||||||
|
}
|
||||||
|
|
||||||
if *ss.StreamOnly &&
|
if *ss.StreamOnly &&
|
||||||
!helpers.IsOneOf(vpnServiceProvider,
|
!helpers.IsOneOf(vpnServiceProvider,
|
||||||
providers.Protonvpn,
|
providers.Protonvpn,
|
||||||
@ -194,6 +212,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
|||||||
Numbers: helpers.CopyUint16Slice(ss.Numbers),
|
Numbers: helpers.CopyUint16Slice(ss.Numbers),
|
||||||
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
|
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
|
||||||
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
|
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
|
||||||
|
PremiumOnly: helpers.CopyBoolPtr(ss.PremiumOnly),
|
||||||
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
|
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
|
||||||
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
|
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
|
||||||
OpenVPN: ss.OpenVPN.copy(),
|
OpenVPN: ss.OpenVPN.copy(),
|
||||||
@ -213,6 +232,7 @@ func (ss *ServerSelection) mergeWith(other ServerSelection) {
|
|||||||
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
|
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
|
||||||
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
|
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
|
||||||
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
|
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
|
||||||
|
ss.PremiumOnly = helpers.MergeWithBool(ss.PremiumOnly, other.PremiumOnly)
|
||||||
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
|
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
|
||||||
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
||||||
|
|
||||||
@ -232,6 +252,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
|||||||
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
|
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
|
||||||
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
|
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
|
||||||
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
|
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
|
||||||
|
ss.PremiumOnly = helpers.OverrideWithBool(ss.PremiumOnly, other.PremiumOnly)
|
||||||
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
|
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
|
||||||
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
||||||
ss.OpenVPN.overrideWith(other.OpenVPN)
|
ss.OpenVPN.overrideWith(other.OpenVPN)
|
||||||
@ -243,6 +264,7 @@ func (ss *ServerSelection) setDefaults(vpnProvider string) {
|
|||||||
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
|
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
|
||||||
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
|
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
|
||||||
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
|
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
|
||||||
|
ss.PremiumOnly = helpers.DefaultBool(ss.PremiumOnly, false)
|
||||||
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
|
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
|
||||||
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
|
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
|
||||||
ss.OpenVPN.setDefaults(vpnProvider)
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
@ -299,6 +321,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Free only servers: yes")
|
node.Appendf("Free only servers: yes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *ss.PremiumOnly {
|
||||||
|
node.Appendf("Premium only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
if *ss.StreamOnly {
|
if *ss.StreamOnly {
|
||||||
node.Appendf("Stream only servers: yes")
|
node.Appendf("Stream only servers: yes")
|
||||||
}
|
}
|
||||||
|
|||||||
19
internal/configuration/sources/env/openvpn.go
vendored
19
internal/configuration/sources/env/openvpn.go
vendored
@ -11,7 +11,8 @@ import (
|
|||||||
func (r *Reader) readOpenVPN() (
|
func (r *Reader) readOpenVPN() (
|
||||||
openVPN settings.OpenVPN, err error) {
|
openVPN settings.OpenVPN, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = unsetEnvKeys([]string{"OPENVPN_KEY", "OPENVPN_CERT"}, err)
|
err = unsetEnvKeys([]string{"OPENVPN_KEY", "OPENVPN_CERT",
|
||||||
|
"OPENVPN_KEY_PASSPHRASE", "OPENVPN_ENCRYPTED_KEY"}, err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
openVPN.Version = getCleanedEnv("OPENVPN_VERSION")
|
openVPN.Version = getCleanedEnv("OPENVPN_VERSION")
|
||||||
@ -40,6 +41,13 @@ func (r *Reader) readOpenVPN() (
|
|||||||
return openVPN, fmt.Errorf("environment variable OPENVPN_KEY: %w", err)
|
return openVPN, fmt.Errorf("environment variable OPENVPN_KEY: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openVPN.EncryptedKey, err = readBase64OrNil("OPENVPN_ENCRYPTED_KEY")
|
||||||
|
if err != nil {
|
||||||
|
return openVPN, fmt.Errorf("environment variable OPENVPN_ENCRYPTED_KEY: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
openVPN.KeyPassphrase = r.readOpenVPNKeyPassphrase()
|
||||||
|
|
||||||
openVPN.PIAEncPreset = r.readPIAEncryptionPreset()
|
openVPN.PIAEncPreset = r.readPIAEncryptionPreset()
|
||||||
|
|
||||||
openVPN.IPv6, err = envToBoolPtr("OPENVPN_IPV6")
|
openVPN.IPv6, err = envToBoolPtr("OPENVPN_IPV6")
|
||||||
@ -94,6 +102,15 @@ func (r *Reader) readOpenVPNPassword() (password *string) {
|
|||||||
return password
|
return password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reader) readOpenVPNKeyPassphrase() (passphrase *string) {
|
||||||
|
passphrase = new(string)
|
||||||
|
*passphrase = getCleanedEnv("OPENVPN_KEY_PASSPHRASE")
|
||||||
|
if *passphrase == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return passphrase
|
||||||
|
}
|
||||||
|
|
||||||
func readBase64OrNil(envKey string) (valueOrNil *string, err error) {
|
func readBase64OrNil(envKey string) (valueOrNil *string, err error) {
|
||||||
value := getCleanedEnv(envKey)
|
value := getCleanedEnv(envKey)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
|
|||||||
@ -77,6 +77,12 @@ func (r *Reader) readServerSelection(vpnProvider, vpnType string) (
|
|||||||
return ss, fmt.Errorf("environment variable FREE_ONLY: %w", err)
|
return ss, fmt.Errorf("environment variable FREE_ONLY: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VPNSecure only
|
||||||
|
ss.PremiumOnly, err = envToBoolPtr("PREMIUM_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return ss, fmt.Errorf("environment variable PREMIUM_ONLY: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// VPNUnlimited only
|
// VPNUnlimited only
|
||||||
ss.MultiHopOnly, err = envToBoolPtr("MULTIHOP_ONLY")
|
ss.MultiHopOnly, err = envToBoolPtr("MULTIHOP_ONLY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const (
|
|||||||
OpenVPNClientKeyPath = "/gluetun/client.key"
|
OpenVPNClientKeyPath = "/gluetun/client.key"
|
||||||
// OpenVPNClientCertificatePath is the OpenVPN client certificate filepath.
|
// OpenVPNClientCertificatePath is the OpenVPN client certificate filepath.
|
||||||
OpenVPNClientCertificatePath = "/gluetun/client.crt"
|
OpenVPNClientCertificatePath = "/gluetun/client.crt"
|
||||||
|
openVPNEncryptedKey = "/gluetun/openvpn_encrypted_key"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Reader) readOpenVPN() (settings settings.OpenVPN, err error) {
|
func (r *Reader) readOpenVPN() (settings settings.OpenVPN, err error) {
|
||||||
@ -24,5 +25,10 @@ func (r *Reader) readOpenVPN() (settings settings.OpenVPN, err error) {
|
|||||||
return settings, fmt.Errorf("client certificate: %w", err)
|
return settings, fmt.Errorf("client certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings.EncryptedKey, err = ReadFromFile(openVPNEncryptedKey)
|
||||||
|
if err != nil {
|
||||||
|
return settings, fmt.Errorf("reading encrypted key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,22 @@ func readOpenVPN() (
|
|||||||
return settings, fmt.Errorf("cannot read client key file: %w", err)
|
return settings, fmt.Errorf("cannot read client key file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings.EncryptedKey, err = readSecretFileAsStringPtr(
|
||||||
|
"OPENVPN_ENCRYPTED_KEY_SECRETFILE",
|
||||||
|
"/run/secrets/openvpn_encrypted_key",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return settings, fmt.Errorf("reading encrypted key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.KeyPassphrase, err = readSecretFileAsStringPtr(
|
||||||
|
"OPENVPN_KEY_PASSPHRASE_SECRETFILE",
|
||||||
|
"/run/secrets/openvpn_key_passphrase",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return settings, fmt.Errorf("reading key passphrase file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
settings.Cert, err = readSecretFileAsStringPtr(
|
settings.Cert, err = readSecretFileAsStringPtr(
|
||||||
"OPENVPN_CLIENTCRT_SECRETFILE",
|
"OPENVPN_CLIENTCRT_SECRETFILE",
|
||||||
"/run/secrets/openvpn_clientcrt",
|
"/run/secrets/openvpn_clientcrt",
|
||||||
|
|||||||
@ -3,4 +3,7 @@ package openvpn
|
|||||||
const (
|
const (
|
||||||
// AuthConf is the file path to the OpenVPN auth file.
|
// AuthConf is the file path to the OpenVPN auth file.
|
||||||
AuthConf = "/etc/openvpn/auth.conf"
|
AuthConf = "/etc/openvpn/auth.conf"
|
||||||
|
// AskPassPath is the file path to the decryption passphrase for
|
||||||
|
// and encrypted private key, which is pointed by `askpass`.
|
||||||
|
AskPassPath = "/etc/openvpn/askpass" //nolint:gosec
|
||||||
)
|
)
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const (
|
|||||||
SlickVPN = "slickvpn"
|
SlickVPN = "slickvpn"
|
||||||
Surfshark = "surfshark"
|
Surfshark = "surfshark"
|
||||||
Torguard = "torguard"
|
Torguard = "torguard"
|
||||||
|
VPNSecure = "vpnsecure"
|
||||||
VPNUnlimited = "vpn unlimited"
|
VPNUnlimited = "vpn unlimited"
|
||||||
Vyprvpn = "vyprvpn"
|
Vyprvpn = "vyprvpn"
|
||||||
Wevpn = "wevpn"
|
Wevpn = "wevpn"
|
||||||
@ -48,6 +49,7 @@ func All() []string {
|
|||||||
SlickVPN,
|
SlickVPN,
|
||||||
Surfshark,
|
Surfshark,
|
||||||
Torguard,
|
Torguard,
|
||||||
|
VPNSecure,
|
||||||
VPNUnlimited,
|
VPNUnlimited,
|
||||||
Vyprvpn,
|
Vyprvpn,
|
||||||
Wevpn,
|
Wevpn,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ const (
|
|||||||
numberHeader = "Number"
|
numberHeader = "Number"
|
||||||
ownedHeader = "Owned"
|
ownedHeader = "Owned"
|
||||||
portForwardHeader = "Port forwarding"
|
portForwardHeader = "Port forwarding"
|
||||||
|
premiumHeader = "Premium"
|
||||||
regionHeader = "Region"
|
regionHeader = "Region"
|
||||||
streamHeader = "Stream"
|
streamHeader = "Stream"
|
||||||
tcpHeader = "TCP"
|
tcpHeader = "TCP"
|
||||||
@ -62,6 +63,8 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) {
|
|||||||
fields[i] = boolToMarkdown(s.Owned)
|
fields[i] = boolToMarkdown(s.Owned)
|
||||||
case portForwardHeader:
|
case portForwardHeader:
|
||||||
fields[i] = boolToMarkdown(s.PortForward)
|
fields[i] = boolToMarkdown(s.PortForward)
|
||||||
|
case premiumHeader:
|
||||||
|
fields[i] = boolToMarkdown(s.Premium)
|
||||||
case regionHeader:
|
case regionHeader:
|
||||||
fields[i] = s.Region
|
fields[i] = s.Region
|
||||||
case streamHeader:
|
case streamHeader:
|
||||||
@ -129,6 +132,8 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
|
|||||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, multiHopHeader, tcpHeader, udpHeader}
|
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, multiHopHeader, tcpHeader, udpHeader}
|
||||||
case providers.Torguard:
|
case providers.Torguard:
|
||||||
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
|
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
|
||||||
|
case providers.VPNSecure:
|
||||||
|
return []string{regionHeader, cityHeader, hostnameHeader, premiumHeader}
|
||||||
case providers.VPNUnlimited:
|
case providers.VPNUnlimited:
|
||||||
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}
|
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}
|
||||||
case providers.Vyprvpn:
|
case providers.Vyprvpn:
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type Server struct {
|
|||||||
WgPubKey string `json:"wgpubkey,omitempty"`
|
WgPubKey string `json:"wgpubkey,omitempty"`
|
||||||
Free bool `json:"free,omitempty"`
|
Free bool `json:"free,omitempty"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
|
Premium bool `json:"premium,omitempty"`
|
||||||
PortForward bool `json:"port_forward,omitempty"`
|
PortForward bool `json:"port_forward,omitempty"`
|
||||||
Keep bool `json:"keep,omitempty"`
|
Keep bool `json:"keep,omitempty"`
|
||||||
IPs []net.IP `json:"ips,omitempty"`
|
IPs []net.IP `json:"ips,omitempty"`
|
||||||
|
|||||||
@ -1,65 +1,56 @@
|
|||||||
package openvpn
|
package openvpn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteAuthFile writes the OpenVPN auth file to disk with the right permissions.
|
// WriteAuthFile writes the OpenVPN auth file to disk with the right permissions.
|
||||||
func (c *Configurator) WriteAuthFile(user, password string) error {
|
func (c *Configurator) WriteAuthFile(user, password string) error {
|
||||||
file, err := os.Open(c.authFilePath)
|
content := strings.Join([]string{user, password}, "\n")
|
||||||
|
return writeIfDifferent(c.authFilePath, content, c.puid, c.pgid)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
}
|
||||||
return err
|
|
||||||
}
|
// WriteAskPassFile writes the OpenVPN askpass file to disk with the right permissions.
|
||||||
|
func (c *Configurator) WriteAskPassFile(passphrase string) error {
|
||||||
if os.IsNotExist(err) {
|
return writeIfDifferent(c.askPassPath, passphrase, c.puid, c.pgid)
|
||||||
file, err = os.OpenFile(c.authFilePath, os.O_WRONLY|os.O_CREATE, 0400)
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
func writeIfDifferent(path, content string, puid, pgid int) (err error) {
|
||||||
}
|
fileStat, err := os.Stat(path)
|
||||||
_, err = file.WriteString(user + "\n" + password)
|
if err != nil && !os.IsNotExist(err) {
|
||||||
if err != nil {
|
return fmt.Errorf("obtaining file information: %w", err)
|
||||||
_ = file.Close()
|
}
|
||||||
return err
|
|
||||||
}
|
const perm = os.FileMode(0400)
|
||||||
err = file.Chown(c.puid, c.pgid)
|
var writeData, setChown bool
|
||||||
if err != nil {
|
if os.IsNotExist(err) {
|
||||||
_ = file.Close()
|
writeData = true
|
||||||
return err
|
setChown = true
|
||||||
}
|
} else {
|
||||||
return file.Close()
|
data, err := os.ReadFile(path)
|
||||||
}
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading file: %w", err)
|
||||||
data, err := io.ReadAll(file)
|
}
|
||||||
if err != nil {
|
writeData = string(data) != content
|
||||||
_ = file.Close()
|
setChown = fileStat.Mode().Perm() != perm
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
if err := file.Close(); err != nil {
|
if writeData {
|
||||||
return err
|
err = ioutil.WriteFile(path, []byte(content), perm)
|
||||||
}
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing file: %w", err)
|
||||||
lines := strings.Split(string(data), "\n")
|
}
|
||||||
if len(lines) > 1 && lines[0] == user && lines[1] == password {
|
}
|
||||||
return nil
|
|
||||||
}
|
if setChown {
|
||||||
|
err = os.Chown(path, puid, pgid)
|
||||||
c.logger.Info("username and password changed in " + c.authFilePath)
|
if err != nil {
|
||||||
file, err = os.OpenFile(c.authFilePath, os.O_TRUNC|os.O_WRONLY, 0400)
|
return fmt.Errorf("setting file permissions: %w", err)
|
||||||
if err != nil {
|
}
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
_, err = file.WriteString(user + "\n" + password)
|
return nil
|
||||||
if err != nil {
|
|
||||||
_ = file.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = file.Chown(c.puid, c.pgid)
|
|
||||||
if err != nil {
|
|
||||||
_ = file.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return file.Close()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ type Configurator struct {
|
|||||||
cmder command.RunStarter
|
cmder command.RunStarter
|
||||||
configPath string
|
configPath string
|
||||||
authFilePath string
|
authFilePath string
|
||||||
|
askPassPath string
|
||||||
puid, pgid int
|
puid, pgid int
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ func New(logger Infoer, cmder command.RunStarter,
|
|||||||
cmder: cmder,
|
cmder: cmder,
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
authFilePath: openvpn.AuthConf,
|
authFilePath: openvpn.AuthConf,
|
||||||
|
askPassPath: openvpn.AskPassPath,
|
||||||
puid: puid,
|
puid: puid,
|
||||||
pgid: pgid,
|
pgid: pgid,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/provider/slickvpn"
|
"github.com/qdm12/gluetun/internal/provider/slickvpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider/surfshark"
|
"github.com/qdm12/gluetun/internal/provider/surfshark"
|
||||||
"github.com/qdm12/gluetun/internal/provider/torguard"
|
"github.com/qdm12/gluetun/internal/provider/torguard"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/vpnsecure"
|
||||||
"github.com/qdm12/gluetun/internal/provider/vpnunlimited"
|
"github.com/qdm12/gluetun/internal/provider/vpnunlimited"
|
||||||
"github.com/qdm12/gluetun/internal/provider/vyprvpn"
|
"github.com/qdm12/gluetun/internal/provider/vyprvpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider/wevpn"
|
"github.com/qdm12/gluetun/internal/provider/wevpn"
|
||||||
@ -75,6 +76,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Torguard: torguard.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Torguard: torguard.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
|
providers.VPNSecure: vpnsecure.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.VPNUnlimited: vpnunlimited.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.VPNUnlimited: vpnunlimited.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Vyprvpn: vyprvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Vyprvpn: vyprvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Wevpn: wevpn.New(storage, randSource, updaterWarner, parallelResolver),
|
providers.Wevpn: wevpn.New(storage, randSource, updaterWarner, parallelResolver),
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import (
|
|||||||
|
|
||||||
func fetchServers(ctx context.Context, client *http.Client) (
|
func fetchServers(ctx context.Context, client *http.Client) (
|
||||||
hostToData map[string]serverData, err error) {
|
hostToData map[string]serverData, err error) {
|
||||||
rootNode, err := fetchHTML(ctx, client)
|
const url = "https://www.slickvpn.com/locations/"
|
||||||
|
rootNode, err := htmlutils.Fetch(ctx, client, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching HTML code: %w", err)
|
return nil, fmt.Errorf("fetching HTML code: %w", err)
|
||||||
}
|
}
|
||||||
@ -26,39 +27,6 @@ func fetchServers(ctx context.Context, client *http.Client) (
|
|||||||
return hostToData, nil
|
return hostToData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrHTTPStatusCode = errors.New("HTTP status code is not OK")
|
|
||||||
|
|
||||||
func fetchHTML(ctx context.Context, client *http.Client) (rootNode *html.Node, err error) {
|
|
||||||
const url = "https://www.slickvpn.com/locations/"
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("%w: %d %s",
|
|
||||||
ErrHTTPStatusCode, response.StatusCode, response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
rootNode, err = html.Parse(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
_ = response.Body.Close()
|
|
||||||
return nil, fmt.Errorf("parsing HTML code: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = response.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("closing response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootNode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type serverData struct {
|
type serverData struct {
|
||||||
ovpnURL string
|
ovpnURL string
|
||||||
country string
|
country string
|
||||||
|
|||||||
@ -104,73 +104,6 @@ func Test_fetchServers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_fetchHTML(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
ctx context.Context
|
|
||||||
responseStatus int
|
|
||||||
responseBody io.ReadCloser
|
|
||||||
rootNode *html.Node
|
|
||||||
errWrapped error
|
|
||||||
errMessage string
|
|
||||||
}{
|
|
||||||
"context canceled": {
|
|
||||||
ctx: canceledCtx,
|
|
||||||
errWrapped: context.Canceled,
|
|
||||||
errMessage: `Get "https://www.slickvpn.com/locations/": context canceled`,
|
|
||||||
},
|
|
||||||
"response status not ok": {
|
|
||||||
ctx: context.Background(),
|
|
||||||
responseStatus: http.StatusNotFound,
|
|
||||||
errWrapped: ErrHTTPStatusCode,
|
|
||||||
errMessage: `HTTP status code is not OK: 404 Not Found`,
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
ctx: context.Background(),
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
rootNode: parseTestHTML(t, "some body"),
|
|
||||||
responseBody: ioutil.NopCloser(strings.NewReader("some body")),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
testCase := testCase
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
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.slickvpn.com/locations/")
|
|
||||||
|
|
||||||
ctxErr := r.Context().Err()
|
|
||||||
if ctxErr != nil {
|
|
||||||
return nil, ctxErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: testCase.responseStatus,
|
|
||||||
Status: http.StatusText(testCase.responseStatus),
|
|
||||||
Body: testCase.responseBody,
|
|
||||||
}, nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
rootNode, err := fetchHTML(testCase.ctx, client)
|
|
||||||
|
|
||||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
|
||||||
if testCase.errWrapped != nil {
|
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
|
||||||
}
|
|
||||||
assert.Equal(t, testCase.rootNode, rootNode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_parseHTML(t *testing.T) {
|
func Test_parseHTML(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,10 @@ func filterServer(server models.Server,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *selection.PremiumOnly && !server.Premium {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if *selection.StreamOnly && !server.Stream {
|
if *selection.StreamOnly && !server.Stream {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,6 +88,19 @@ func Test_FilterServers(t *testing.T) {
|
|||||||
{Free: true, VPN: vpn.OpenVPN, UDP: true},
|
{Free: true, VPN: vpn.OpenVPN, UDP: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"filter by premium only": {
|
||||||
|
selection: settings.ServerSelection{
|
||||||
|
PremiumOnly: boolPtr(true),
|
||||||
|
}.WithDefaults(providers.Surfshark),
|
||||||
|
servers: []models.Server{
|
||||||
|
{Premium: false, VPN: vpn.OpenVPN, UDP: true},
|
||||||
|
{Premium: true, VPN: vpn.OpenVPN, UDP: true},
|
||||||
|
{Premium: false, VPN: vpn.OpenVPN, UDP: true},
|
||||||
|
},
|
||||||
|
filtered: []models.Server{
|
||||||
|
{Premium: true, VPN: vpn.OpenVPN, UDP: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
"filter by stream only": {
|
"filter by stream only": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
StreamOnly: boolPtr(true),
|
StreamOnly: boolPtr(true),
|
||||||
|
|||||||
@ -189,6 +189,13 @@ func OpenVPNConfig(provider OpenVPNProviderSettings,
|
|||||||
lines.addLines(WrapOpenvpnTLSCrypt(provider.TLSCrypt))
|
lines.addLines(WrapOpenvpnTLSCrypt(provider.TLSCrypt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *settings.EncryptedKey != "" {
|
||||||
|
lines.add("askpass", openvpn.AskPassPath)
|
||||||
|
keyData, err := extract.PEM([]byte(*settings.EncryptedKey))
|
||||||
|
panicOnError(err, "cannot extract PEM encrypted key")
|
||||||
|
lines.addLines(WrapOpenvpnEncryptedKey(keyData))
|
||||||
|
}
|
||||||
|
|
||||||
if *settings.Cert != "" {
|
if *settings.Cert != "" {
|
||||||
certData, err := extract.PEM([]byte(*settings.Cert))
|
certData, err := extract.PEM([]byte(*settings.Cert))
|
||||||
panicOnError(err, "cannot extract OpenVPN certificate")
|
panicOnError(err, "cannot extract OpenVPN certificate")
|
||||||
@ -295,6 +302,16 @@ func WrapOpenvpnKey(clientKey string) (lines []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WrapOpenvpnEncryptedKey(encryptedKey string) (lines []string) {
|
||||||
|
return []string{
|
||||||
|
"<key>",
|
||||||
|
"-----BEGIN ENCRYPTED PRIVATE KEY-----",
|
||||||
|
encryptedKey,
|
||||||
|
"-----END ENCRYPTED PRIVATE KEY-----",
|
||||||
|
"</key>",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WrapOpenvpnRSAKey(rsaPrivateKey string) (lines []string) {
|
func WrapOpenvpnRSAKey(rsaPrivateKey string) (lines []string) {
|
||||||
return []string{
|
return []string{
|
||||||
"<key>",
|
"<key>",
|
||||||
|
|||||||
14
internal/provider/vpnsecure/connection.go
Normal file
14
internal/provider/vpnsecure/connection.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package vpnsecure
|
||||||
|
|
||||||
|
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) (
|
||||||
|
connection models.Connection, err error) {
|
||||||
|
defaults := utils.NewConnectionDefaults(110, 1282, 0) //nolint:gomnd
|
||||||
|
return utils.GetConnection(p.Name(),
|
||||||
|
p.storage, selection, defaults, p.randSource)
|
||||||
|
}
|
||||||
26
internal/provider/vpnsecure/openvpnconf.go
Normal file
26
internal/provider/vpnsecure/openvpnconf.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package vpnsecure
|
||||||
|
|
||||||
|
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) (lines []string) {
|
||||||
|
//nolint:gomnd
|
||||||
|
providerSettings := utils.OpenVPNProviderSettings{
|
||||||
|
RemoteCertTLS: true,
|
||||||
|
AuthUserPass: true,
|
||||||
|
Ping: 10,
|
||||||
|
// note DES-CBC is not added since it's quite unsecure
|
||||||
|
Ciphers: []string{openvpn.AES256cbc, openvpn.AES128cbc},
|
||||||
|
ExtraLines: []string{
|
||||||
|
"comp-lzo",
|
||||||
|
"float",
|
||||||
|
},
|
||||||
|
CA: "MIIEJjCCAw6gAwIBAgIJAMkzh6p4m6XfMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJOWTERMA8GA1UEBxMITmV3IFlvcmsxFTATBgNVBAoTDHZwbnNlY3VyZS5tZTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEB2cG5zZWN1cmUubWUwIBcNMTcwNTA2MTMzMTQyWhgPMjkzODA4MjYxMzMxNDJaMGkxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJOWTERMA8GA1UEBxMITmV3IFlvcmsxFTATBgNVBAoTDHZwbnNlY3VyZS5tZTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEB2cG5zZWN1cmUubWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiClT1wcZ6oovYjSxUJIQplrBSQRKB44uymC8evohzK7q67x0NE2sLz5Zn9ZiC7RnXQCtEqJfHqjuqjaH5MghjhUDnRbZS/8ElxdGKn9FPvs9b+aTVGSfrQm5KKoVigwAye3ilNiWAyy6MDlBeoKluQ4xW7SGiVZRxLcJbLAmjmfCjBS7eUGbtA8riTkIegFo4WFiy9G76zQWw1V26kDhyzcJNT4xO7USMPUeZthy13g+zi9+rcILhEAnl776sIil6w8UVK8xevFKBlOPk+YyXlo4eZiuppq300ogaS+fX/0mfD7DDE+Gk5/nCeACDNiBlfQ3ol/De8Cm60HWEUtZVAgMBAAGjgc4wgcswHQYDVR0OBBYEFBJyf4mpGT3dIu65/1zAFqCgGxZoMIGbBgNVHSMEgZMwgZCAFBJyf4mpGT3dIu65/1zAFqCgGxZooW2kazBpMQswCQYDVQQGEwJVUzELMAkGA1UECBMCTlkxETAPBgNVBAcTCE5ldyBZb3JrMRUwEwYDVQQKEwx2cG5zZWN1cmUubWUxIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAdnBuc2VjdXJlLm1lggkAyTOHqnibpd8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEArbTAibGQilY4Lu2RAVPjNx14SfojueBroeN7NIpAFUfbifPQRWvLamzRfxFTO0PXRc2pw/It7oa8yM7BsZj0vOiZY2p1JBHZwKom6tiSUVENDGW6JaYtiaE8XPyjfA5Yhfx4FefmaJ1veDYid18S+VVpt+Y+UIUxNmg1JB3CCUwbjl+dWlcvDBy4+jI+sZ7A1LF3uX64ZucDQ/XrpuopHhvDjw7g1PpKXsRqBYL+cpxUI7GrINBa/rGvXqv/NvFH8bguggknWKxKhd+jyMqkW3Ws258e0OwHz7gQ+tTJ909tR0TxJhZGkHatNSbpwW1Y52A972+9gYJMadSfm4bUHA==", //nolint:lll
|
||||||
|
}
|
||||||
|
return utils.OpenVPNConfig(providerSettings, connection, settings)
|
||||||
|
}
|
||||||
33
internal/provider/vpnsecure/provider.go
Normal file
33
internal/provider/vpnsecure/provider.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package vpnsecure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/vpnsecure/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
storage common.Storage
|
||||||
|
randSource rand.Source
|
||||||
|
utils.NoPortForwarder
|
||||||
|
common.Fetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
|
client *http.Client, updaterWarner common.Warner,
|
||||||
|
parallelResolver common.ParallelResolver) *Provider {
|
||||||
|
return &Provider{
|
||||||
|
storage: storage,
|
||||||
|
randSource: randSource,
|
||||||
|
NoPortForwarder: utils.NewNoPortForwarding(providers.VPNSecure),
|
||||||
|
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Name() string {
|
||||||
|
return providers.VPNSecure
|
||||||
|
}
|
||||||
26
internal/provider/vpnsecure/updater/helpers_test.go
Normal file
26
internal/provider/vpnsecure/updater/helpers_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseTestHTML(t *testing.T, htmlString string) *html.Node {
|
||||||
|
t.Helper()
|
||||||
|
rootNode, err := html.Parse(strings.NewReader(htmlString))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return rootNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTestDataIndexHTML(t *testing.T) *html.Node {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
data, err := os.ReadFile("testdata/index.html")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return parseTestHTML(t, string(data))
|
||||||
|
}
|
||||||
38
internal/provider/vpnsecure/updater/hosttoserver.go
Normal file
38
internal/provider/vpnsecure/updater/hosttoserver.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hostToServer map[string]models.Server
|
||||||
|
|
||||||
|
func (hts hostToServer) toHostsSlice() (hosts []string) {
|
||||||
|
hosts = make([]string, 0, len(hts))
|
||||||
|
for host := range hts {
|
||||||
|
hosts = append(hosts, host)
|
||||||
|
}
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]net.IP) {
|
||||||
|
for host, IPs := range hostToIPs {
|
||||||
|
server := hts[host]
|
||||||
|
server.IPs = IPs
|
||||||
|
hts[host] = server
|
||||||
|
}
|
||||||
|
for host, server := range hts {
|
||||||
|
if len(server.IPs) == 0 {
|
||||||
|
delete(hts, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hts hostToServer) toServersSlice() (servers []models.Server) {
|
||||||
|
servers = make([]models.Server, 0, len(hts))
|
||||||
|
for _, server := range hts {
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
26
internal/provider/vpnsecure/updater/resolve.go
Normal file
26
internal/provider/vpnsecure/updater/resolve.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
|
||||||
|
const (
|
||||||
|
maxDuration = 5 * time.Second
|
||||||
|
maxFailRatio = 0.1
|
||||||
|
maxNoNew = 2
|
||||||
|
maxFails = 3
|
||||||
|
)
|
||||||
|
return resolver.ParallelSettings{
|
||||||
|
Hosts: hosts,
|
||||||
|
MaxFailRatio: maxFailRatio,
|
||||||
|
Repeat: resolver.RepeatSettings{
|
||||||
|
MaxDuration: maxDuration,
|
||||||
|
MaxNoNew: maxNoNew,
|
||||||
|
MaxFails: maxFails,
|
||||||
|
SortIPs: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
57
internal/provider/vpnsecure/updater/servers.go
Normal file
57
internal/provider/vpnsecure/updater/servers.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"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) {
|
||||||
|
servers, err = fetchServers(ctx, u.client, u.warner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch servers: %w", err)
|
||||||
|
} else if len(servers) < minServers {
|
||||||
|
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||||
|
common.ErrNotEnoughServers, len(servers), minServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
hts := make(hostToServer, len(servers))
|
||||||
|
for _, server := range servers {
|
||||||
|
hts[server.Hostname] = server
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := hts.toHostsSlice()
|
||||||
|
|
||||||
|
resolveSettings := parallelResolverSettings(hosts)
|
||||||
|
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
u.warner.Warn(warning)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hostToIPs) < minServers {
|
||||||
|
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||||
|
common.ErrNotEnoughServers, len(servers), minServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
hts.adaptWithIPs(hostToIPs)
|
||||||
|
|
||||||
|
servers = hts.toServersSlice()
|
||||||
|
|
||||||
|
for i := range servers {
|
||||||
|
servers[i].VPN = vpn.OpenVPN
|
||||||
|
servers[i].UDP = true
|
||||||
|
servers[i].TCP = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(models.SortableServers(servers))
|
||||||
|
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
7345
internal/provider/vpnsecure/updater/testdata/index.html
vendored
Normal file
7345
internal/provider/vpnsecure/updater/testdata/index.html
vendored
Normal file
File diff suppressed because one or more lines are too long
22
internal/provider/vpnsecure/updater/updater.go
Normal file
22
internal/provider/vpnsecure/updater/updater.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Updater struct {
|
||||||
|
client *http.Client
|
||||||
|
parallelResolver common.ParallelResolver
|
||||||
|
warner common.Warner
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(client *http.Client, warner common.Warner,
|
||||||
|
parallelResolver common.ParallelResolver) *Updater {
|
||||||
|
return &Updater{
|
||||||
|
client: client,
|
||||||
|
parallelResolver: parallelResolver,
|
||||||
|
warner: warner,
|
||||||
|
}
|
||||||
|
}
|
||||||
239
internal/provider/vpnsecure/updater/website.go
Normal file
239
internal/provider/vpnsecure/updater/website.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
|
htmlutils "github.com/qdm12/gluetun/internal/updater/html"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fetchServers(ctx context.Context, client *http.Client,
|
||||||
|
warner common.Warner) (servers []models.Server, err error) {
|
||||||
|
const url = "https://www.vpnsecure.me/vpn-locations/"
|
||||||
|
rootNode, err := htmlutils.Fetch(ctx, client, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching HTML code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
servers, warnings, err := parseHTML(rootNode)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
warner.Warn(warning)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing HTML code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrHTMLServersDivNotFound = errors.New("HTML servers container div not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
const divString = "div"
|
||||||
|
|
||||||
|
func parseHTML(rootNode *html.Node) (servers []models.Server,
|
||||||
|
warnings []string, err error) {
|
||||||
|
// Find div container for all servers, searching with BFS.
|
||||||
|
serversDiv := findServersDiv(rootNode)
|
||||||
|
if serversDiv == nil {
|
||||||
|
return nil, nil, htmlutils.WrapError(ErrHTMLServersDivNotFound, rootNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
for countryNode := serversDiv.FirstChild; countryNode != nil; countryNode = countryNode.NextSibling {
|
||||||
|
if countryNode.Data != divString {
|
||||||
|
// empty line(s) and tab(s)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
country := findCountry(countryNode)
|
||||||
|
if country == "" {
|
||||||
|
warnings = append(warnings, htmlutils.WrapWarning("country not found", countryNode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
grid := htmlutils.BFS(countryNode, matchGridDiv)
|
||||||
|
if grid == nil {
|
||||||
|
warnings = append(warnings, htmlutils.WrapWarning("grid div not found", countryNode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
gridItems := htmlutils.DirectChildren(grid, matchGridItem)
|
||||||
|
if len(gridItems) == 0 {
|
||||||
|
warnings = append(warnings, htmlutils.WrapWarning("no grid item found", grid))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gridItem := range gridItems {
|
||||||
|
server, warning := parseHTMLGridItem(gridItem)
|
||||||
|
if warning != "" {
|
||||||
|
warnings = append(warnings, warning)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Country = country
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers, warnings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHTMLGridItem(gridItem *html.Node) (
|
||||||
|
server models.Server, warning string) {
|
||||||
|
gridItemDT := htmlutils.DirectChild(gridItem, matchDT)
|
||||||
|
if gridItemDT == nil {
|
||||||
|
return server, htmlutils.WrapWarning("grid item <dt> not found", gridItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
host := findHost(gridItemDT)
|
||||||
|
if host == "" {
|
||||||
|
return server, htmlutils.WrapWarning("host not found", gridItemDT)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := findStatus(gridItemDT)
|
||||||
|
if !strings.EqualFold(status, "up") {
|
||||||
|
warning := fmt.Sprintf("skipping server with host %s which has status %q", host, status)
|
||||||
|
warning = htmlutils.WrapWarning(warning, gridItemDT)
|
||||||
|
return server, warning
|
||||||
|
}
|
||||||
|
|
||||||
|
gridItemDD := htmlutils.DirectChild(gridItem, matchDD)
|
||||||
|
if gridItemDD == nil {
|
||||||
|
return server, htmlutils.WrapWarning("grid item dd not found", gridItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
region := findSpanStrong(gridItemDD, "Region:")
|
||||||
|
if region == "" {
|
||||||
|
warning := fmt.Sprintf("region for host %s not found", host)
|
||||||
|
return server, htmlutils.WrapWarning(warning, gridItemDD)
|
||||||
|
}
|
||||||
|
|
||||||
|
city := findSpanStrong(gridItemDD, "City:")
|
||||||
|
if city == "" {
|
||||||
|
warning := fmt.Sprintf("region for host %s not found", host)
|
||||||
|
return server, htmlutils.WrapWarning(warning, gridItemDD)
|
||||||
|
}
|
||||||
|
|
||||||
|
premiumString := findSpanStrong(gridItemDD, "Premium:")
|
||||||
|
if premiumString == "" {
|
||||||
|
warning := fmt.Sprintf("premium for host %s not found", host)
|
||||||
|
return server, htmlutils.WrapWarning(warning, gridItemDD)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.Server{
|
||||||
|
Region: region,
|
||||||
|
City: city,
|
||||||
|
Hostname: host + ".isponeder.com",
|
||||||
|
Premium: strings.EqualFold(premiumString, "yes"),
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCountry(countryNode *html.Node) (country string) {
|
||||||
|
for node := countryNode.FirstChild; node != nil; node = node.NextSibling {
|
||||||
|
if node.Data != "a" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for subNode := node.FirstChild; subNode != nil; subNode = subNode.NextSibling {
|
||||||
|
if subNode.Data != "h4" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return subNode.FirstChild.Data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findServersDiv(rootNode *html.Node) (serversDiv *html.Node) {
|
||||||
|
locationsDiv := htmlutils.BFS(rootNode, matchLocationsListDiv)
|
||||||
|
if locationsDiv == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return htmlutils.BFS(locationsDiv, matchServersDiv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHost(gridItemDT *html.Node) (host string) {
|
||||||
|
hostNode := htmlutils.DirectChild(gridItemDT, matchText)
|
||||||
|
return strings.TrimSpace(hostNode.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchText(node *html.Node) (match bool) {
|
||||||
|
if node.Type != html.TextNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
data := strings.TrimSpace(node.Data)
|
||||||
|
return data != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findStatus(gridItemDT *html.Node) (status string) {
|
||||||
|
statusNode := htmlutils.DirectChild(gridItemDT, matchStatusSpan)
|
||||||
|
return strings.TrimSpace(statusNode.FirstChild.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchServersDiv(node *html.Node) (match bool) {
|
||||||
|
return node != nil && node.Data == divString &&
|
||||||
|
htmlutils.HasClassStrings(node, "blk__i")
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchLocationsListDiv(node *html.Node) (match bool) {
|
||||||
|
return node != nil && node.Data == divString &&
|
||||||
|
htmlutils.HasClassStrings(node, "locations-list")
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchGridDiv(node *html.Node) (match bool) {
|
||||||
|
return node != nil && node.Data == divString &&
|
||||||
|
htmlutils.HasClassStrings(node, "grid--locations")
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchGridItem(node *html.Node) (match bool) {
|
||||||
|
return node != nil && node.Data == "dl" &&
|
||||||
|
htmlutils.HasClassStrings(node, "grid__i")
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchDT(node *html.Node) (match bool) {
|
||||||
|
return node != nil && node.Data == "dt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchDD(node *html.Node) (match bool) {
|
||||||
|
return node != nil && node.Data == "dd"
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStatusSpan(node *html.Node) (match bool) {
|
||||||
|
return node.Data == "span" && htmlutils.HasClassStrings(node, "status")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSpanStrong(gridItemDD *html.Node, spanData string) (
|
||||||
|
strongValue string) {
|
||||||
|
spanFound := false
|
||||||
|
for child := gridItemDD.FirstChild; child != nil; child = child.NextSibling {
|
||||||
|
if !htmlutils.MatchData("div")(child) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for subchild := child.FirstChild; subchild != nil; subchild = subchild.NextSibling {
|
||||||
|
if htmlutils.MatchData("span")(subchild) && subchild.FirstChild.Data == spanData {
|
||||||
|
spanFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !spanFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for subchild := child.FirstChild; subchild != nil; subchild = subchild.NextSibling {
|
||||||
|
if htmlutils.MatchData("strong")(subchild) {
|
||||||
|
return subchild.FirstChild.Data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
235
internal/provider/vpnsecure/updater/website_test.go
Normal file
235
internal/provider/vpnsecure/updater/website_test.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
return f(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_fetchServers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
ctx context.Context
|
||||||
|
responseStatus int
|
||||||
|
responseBody io.ReadCloser
|
||||||
|
servers []models.Server
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"context canceled": {
|
||||||
|
ctx: canceledCtx,
|
||||||
|
errWrapped: context.Canceled,
|
||||||
|
errMessage: `fetching HTML code: Get "https://www.vpnsecure.me/vpn-locations/": context canceled`,
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
ctx: context.Background(),
|
||||||
|
responseStatus: http.StatusOK,
|
||||||
|
responseBody: ioutil.NopCloser(strings.NewReader(`
|
||||||
|
<div class="blk blk--white locations-list">
|
||||||
|
<div class="blk__i">
|
||||||
|
<div>
|
||||||
|
<a href="https://www.vpnsecure.me/vpn-locations/australia/">
|
||||||
|
<h4>Australia</h4>
|
||||||
|
</a>
|
||||||
|
<div class="grid grid--3 grid--locations">
|
||||||
|
<dl class="grid__i">
|
||||||
|
<dt>
|
||||||
|
au1
|
||||||
|
<span class="status status--up">up</span>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<div><span>City:</span> <strong>City</strong></div>
|
||||||
|
<div><span>Region:</span> <strong>Region</strong></div>
|
||||||
|
<div><span>Premium:</span> <strong>YES</strong></div>
|
||||||
|
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)),
|
||||||
|
servers: []models.Server{
|
||||||
|
{
|
||||||
|
Country: "Australia",
|
||||||
|
City: "City",
|
||||||
|
Region: "Region",
|
||||||
|
Hostname: "au1.isponeder.com",
|
||||||
|
Premium: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
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.vpnsecure.me/vpn-locations/")
|
||||||
|
|
||||||
|
ctxErr := r.Context().Err()
|
||||||
|
if ctxErr != nil {
|
||||||
|
return nil, ctxErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Status: http.StatusText(testCase.responseStatus),
|
||||||
|
Body: testCase.responseBody,
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
warner := common.NewMockWarner(ctrl)
|
||||||
|
|
||||||
|
servers, err := fetchServers(testCase.ctx, client, warner)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.servers, servers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_parseHTML(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
rootNode *html.Node
|
||||||
|
servers []models.Server
|
||||||
|
warnings []string
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"empty html": {
|
||||||
|
rootNode: parseTestHTML(t, ""),
|
||||||
|
errWrapped: ErrHTMLServersDivNotFound,
|
||||||
|
errMessage: `HTML servers container div not found: in HTML code: <html><head></head><body></body></html>`,
|
||||||
|
},
|
||||||
|
"test data": {
|
||||||
|
rootNode: parseTestDataIndexHTML(t),
|
||||||
|
warnings: []string{
|
||||||
|
"no grid item found: in HTML code: <div class=\"grid grid--3 grid--locations\">\n </div>",
|
||||||
|
},
|
||||||
|
//nolint:lll
|
||||||
|
servers: []models.Server{
|
||||||
|
{Country: "Australia", Region: "Queensland", City: "Brisbane", Hostname: "au1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Australia", Region: "New South Wales", City: "Sydney", Hostname: "au2.isponeder.com"},
|
||||||
|
{Country: "Australia", Region: "New South Wales", City: "Sydney", Hostname: "au3.isponeder.com"},
|
||||||
|
{Country: "Australia", Region: "New South Wales", City: "Sydney", Hostname: "au4.isponeder.com", Premium: true},
|
||||||
|
{Country: "Austria", Region: "Vienna", City: "Vienna", Hostname: "at1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Austria", Region: "Vienna", City: "Vienna", Hostname: "at2.isponeder.com"},
|
||||||
|
{Country: "Brazil", Region: "Sao Paulo", City: "Sao Paulo", Hostname: "br1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Belgium", Region: "Flanders", City: "Zaventem", Hostname: "be1.isponeder.com"},
|
||||||
|
{Country: "Belgium", Region: "Brussels Hoofdstedelijk Gewest", City: "Brussel", Hostname: "be2.isponeder.com"},
|
||||||
|
{Country: "Canada", Region: "Ontario", City: "Richmond Hill", Hostname: "ca1.isponeder.com"},
|
||||||
|
{Country: "Canada", Region: "Ontario", City: "Richmond Hill", Hostname: "ca2.isponeder.com"},
|
||||||
|
{Country: "Canada", Region: "Quebec", City: "Montréal", Hostname: "ca3.isponeder.com", Premium: true},
|
||||||
|
{Country: "Denmark", Region: "Capital Region", City: "Copenhagen", Hostname: "dk1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Denmark", Region: "Capital Region", City: "Copenhagen", Hostname: "dk2.isponeder.com", Premium: true},
|
||||||
|
{Country: "Denmark", Region: "Capital Region", City: "Ballerup", Hostname: "dk3.isponeder.com"},
|
||||||
|
{Country: "France", Region: "Île-de-France", City: "Paris", Hostname: "fr1.isponeder.com"},
|
||||||
|
{Country: "France", Region: "Île-de-France", City: "Paris", Hostname: "fr2.isponeder.com"},
|
||||||
|
{Country: "France", Region: "Grand Est", City: "Strasbourg", Hostname: "fr3.isponeder.com"},
|
||||||
|
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de1.isponeder.com"},
|
||||||
|
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de2.isponeder.com"},
|
||||||
|
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de3.isponeder.com"},
|
||||||
|
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de4.isponeder.com"},
|
||||||
|
{Country: "Germany", Region: "Hesse", City: "Limburg an der Lahn", Hostname: "de5.isponeder.com"},
|
||||||
|
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de6.isponeder.com"},
|
||||||
|
{Country: "Hungary", Region: "Budapest", City: "Budapest", Hostname: "hu1.isponeder.com", Premium: true},
|
||||||
|
{Country: "India", Region: "Karnataka", City: "Doddaballapura", Hostname: "in1.isponeder.com"},
|
||||||
|
{Country: "Indonesia", Region: "Special Capital Region of Jakarta", City: "Jakarta", Hostname: "id1.isponeder.com"},
|
||||||
|
{Country: "Ireland", Region: "Dublin City", City: "Dublin", Hostname: "ie1.isponeder.com"},
|
||||||
|
{Country: "Israel", Region: "Tel Aviv", City: "Tel Aviv", Hostname: "il1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Italy", Region: "Lombardy", City: "Milan", Hostname: "it1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Japan", Region: "Tokyo", City: "Tokyo", Hostname: "jp2.isponeder.com", Premium: true},
|
||||||
|
{Country: "Mexico", Region: "México", City: "Ampliación San Mateo (Colonia Solidaridad)", Hostname: "mx1.isponeder.com"},
|
||||||
|
{Country: "Netherlands", Region: "North Holland", City: "Haarlem", Hostname: "nl1.isponeder.com"},
|
||||||
|
{Country: "Netherlands", Region: "South Holland", City: "Naaldwijk", Hostname: "nl2.isponeder.com"},
|
||||||
|
{Country: "New Zealand", Region: "Auckland", City: "Auckland", Hostname: "nz1.isponeder.com"},
|
||||||
|
{Country: "Norway", Region: "Oslo", City: "Oslo", Hostname: "no1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Norway", Region: "Stockholm", City: "Stockholm", Hostname: "no2.isponeder.com", Premium: true},
|
||||||
|
{Country: "Poland", Region: "Mazovia", City: "Warsaw", Hostname: "pl1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Romania", Region: "Bucure?ti", City: "Bucharest", Hostname: "ro1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Russia", Region: "Moscow", City: "Moscow", Hostname: "ru1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Singapore", Region: "Singapore", City: "Singapore", Hostname: "sg1.isponeder.com", Premium: true},
|
||||||
|
{Country: "South Africa", Region: "Western Cape", City: "Cape Town", Hostname: "za1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Spain", Region: "Madrid", City: "Madrid", Hostname: "es2.isponeder.com"},
|
||||||
|
{Country: "Spain", Region: "Valencia", City: "Valencia", Hostname: "se1.isponeder.com"},
|
||||||
|
{Country: "Sweden", Region: "Stockholm", City: "Stockholm", Hostname: "se2.isponeder.com", Premium: true},
|
||||||
|
{Country: "Sweden", Region: "Stockholm", City: "Stockholm", Hostname: "se3.isponeder.com"},
|
||||||
|
{Country: "Switzerland", Region: "Vaud", City: "Lausanne", Hostname: "ch1.isponeder.com"},
|
||||||
|
{Country: "Switzerland", Region: "Geneva", City: "Geneva", Hostname: "ch1.isponeder.com", Premium: true},
|
||||||
|
{Country: "Switzerland", Region: "Geneva", City: "Genève", Hostname: "ch2.isponeder.com", Premium: true},
|
||||||
|
{Country: "Ukraine", Region: "Poltavs'ka Oblast'", City: "Kremenchuk", Hostname: "ua1.isponeder.com", Premium: true},
|
||||||
|
{Country: "United Arab Emirates", Region: "Maharashtra", City: "Mumbai", Hostname: "ae1.isponeder.com", Premium: true},
|
||||||
|
{Country: "United Kingdom", Region: "England", City: "London", Hostname: "uk2.isponeder.com"},
|
||||||
|
{Country: "United Kingdom", Region: "England", City: "Kent", Hostname: "uk3.isponeder.com"},
|
||||||
|
{Country: "United Kingdom", Region: "England", City: "London", Hostname: "uk4.isponeder.com"},
|
||||||
|
{Country: "United Kingdom", Region: "England", City: "London", Hostname: "uk5.isponeder.com"},
|
||||||
|
{Country: "United Kingdom", Region: "Brent", City: "Harlesden", Hostname: "uk6.isponeder.com"},
|
||||||
|
{Country: "United Kingdom", Region: "England", City: "Manchester", Hostname: "uk7.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "New Jersey", City: "Secaucus", Hostname: "us1.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "New York", City: "New York City", Hostname: "us10.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us11.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us12.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us13.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us14.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us15.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us16.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "New York", City: "New York City", Hostname: "us2.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "Oregon", City: "Portland", Hostname: "us3.isponeder.com", Premium: true},
|
||||||
|
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us4.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us5.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us6.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us7.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "Georgia", City: "Atlanta", Hostname: "us8.isponeder.com"},
|
||||||
|
{Country: "United States", Region: "Georgia", City: "Atlanta", Hostname: "us9.isponeder.com"},
|
||||||
|
{Country: "Hong Kong", Region: "Central and Western", City: "Hong Kong", Hostname: "hk1.isponeder.com"},
|
||||||
|
{Country: "United States West", Region: "California", City: "Los Angeles", Hostname: "us3.isponeder.com", Premium: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
servers, warnings, err := parseHTML(testCase.rootNode)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.servers, servers)
|
||||||
|
assert.Equal(t, testCase.warnings, warnings)
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -114,6 +114,10 @@ func noServerFoundError(selection settings.ServerSelection) (err error) {
|
|||||||
messageParts = append(messageParts, "free tier only")
|
messageParts = append(messageParts, "free tier only")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *selection.PremiumOnly {
|
||||||
|
messageParts = append(messageParts, "premium tier only")
|
||||||
|
}
|
||||||
|
|
||||||
message := "for " + strings.Join(messageParts, "; ")
|
message := "for " + strings.Join(messageParts, "; ")
|
||||||
|
|
||||||
return fmt.Errorf("%w: %s", ErrNoServerFound, message)
|
return fmt.Errorf("%w: %s", ErrNoServerFound, message)
|
||||||
|
|||||||
@ -119755,6 +119755,913 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"vpnsecure": {
|
||||||
|
"version": 1,
|
||||||
|
"timestamp": 1655858488,
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Australia",
|
||||||
|
"region": "New South Wales",
|
||||||
|
"city": "Sydney",
|
||||||
|
"hostname": "au2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"139.99.131.191"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Australia",
|
||||||
|
"region": "New South Wales",
|
||||||
|
"city": "Sydney",
|
||||||
|
"hostname": "au3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"37.120.234.22"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Australia",
|
||||||
|
"region": "New South Wales",
|
||||||
|
"city": "Sydney",
|
||||||
|
"hostname": "au4.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"217.138.205.151"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Australia",
|
||||||
|
"region": "Queensland",
|
||||||
|
"city": "Brisbane",
|
||||||
|
"hostname": "au1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"51.161.157.216"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Austria",
|
||||||
|
"region": "Vienna",
|
||||||
|
"city": "Vienna",
|
||||||
|
"hostname": "at1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"185.236.202.181"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Austria",
|
||||||
|
"region": "Vienna",
|
||||||
|
"city": "Vienna",
|
||||||
|
"hostname": "at2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"91.151.16.17"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Belgium",
|
||||||
|
"region": "Brussels Hoofdstedelijk Gewest",
|
||||||
|
"city": "Brussel",
|
||||||
|
"hostname": "be2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"37.120.236.166"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Belgium",
|
||||||
|
"region": "Flanders",
|
||||||
|
"city": "Zaventem",
|
||||||
|
"hostname": "be1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"37.120.236.166"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Brazil",
|
||||||
|
"region": "Sao Paulo",
|
||||||
|
"city": "Sao Paulo",
|
||||||
|
"hostname": "br1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"154.16.57.215"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Canada",
|
||||||
|
"region": "Ontario",
|
||||||
|
"city": "Richmond Hill",
|
||||||
|
"hostname": "ca1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"149.56.46.132"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Canada",
|
||||||
|
"region": "Ontario",
|
||||||
|
"city": "Richmond Hill",
|
||||||
|
"hostname": "ca2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.222.50.187"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Canada",
|
||||||
|
"region": "Quebec",
|
||||||
|
"city": "Montréal",
|
||||||
|
"hostname": "ca3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"67.43.234.50"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Denmark",
|
||||||
|
"region": "Capital Region",
|
||||||
|
"city": "Ballerup",
|
||||||
|
"hostname": "dk3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"37.120.145.132"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Denmark",
|
||||||
|
"region": "Capital Region",
|
||||||
|
"city": "Copenhagen",
|
||||||
|
"hostname": "dk1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"192.36.27.55"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Denmark",
|
||||||
|
"region": "Capital Region",
|
||||||
|
"city": "Copenhagen",
|
||||||
|
"hostname": "dk2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"192.36.27.61"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "France",
|
||||||
|
"region": "Grand Est",
|
||||||
|
"city": "Strasbourg",
|
||||||
|
"hostname": "fr3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"151.80.148.41"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "France",
|
||||||
|
"region": "Île-de-France",
|
||||||
|
"city": "Paris",
|
||||||
|
"hostname": "fr1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"87.98.158.99"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "France",
|
||||||
|
"region": "Île-de-France",
|
||||||
|
"city": "Paris",
|
||||||
|
"hostname": "fr2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"87.98.158.117"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Germany",
|
||||||
|
"region": "Hesse",
|
||||||
|
"city": "Frankfurt am Main",
|
||||||
|
"hostname": "de1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.125.201.229"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Germany",
|
||||||
|
"region": "Hesse",
|
||||||
|
"city": "Frankfurt am Main",
|
||||||
|
"hostname": "de2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.195.113.80"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Germany",
|
||||||
|
"region": "Hesse",
|
||||||
|
"city": "Frankfurt am Main",
|
||||||
|
"hostname": "de3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.195.113.82"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Germany",
|
||||||
|
"region": "Hesse",
|
||||||
|
"city": "Frankfurt am Main",
|
||||||
|
"hostname": "de4.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.195.37.144"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Germany",
|
||||||
|
"region": "Hesse",
|
||||||
|
"city": "Frankfurt am Main",
|
||||||
|
"hostname": "de6.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.195.37.144"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Germany",
|
||||||
|
"region": "Hesse",
|
||||||
|
"city": "Limburg an der Lahn",
|
||||||
|
"hostname": "de5.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.125.183.212"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Hong Kong",
|
||||||
|
"region": "Central and Western",
|
||||||
|
"city": "Hong Kong",
|
||||||
|
"hostname": "hk1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"103.253.43.30"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Hungary",
|
||||||
|
"region": "Budapest",
|
||||||
|
"city": "Budapest",
|
||||||
|
"hostname": "hu1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"194.71.130.93"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "India",
|
||||||
|
"region": "Karnataka",
|
||||||
|
"city": "Doddaballapura",
|
||||||
|
"hostname": "in1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"142.93.222.157"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Indonesia",
|
||||||
|
"region": "Special Capital Region of Jakarta",
|
||||||
|
"city": "Jakarta",
|
||||||
|
"hostname": "id1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"45.114.118.84"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Ireland",
|
||||||
|
"region": "Dublin City",
|
||||||
|
"city": "Dublin",
|
||||||
|
"hostname": "ie1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"185.224.197.60"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Israel",
|
||||||
|
"region": "Tel Aviv",
|
||||||
|
"city": "Tel Aviv",
|
||||||
|
"hostname": "il1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"193.182.144.18"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Italy",
|
||||||
|
"region": "Lombardy",
|
||||||
|
"city": "Milan",
|
||||||
|
"hostname": "it1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"149.154.157.94"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Japan",
|
||||||
|
"region": "Tokyo",
|
||||||
|
"city": "Tokyo",
|
||||||
|
"hostname": "jp2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"194.68.27.45"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Mexico",
|
||||||
|
"region": "México",
|
||||||
|
"city": "Ampliación San Mateo (Colonia Solidaridad)",
|
||||||
|
"hostname": "mx1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"190.103.179.17"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Netherlands",
|
||||||
|
"region": "North Holland",
|
||||||
|
"city": "Haarlem",
|
||||||
|
"hostname": "nl1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.15.2.92"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Netherlands",
|
||||||
|
"region": "South Holland",
|
||||||
|
"city": "Naaldwijk",
|
||||||
|
"hostname": "nl2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"212.83.133.203"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "New Zealand",
|
||||||
|
"region": "Auckland",
|
||||||
|
"city": "Auckland",
|
||||||
|
"hostname": "nz1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"185.121.168.31"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Norway",
|
||||||
|
"region": "Oslo",
|
||||||
|
"city": "Oslo",
|
||||||
|
"hostname": "no1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"194.68.32.36"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Norway",
|
||||||
|
"region": "Stockholm",
|
||||||
|
"city": "Stockholm",
|
||||||
|
"hostname": "no2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"194.68.32.36"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Poland",
|
||||||
|
"region": "Mazovia",
|
||||||
|
"city": "Warsaw",
|
||||||
|
"hostname": "pl1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"89.207.169.53"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Romania",
|
||||||
|
"region": "Bucure?ti",
|
||||||
|
"city": "Bucharest",
|
||||||
|
"hostname": "ro1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"5.252.178.107"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Russia",
|
||||||
|
"region": "Moscow",
|
||||||
|
"city": "Moscow",
|
||||||
|
"hostname": "ru1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"213.183.56.97"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Singapore",
|
||||||
|
"region": "Singapore",
|
||||||
|
"city": "Singapore",
|
||||||
|
"hostname": "sg1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"139.99.57.42"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "South Africa",
|
||||||
|
"region": "Western Cape",
|
||||||
|
"city": "Cape Town",
|
||||||
|
"hostname": "za1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"102.165.60.248"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Spain",
|
||||||
|
"region": "Madrid",
|
||||||
|
"city": "Madrid",
|
||||||
|
"hostname": "es2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"95.85.89.55"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Spain",
|
||||||
|
"region": "Valencia",
|
||||||
|
"city": "Valencia",
|
||||||
|
"hostname": "se1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"79.141.174.55"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Sweden",
|
||||||
|
"region": "Stockholm",
|
||||||
|
"city": "Stockholm",
|
||||||
|
"hostname": "se2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"178.73.210.95"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Sweden",
|
||||||
|
"region": "Stockholm",
|
||||||
|
"city": "Stockholm",
|
||||||
|
"hostname": "se3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"185.117.89.229"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Switzerland",
|
||||||
|
"region": "Geneva",
|
||||||
|
"city": "Geneva",
|
||||||
|
"hostname": "ch1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"45.90.57.209"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Switzerland",
|
||||||
|
"region": "Geneva",
|
||||||
|
"city": "Genève",
|
||||||
|
"hostname": "ch2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"45.90.58.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "Ukraine",
|
||||||
|
"region": "Poltavs'ka Oblast'",
|
||||||
|
"city": "Kremenchuk",
|
||||||
|
"hostname": "ua1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"139.28.36.34"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United Arab Emirates",
|
||||||
|
"region": "Maharashtra",
|
||||||
|
"city": "Mumbai",
|
||||||
|
"hostname": "ae1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"103.57.251.73"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United Kingdom",
|
||||||
|
"region": "Brent",
|
||||||
|
"city": "Harlesden",
|
||||||
|
"hostname": "uk6.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"45.141.154.190"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United Kingdom",
|
||||||
|
"region": "England",
|
||||||
|
"city": "Kent",
|
||||||
|
"hostname": "uk3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"185.62.86.143"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United Kingdom",
|
||||||
|
"region": "England",
|
||||||
|
"city": "London",
|
||||||
|
"hostname": "uk2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.195.221.43"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United Kingdom",
|
||||||
|
"region": "England",
|
||||||
|
"city": "London",
|
||||||
|
"hostname": "uk4.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"185.62.86.142"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United Kingdom",
|
||||||
|
"region": "England",
|
||||||
|
"city": "London",
|
||||||
|
"hostname": "uk5.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"45.141.154.190",
|
||||||
|
"46.17.63.208"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United Kingdom",
|
||||||
|
"region": "England",
|
||||||
|
"city": "Manchester",
|
||||||
|
"hostname": "uk7.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"77.243.187.81"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "California",
|
||||||
|
"city": "Los Angeles",
|
||||||
|
"hostname": "us11.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"51.81.208.46"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "California",
|
||||||
|
"city": "Los Angeles",
|
||||||
|
"hostname": "us13.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"167.160.91.13"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "California",
|
||||||
|
"city": "Los Angeles",
|
||||||
|
"hostname": "us14.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"167.160.91.14"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "California",
|
||||||
|
"city": "Los Angeles",
|
||||||
|
"hostname": "us15.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"167.160.91.10"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "California",
|
||||||
|
"city": "Los Angeles",
|
||||||
|
"hostname": "us5.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"167.160.91.11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "California",
|
||||||
|
"city": "Los Angeles",
|
||||||
|
"hostname": "us6.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"167.160.91.12"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "Georgia",
|
||||||
|
"city": "Atlanta",
|
||||||
|
"hostname": "us8.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"64.42.181.50"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "Georgia",
|
||||||
|
"city": "Atlanta",
|
||||||
|
"hostname": "us9.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.148.27.95"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "Illinois",
|
||||||
|
"city": "Chicago",
|
||||||
|
"hostname": "us12.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"64.42.183.139"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "Illinois",
|
||||||
|
"city": "Chicago",
|
||||||
|
"hostname": "us16.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"66.23.205.83"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "Illinois",
|
||||||
|
"city": "Chicago",
|
||||||
|
"hostname": "us4.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.148.27.95"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "Illinois",
|
||||||
|
"city": "Chicago",
|
||||||
|
"hostname": "us7.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.148.27.95"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "New Jersey",
|
||||||
|
"city": "Secaucus",
|
||||||
|
"hostname": "us1.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.148.100.25"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "New York",
|
||||||
|
"city": "New York City",
|
||||||
|
"hostname": "us10.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.148.27.95"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States",
|
||||||
|
"region": "New York",
|
||||||
|
"city": "New York City",
|
||||||
|
"hostname": "us2.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"ips": [
|
||||||
|
"135.148.27.95"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vpn": "openvpn",
|
||||||
|
"country": "United States West",
|
||||||
|
"region": "California",
|
||||||
|
"city": "Los Angeles",
|
||||||
|
"hostname": "us3.isponeder.com",
|
||||||
|
"tcp": true,
|
||||||
|
"udp": true,
|
||||||
|
"premium": true,
|
||||||
|
"ips": [
|
||||||
|
"167.160.91.10"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"vyprvpn": {
|
"vyprvpn": {
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"timestamp": 1627008363,
|
"timestamp": 1627008363,
|
||||||
|
|||||||
43
internal/updater/html/fetch.go
Normal file
43
internal/updater/html/fetch.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code is not OK")
|
||||||
|
|
||||||
|
func Fetch(ctx context.Context, client *http.Client, url string) (
|
||||||
|
rootNode *html.Node, err error) {
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||||
|
response.StatusCode, response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNode, err = html.Parse(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
_ = response.Body.Close()
|
||||||
|
return nil, fmt.Errorf("parsing HTML code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("closing response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNode, nil
|
||||||
|
}
|
||||||
98
internal/updater/html/fetch_test.go
Normal file
98
internal/updater/html/fetch_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseTestHTML(t *testing.T, htmlString string) *html.Node {
|
||||||
|
t.Helper()
|
||||||
|
rootNode, err := html.Parse(strings.NewReader(htmlString))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return rootNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
return f(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Fetch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
ctx context.Context
|
||||||
|
url string
|
||||||
|
responseStatus int
|
||||||
|
responseBody io.ReadCloser
|
||||||
|
rootNode *html.Node
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"context canceled": {
|
||||||
|
ctx: canceledCtx,
|
||||||
|
url: "https://example.com/path",
|
||||||
|
errWrapped: context.Canceled,
|
||||||
|
errMessage: `Get "https://example.com/path": context canceled`,
|
||||||
|
},
|
||||||
|
"response status not ok": {
|
||||||
|
ctx: context.Background(),
|
||||||
|
url: "https://example.com/path",
|
||||||
|
responseStatus: http.StatusNotFound,
|
||||||
|
errWrapped: ErrHTTPStatusCodeNotOK,
|
||||||
|
errMessage: `HTTP status code is not OK: 404 Not Found`,
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
ctx: context.Background(),
|
||||||
|
url: "https://example.com/path",
|
||||||
|
responseStatus: http.StatusOK,
|
||||||
|
rootNode: parseTestHTML(t, "some body"),
|
||||||
|
responseBody: ioutil.NopCloser(strings.NewReader("some body")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
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(), testCase.url)
|
||||||
|
|
||||||
|
ctxErr := r.Context().Err()
|
||||||
|
if ctxErr != nil {
|
||||||
|
return nil, ctxErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: testCase.responseStatus,
|
||||||
|
Status: http.StatusText(testCase.responseStatus),
|
||||||
|
Body: testCase.responseBody,
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNode, err := Fetch(testCase.ctx, client, testCase.url)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.rootNode, rootNode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@ type PortForward interface {
|
|||||||
type OpenVPN interface {
|
type OpenVPN interface {
|
||||||
WriteConfig(lines []string) error
|
WriteConfig(lines []string) error
|
||||||
WriteAuthFile(user, password string) error
|
WriteAuthFile(user, password string) error
|
||||||
|
WriteAskPassFile(passphrase string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Providers interface {
|
type Providers interface {
|
||||||
|
|||||||
@ -34,6 +34,13 @@ func setupOpenVPN(ctx context.Context, fw Firewall,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *settings.OpenVPN.KeyPassphrase != "" {
|
||||||
|
err := openvpnConf.WriteAskPassFile(*settings.OpenVPN.KeyPassphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("writing askpass file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := fw.SetVPNConnection(ctx, connection, settings.OpenVPN.Interface); err != nil {
|
if err := fw.SetVPNConnection(ctx, connection, settings.OpenVPN.Interface); err != nil {
|
||||||
return nil, "", fmt.Errorf("failed allowing VPN connection through firewall: %w", err)
|
return nil, "", fmt.Errorf("failed allowing VPN connection through firewall: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user