Skip to content

Commit

Permalink
Initial code for VPN secure
Browse files Browse the repository at this point in the history
- Read encrypted private key from /gluetun/encrypted-private-key
  • Loading branch information
qdm12 committed Jun 12, 2022
1 parent fb62de8 commit 689e363
Show file tree
Hide file tree
Showing 31 changed files with 1,630 additions and 24 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ body:
- PureVPN
- Surfshark
- TorGuard
- VPNSecure.me
- VPNUnlimited
- VyprVPN
- WeVPN
Expand Down
2 changes: 2 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
- name: ":cloud: Torguard"
color: "cfe8d4"
description: ""
- name: ":cloud: VPNSecure.me"
color: "cfe8d4"
- name: ":cloud: VPNUnlimited"
color: "cfe8d4"
description: ""
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN,
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNSecure.me, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers
using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*


![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)

[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
Expand Down Expand Up @@ -61,7 +60,7 @@ using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP
## 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var (
ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncPrivateKeyMissing = errors.New("client encrypted private key is missing")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
Expand Down
68 changes: 49 additions & 19 deletions internal/configuration/settings/openvpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type OpenVPN struct {
// It can be set to the empty string to be ignored.
// It cannot be nil in the internal state.
ClientKey *string
// EncryptedPrivateKey is the content of an encrypted
// private key for OpenVPN.
EncryptedPrivateKey *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 @@ -115,6 +118,11 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
return fmt.Errorf("client key: %w", err)
}

err = validateOpenVPNEncryptedPrivateKey(vpnProvider, *o.EncryptedPrivateKey)
if err != nil {
return fmt.Errorf("encrypted private key: %w", err)
}

const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d",
Expand Down Expand Up @@ -174,10 +182,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
}

_, err = parse.ExtractCert([]byte(clientCert))
if err != nil {
return err
}
return nil
return err
}

func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
Expand All @@ -202,23 +207,41 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
return nil
}

func validateOpenVPNEncryptedPrivateKey(vpnProvider,
encryptedPrivateKey string) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return ErrOpenVPNEncPrivateKeyMissing
}

if encryptedPrivateKey == "" {
return nil
}

_, err = parse.ExtractEncryptedPrivateKey([]byte(encryptedPrivateKey))
if err != nil {
return fmt.Errorf("cannot extract encrypted private key: %w", err)
}
return nil
}

func (o *OpenVPN) copy() (copied OpenVPN) {
return OpenVPN{
Version: o.Version,
User: o.User,
Password: o.Password,
ConfFile: helpers.CopyStringPtr(o.ConfFile),
Ciphers: helpers.CopyStringSlice(o.Ciphers),
Auth: helpers.CopyStringPtr(o.Auth),
ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
ClientKey: helpers.CopyStringPtr(o.ClientKey),
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: o.User,
Password: o.Password,
ConfFile: helpers.CopyStringPtr(o.ConfFile),
Ciphers: helpers.CopyStringSlice(o.Ciphers),
Auth: helpers.CopyStringPtr(o.Auth),
ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
ClientKey: helpers.CopyStringPtr(o.ClientKey),
EncryptedPrivateKey: helpers.CopyStringPtr(o.EncryptedPrivateKey),
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 @@ -233,6 +256,7 @@ func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
o.EncryptedPrivateKey = helpers.MergeWithStringPtr(o.EncryptedPrivateKey, other.EncryptedPrivateKey)
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 @@ -254,6 +278,7 @@ func (o *OpenVPN) overrideWith(other OpenVPN) {
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey)
o.EncryptedPrivateKey = helpers.OverrideWithStringPtr(o.EncryptedPrivateKey, other.EncryptedPrivateKey)
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 @@ -273,6 +298,7 @@ func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "")
o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "")
o.EncryptedPrivateKey = helpers.DefaultStringPtr(o.EncryptedPrivateKey, "")

var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
Expand Down Expand Up @@ -317,6 +343,10 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey))
}

if *o.EncryptedPrivateKey != "" {
node.Appendf("Encrypted private key: %s", helpers.ObfuscateData(*o.ClientKey))
}

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
6 changes: 6 additions & 0 deletions internal/configuration/sources/env/serverselection.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
OpenVPNClientKeyPath = "/gluetun/client.key"
// OpenVPNClientCertificatePath is the OpenVPN client certificate filepath.
OpenVPNClientCertificatePath = "/gluetun/client.crt"
openVPNEncryptedPrivateKey = "/gluetun/encrypted-private-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.EncryptedPrivateKey, err = ReadFromFile(openVPNEncryptedPrivateKey)
if err != nil {
return settings, fmt.Errorf("cannot read client encrypted private key: %w", err)
}

return settings, nil
}
2 changes: 2 additions & 0 deletions internal/constants/providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
Purevpn = "purevpn"
Surfshark = "surfshark"
Torguard = "torguard"
VPNSecure = "vpn secure"
VPNUnlimited = "vpn unlimited"
Vyprvpn = "vyprvpn"
Wevpn = "wevpn"
Expand All @@ -45,6 +46,7 @@ func All() []string {
Purevpn,
Surfshark,
Torguard,
VPNSecure,
VPNUnlimited,
Vyprvpn,
Wevpn,
Expand Down
5 changes: 5 additions & 0 deletions internal/models/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
udpHeader = "UDP"
multiHopHeader = "MultiHop"
freeHeader = "Free"
premiumHeader = "Premium"
streamHeader = "Stream"
portForwardHeader = "Port forwarding"
)
Expand Down Expand Up @@ -68,6 +69,8 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) {
fields[i] = boolToMarkdown(s.MultiHop)
case freeHeader:
fields[i] = boolToMarkdown(s.Free)
case premiumHeader:
fields[i] = boolToMarkdown(s.Premium)
case streamHeader:
fields[i] = boolToMarkdown(s.Stream)
case portForwardHeader:
Expand Down Expand Up @@ -127,6 +130,8 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, multiHopHeader, tcpHeader, udpHeader}
case providers.Torguard:
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
case providers.VPNSecure:
return []string{regionHeader, cityHeader, hostnameHeader, premiumHeader}
case providers.VPNUnlimited:
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}
case providers.Vyprvpn:
Expand Down
1 change: 1 addition & 0 deletions internal/models/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Server struct {
WgPubKey string `json:"wgpubkey,omitempty"`
Free bool `json:"free,omitempty"`
Stream bool `json:"stream,omitempty"`
Premium bool `json:"premium,omitempty"`
PortForward bool `json:"port_forward,omitempty"`
Keep bool `json:"keep,omitempty"`
IPs []net.IP `json:"ips,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions internal/models/servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func Test_AllServers_MarshalJSON(t *testing.T) {
`"purevpn":{"version":0,"timestamp":0},` +
`"surfshark":{"version":0,"timestamp":0},` +
`"torguard":{"version":0,"timestamp":0},` +
`"vpn secure":{"version":0,"timestamp":0},` +
`"vpn unlimited":{"version":0,"timestamp":0},` +
`"vyprvpn":{"version":0,"timestamp":0},` +
`"wevpn":{"version":0,"timestamp":0},` +
Expand Down Expand Up @@ -84,6 +85,7 @@ func Test_AllServers_MarshalJSON(t *testing.T) {
`"purevpn":{"version":0,"timestamp":0},` +
`"surfshark":{"version":0,"timestamp":0},` +
`"torguard":{"version":0,"timestamp":0},` +
`"vpn secure":{"version":0,"timestamp":0},` +
`"vpn unlimited":{"version":0,"timestamp":0},` +
`"vyprvpn":{"version":0,"timestamp":0},` +
`"wevpn":{"version":0,"timestamp":0},` +
Expand Down
9 changes: 9 additions & 0 deletions internal/openvpn/parse/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ func ExtractPrivateKey(b []byte) (keyData string, err error) {

return keyData, nil
}

func ExtractEncryptedPrivateKey(b []byte) (keyData string, err error) {
keyData, err = extractPEM(b, "ENCRYPTED PRIVATE KEY")
if err != nil {
return "", fmt.Errorf("cannot extract PEM data: %w", err)
}

return keyData, nil
}

0 comments on commit 689e363

Please sign in to comment.