Skip to content

Commit

Permalink
feat: add VPNsecure.me support (#848)
Browse files Browse the repository at this point in the history
- `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
  • Loading branch information
qdm12 committed Aug 15, 2022
1 parent 991cfb8 commit a182e35
Show file tree
Hide file tree
Showing 41 changed files with 9,356 additions and 163 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Expand Up @@ -57,6 +57,7 @@ body:
- SlickVPN
- Surfshark
- TorGuard
- VPNSecure.me
- VPNUnlimited
- VyprVPN
- WeVPN
Expand Down
2 changes: 2 additions & 0 deletions .github/labels.yml
Expand Up @@ -73,6 +73,8 @@
- name: ":cloud: Torguard"
color: "cfe8d4"
description: ""
- name: ":cloud: VPNSecure.me"
color: "cfe8d4"
- name: ":cloud: VPNUnlimited"
color: "cfe8d4"
description: ""
Expand Down
7 changes: 7 additions & 0 deletions Dockerfile
Expand Up @@ -115,6 +115,11 @@ ENV VPN_SERVICE_PROVIDER=pia \
OPENVPN_KEY= \
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
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:
SERVER_NUMBER= \
# # PIA only:
Expand All @@ -123,6 +128,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
FREE_ONLY= \
# # Surfshark only:
MULTIHOP_ONLY= \
# # VPN Secure only:
PREMIUM_ONLY= \
# Firewall
FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -58,7 +58,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
## Features

- 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 Wireguard both kernelspace and userspace
- For **Mullvad**, **Ivpn** and **Windscribe**
Expand Down
1 change: 1 addition & 0 deletions internal/configuration/settings/errors.go
Expand Up @@ -17,6 +17,7 @@ var (
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset 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")
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
Expand Down
81 changes: 65 additions & 16 deletions internal/configuration/settings/openvpn.go
Expand Up @@ -42,7 +42,7 @@ type OpenVPN struct {
// It is ignored if it is set to the empty string.
Auth *string
// 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 cannot be nil in the internal state.
Cert *string
Expand All @@ -51,6 +51,15 @@ type OpenVPN struct {
// It can be set to the empty string to be ignored.
// It cannot be nil in the internal state.
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
// Private Internet Access. It can be set to an
// empty string for other providers.
Expand Down Expand Up @@ -116,6 +125,15 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
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
if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d",
Expand Down Expand Up @@ -164,6 +182,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
switch vpnProvider {
case
providers.Cyberghost,
providers.VPNSecure,
providers.VPNUnlimited:
if clientCert == "" {
return ErrMissingValue
Expand Down Expand Up @@ -203,23 +222,42 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
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) {
return OpenVPN{
Version: o.Version,
User: helpers.CopyStringPtr(o.User),
Password: helpers.CopyStringPtr(o.Password),
ConfFile: helpers.CopyStringPtr(o.ConfFile),
Ciphers: helpers.CopyStringSlice(o.Ciphers),
Auth: helpers.CopyStringPtr(o.Auth),
Cert: helpers.CopyStringPtr(o.Cert),
Key: helpers.CopyStringPtr(o.Key),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
IPv6: helpers.CopyBoolPtr(o.IPv6),
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
Interface: o.Interface,
ProcessUser: o.ProcessUser,
Verbosity: helpers.CopyIntPtr(o.Verbosity),
Flags: helpers.CopyStringSlice(o.Flags),
Version: o.Version,
User: helpers.CopyStringPtr(o.User),
Password: helpers.CopyStringPtr(o.Password),
ConfFile: helpers.CopyStringPtr(o.ConfFile),
Ciphers: helpers.CopyStringSlice(o.Ciphers),
Auth: helpers.CopyStringPtr(o.Auth),
Cert: helpers.CopyStringPtr(o.Cert),
Key: helpers.CopyStringPtr(o.Key),
EncryptedKey: helpers.CopyStringPtr(o.EncryptedKey),
KeyPassphrase: helpers.CopyStringPtr(o.KeyPassphrase),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
IPv6: helpers.CopyBoolPtr(o.IPv6),
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
Interface: o.Interface,
ProcessUser: o.ProcessUser,
Verbosity: helpers.CopyIntPtr(o.Verbosity),
Flags: helpers.CopyStringSlice(o.Flags),
}
}

Expand All @@ -234,6 +272,8 @@ func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
o.Cert = helpers.MergeWithStringPtr(o.Cert, other.Cert)
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.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
Expand All @@ -255,6 +295,8 @@ func (o *OpenVPN) overrideWith(other OpenVPN) {
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
o.Cert = helpers.OverrideWithStringPtr(o.Cert, other.Cert)
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.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
Expand All @@ -277,6 +319,8 @@ func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
o.Cert = helpers.DefaultStringPtr(o.Cert, "")
o.Key = helpers.DefaultStringPtr(o.Key, "")
o.EncryptedKey = helpers.DefaultStringPtr(o.EncryptedKey, "")
o.KeyPassphrase = helpers.DefaultStringPtr(o.KeyPassphrase, "")

var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
Expand Down Expand Up @@ -321,6 +365,11 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
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 != "" {
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/configuration/settings/openvpnselection.go
Expand Up @@ -60,8 +60,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Expressvpn, providers.Fastestvpn,
providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn,
providers.Surfshark, providers.VPNUnlimited,
providers.Vyprvpn:
providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default:
Expand Down
26 changes: 26 additions & 0 deletions internal/configuration/settings/serverselection.go
Expand Up @@ -45,6 +45,10 @@ type ServerSelection struct { //nolint:maligned
// FreeOnly is true if VPN servers that are not free should
// be filtered. This is used with ProtonVPN and VPN Unlimited.
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
// be filtered. This is used with VPNUnlimited.
StreamOnly *bool
Expand All @@ -63,8 +67,10 @@ type ServerSelection struct { //nolint:maligned
var (
ErrOwnedOnlyNotSupported = errors.New("owned 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")
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,
Expand Down Expand Up @@ -103,6 +109,18 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
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 &&
!helpers.IsOneOf(vpnServiceProvider,
providers.Protonvpn,
Expand Down Expand Up @@ -194,6 +212,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
Numbers: helpers.CopyUint16Slice(ss.Numbers),
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
PremiumOnly: helpers.CopyBoolPtr(ss.PremiumOnly),
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Expand All @@ -213,6 +232,7 @@ func (ss *ServerSelection) mergeWith(other ServerSelection) {
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
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.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)

Expand All @@ -232,6 +252,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
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.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.OpenVPN.overrideWith(other.OpenVPN)
Expand All @@ -243,6 +264,7 @@ func (ss *ServerSelection) setDefaults(vpnProvider string) {
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
ss.PremiumOnly = helpers.DefaultBool(ss.PremiumOnly, false)
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
ss.OpenVPN.setDefaults(vpnProvider)
Expand Down Expand Up @@ -299,6 +321,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Free only servers: yes")
}

if *ss.PremiumOnly {
node.Appendf("Premium only servers: yes")
}

if *ss.StreamOnly {
node.Appendf("Stream only servers: yes")
}
Expand Down
19 changes: 18 additions & 1 deletion internal/configuration/sources/env/openvpn.go
Expand Up @@ -11,7 +11,8 @@ import (
func (r *Reader) readOpenVPN() (
openVPN settings.OpenVPN, err error) {
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")
Expand Down Expand Up @@ -40,6 +41,13 @@ func (r *Reader) readOpenVPN() (
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.IPv6, err = envToBoolPtr("OPENVPN_IPV6")
Expand Down Expand Up @@ -94,6 +102,15 @@ func (r *Reader) readOpenVPNPassword() (password *string) {
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) {
value := getCleanedEnv(envKey)
if value == "" {
Expand Down
6 changes: 6 additions & 0 deletions internal/configuration/sources/env/serverselection.go
Expand Up @@ -77,6 +77,12 @@ func (r *Reader) readServerSelection(vpnProvider, vpnType string) (
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
ss.MultiHopOnly, err = envToBoolPtr("MULTIHOP_ONLY")
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions internal/configuration/sources/files/openvpn.go
Expand Up @@ -11,6 +11,7 @@ const (
OpenVPNClientKeyPath = "/gluetun/client.key"
// OpenVPNClientCertificatePath is the OpenVPN client certificate filepath.
OpenVPNClientCertificatePath = "/gluetun/client.crt"
openVPNEncryptedKey = "/gluetun/openvpn_encrypted_key"
)

func (r *Reader) readOpenVPN() (settings settings.OpenVPN, err error) {
Expand All @@ -24,5 +25,10 @@ func (r *Reader) readOpenVPN() (settings settings.OpenVPN, err error) {
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
}
16 changes: 16 additions & 0 deletions internal/configuration/sources/secrets/openvpn.go
Expand Up @@ -32,6 +32,22 @@ func readOpenVPN() (
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(
"OPENVPN_CLIENTCRT_SECRETFILE",
"/run/secrets/openvpn_clientcrt",
Expand Down
3 changes: 3 additions & 0 deletions internal/constants/openvpn/paths.go
Expand Up @@ -3,4 +3,7 @@ package openvpn
const (
// AuthConf is the file path to the OpenVPN auth file.
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
)

0 comments on commit a182e35

Please sign in to comment.