diff --git a/.github/labels.yml b/.github/labels.yml
index 5b2c3782..d9a74d66 100644
--- a/.github/labels.yml
+++ b/.github/labels.yml
@@ -33,6 +33,9 @@
- name: ":cloud: Privado"
color: "cfe8d4"
description: ""
+- name: ":cloud: PrivateVPN"
+ color: "cfe8d4"
+ description: ""
- name: ":cloud: PureVPN"
color: "cfe8d4"
description: ""
diff --git a/README.md b/README.md
index 0ed14fee..0b3559a4 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost,
-HideMyAss, Mullvad, NordVPN, Privado, Private Internet Access, PureVPN,
-Surfshark, TorGuard, VyprVPN and Windscribe VPN servers
+HideMyAss, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN,
+PureVPN, Surfshark, TorGuard, VyprVPN and Windscribe VPN servers
using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
**ANNOUNCEMENT**: *New Docker image name `qmcgaw/gluetun`*
@@ -39,7 +39,7 @@ using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
## Features
- Based on Alpine 3.12 for a small Docker image of 52MB
-- Supports: **Cyberghost**, **HideMyAss**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PureVPN**, **Surfshark**, **TorGuard**, **Vyprvpn**, **Windscribe**, servers
+- Supports: **Cyberghost**, **HideMyAss**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **Vyprvpn**, **Windscribe**, servers
- Supports Openvpn only for now
- DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
diff --git a/internal/cli/update.go b/internal/cli/update.go
index 4c01448b..d603568e 100644
--- a/internal/cli/update.go
+++ b/internal/cli/update.go
@@ -28,6 +28,7 @@ func (c *cli) Update(ctx context.Context, args []string, os os.OS) error {
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers")
+ flagSet.BoolVar(&options.Privatevpn, "privatevpn", false, "Update Private VPN servers")
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard servers")
diff --git a/internal/configuration/openvpn.go b/internal/configuration/openvpn.go
index 8c131368..284e083b 100644
--- a/internal/configuration/openvpn.go
+++ b/internal/configuration/openvpn.go
@@ -57,8 +57,8 @@ var (
func (settings *OpenVPN) read(r reader) (err error) {
vpnsp, err := r.env.Inside("VPNSP", []string{
"cyberghost", "hidemyass", "mullvad", "nordvpn", "privado",
- "pia", "private internet access", "purevpn", "surfshark",
- "torguard", "vyprvpn", "windscribe"},
+ "pia", "private internet access", "privatevpn",
+ "purevpn", "surfshark", "torguard", "vyprvpn", "windscribe"},
params.Default("private internet access"))
if err != nil {
return err
@@ -125,6 +125,8 @@ func (settings *OpenVPN) read(r reader) (err error) {
readProvider = settings.Provider.readPrivado
case constants.PrivateInternetAccess:
readProvider = settings.Provider.readPrivateInternetAccess
+ case constants.Privatevpn:
+ readProvider = settings.Provider.readPrivatevpn
case constants.Purevpn:
readProvider = settings.Provider.readPurevpn
case constants.Surfshark:
diff --git a/internal/configuration/privatevpn.go b/internal/configuration/privatevpn.go
new file mode 100644
index 00000000..65aac63b
--- /dev/null
+++ b/internal/configuration/privatevpn.go
@@ -0,0 +1,52 @@
+package configuration
+
+import (
+ "github.com/qdm12/gluetun/internal/constants"
+)
+
+func (settings *Provider) privatevpnLines() (lines []string) {
+ if len(settings.ServerSelection.Countries) > 0 {
+ lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
+ }
+
+ if len(settings.ServerSelection.Cities) > 0 {
+ lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
+ }
+
+ if len(settings.ServerSelection.Hostnames) > 0 {
+ lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
+ }
+
+ return lines
+}
+
+func (settings *Provider) readPrivatevpn(r reader) (err error) {
+ settings.Name = constants.Privatevpn
+
+ settings.ServerSelection.Protocol, err = readProtocol(r.env)
+ if err != nil {
+ return err
+ }
+
+ settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
+ if err != nil {
+ return err
+ }
+
+ settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivatevpnCountryChoices())
+ if err != nil {
+ return err
+ }
+
+ settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivatevpnCityChoices())
+ if err != nil {
+ return err
+ }
+
+ settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PrivatevpnHostnameChoices())
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go
index d50c680e..293b29ff 100644
--- a/internal/configuration/provider.go
+++ b/internal/configuration/provider.go
@@ -39,6 +39,8 @@ func (settings *Provider) lines() (lines []string) {
providerLines = settings.nordvpnLines()
case "privado":
providerLines = settings.privadoLines()
+ case "privatevpn":
+ providerLines = settings.privatevpnLines()
case "private internet access":
providerLines = settings.privateinternetaccessLines()
case "purevpn":
diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go
index 04eeb328..3651c9e8 100644
--- a/internal/configuration/provider_test.go
+++ b/internal/configuration/provider_test.go
@@ -114,6 +114,24 @@ func Test_Provider_lines(t *testing.T) {
" |--Hostnames: a, b",
},
},
+ "privatevpn": {
+ settings: Provider{
+ Name: constants.Privatevpn,
+ ServerSelection: ServerSelection{
+ Protocol: constants.UDP,
+ Hostnames: []string{"a", "b"},
+ Countries: []string{"c", "d"},
+ Cities: []string{"e", "f"},
+ },
+ },
+ lines: []string{
+ "|--Privatevpn settings:",
+ " |--Network protocol: udp",
+ " |--Countries: c, d",
+ " |--Cities: e, f",
+ " |--Hostnames: a, b",
+ },
+ },
"private internet access": {
settings: Provider{
Name: constants.PrivateInternetAccess,
diff --git a/internal/configuration/selection.go b/internal/configuration/selection.go
index 261a4173..23c779b0 100644
--- a/internal/configuration/selection.go
+++ b/internal/configuration/selection.go
@@ -15,9 +15,9 @@ type ServerSelection struct {
// Cyberghost
Group string `json:"group"`
- Countries []string `json:"countries"` // HideMyAss, Mullvad, PureVPN
- Cities []string `json:"cities"` // HideMyAss, Mullvad, PureVPN, Windscribe
- Hostnames []string `json:"hostnames"` // HideMyAss, Windscribe, Privado
+ Countries []string `json:"countries"` // HideMyAss, Mullvad, PrivateVPN, PureVPN
+ Cities []string `json:"cities"` // HideMyAss, Mullvad, PrivateVPN, PureVPN, Windscribe
+ Hostnames []string `json:"hostnames"` // HideMyAss, PrivateVPN, Windscribe, Privado
// Mullvad
ISPs []string `json:"isps"`
diff --git a/internal/configuration/updater.go b/internal/configuration/updater.go
index 8ff7ad42..132885c8 100644
--- a/internal/configuration/updater.go
+++ b/internal/configuration/updater.go
@@ -16,6 +16,7 @@ type Updater struct {
Nordvpn bool `json:"nordvpn"`
PIA bool `json:"pia"`
Privado bool `json:"privado"`
+ Privatevpn bool `json:"privatevpn"`
Purevpn bool `json:"purevpn"`
Surfshark bool `json:"surfshark"`
Torguard bool `json:"torguard"`
@@ -49,6 +50,8 @@ func (settings *Updater) read(r reader) (err error) {
settings.Nordvpn = true
settings.Privado = true
settings.PIA = true
+ settings.Privado = true
+ settings.Privatevpn = true
settings.Purevpn = true
settings.Surfshark = true
settings.Torguard = true
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
index 0828edfc..7f0ed071 100644
--- a/internal/constants/constants.go
+++ b/internal/constants/constants.go
@@ -1,3 +1,25 @@
// Package constants defines constants shared throughout the program.
// It also defines constant maps and slices using functions.
package constants
+
+import "sort"
+
+func makeChoicesUnique(choices []string) []string {
+ uniqueChoices := map[string]struct{}{}
+ for _, choice := range choices {
+ uniqueChoices[choice] = struct{}{}
+ }
+
+ uniqueChoicesSlice := make([]string, len(uniqueChoices))
+ i := 0
+ for choice := range uniqueChoices {
+ uniqueChoicesSlice[i] = choice
+ i++
+ }
+
+ sort.Slice(uniqueChoicesSlice, func(i, j int) bool {
+ return uniqueChoicesSlice[i] < uniqueChoicesSlice[j]
+ })
+
+ return uniqueChoicesSlice
+}
diff --git a/internal/constants/countries.go b/internal/constants/countries.go
index d98d1a78..d69cbd7b 100644
--- a/internal/constants/countries.go
+++ b/internal/constants/countries.go
@@ -236,6 +236,7 @@ func CountryCodes() map[string]string {
"ua": "Ukraine",
"ae": "United Arab Emirates",
"gb": "United Kingdom",
+ "uk": "United Kingdom",
"um": "United States Minor Outlying Islands",
"us": "United States",
"uy": "Uruguay",
diff --git a/internal/constants/privatevpn.go b/internal/constants/privatevpn.go
new file mode 100644
index 00000000..99902e5b
--- /dev/null
+++ b/internal/constants/privatevpn.go
@@ -0,0 +1,120 @@
+package constants
+
+import (
+ "net"
+
+ "github.com/qdm12/gluetun/internal/models"
+)
+
+//nolint:lll
+const (
+ PrivatevpnCertificate = "MIIErTCCA5WgAwIBAgIJAPp3HmtYGCIOMA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJTRTELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN0b2NraG9sbTETMBEGA1UEChMKUHJpdmF0ZVZQTjEWMBQGA1UEAxMNUHJpdmF0ZVZQTiBDQTETMBEGA1UEKRMKUHJpdmF0ZVZQTjEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBwcml2YXR2cG4uc2UwHhcNMTcwNTI0MjAxNTM3WhcNMjcwNTIyMjAxNTM3WjCBlTELMAkGA1UEBhMCU0UxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdG9ja2hvbG0xEzARBgNVBAoTClByaXZhdGVWUE4xFjAUBgNVBAMTDVByaXZhdGVWUE4gQ0ExEzARBgNVBCkTClByaXZhdGVWUE4xIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAcHJpdmF0dnBuLnNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwjqTWbKk85WN8nd1TaBgBnBHceQWosp8mMHr4xWMTLagWRcq2Modfy7RPnBo9kyn5j/ZZwL/21gLWJbxidurGyZZdEV9Wb5KQl3DUNxa19kwAbkkEchdES61e99MjmQlWq4vGPXAHjEuDxOZ906AXglCyAvQoXcYW0mNm9yybWllVp1aBrCaZQrNYr7eoFvolqJXdQQ3FFsTBCYa5bHJcKQLBfsiqdJ/BAxhNkQtcmWNSgLy16qoxQpCsxNCxAcYnasuL4rwOP+RazBkJTPXA/2neCJC5rt+sXR9CSfiXdJGwMpYso5m31ZEd7JL2+is0FeAZ6ETrKMnEZMsTpTkdwIDAQABo4H9MIH6MB0GA1UdDgQWBBRCkBlC94zCY6VNncMnK36JxT7bazCBygYDVR0jBIHCMIG/gBRCkBlC94zCY6VNncMnK36JxT7ba6GBm6SBmDCBlTELMAkGA1UEBhMCU0UxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdG9ja2hvbG0xEzARBgNVBAoTClByaXZhdGVWUE4xFjAUBgNVBAMTDVByaXZhdGVWUE4gQ0ExEzARBgNVBCkTClByaXZhdGVWUE4xIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAcHJpdmF0dnBuLnNlggkA+ncea1gYIg4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAayugvExKDHar7t1zyYn99Vt1NMf46J8x4Dt9TNjBml5mR9nKvWmreMUuuOhLaO8Da466KGdXeDFNLcBYZd/J2iTawE6/3fmrML9H2sa+k/+E4uU5nQ84ZGOwCinCkMalVjM8EZ0/H2RZvLAVUnvPuUz2JfJhmiRkbeE75fVuqpAm9qdE+/7lg3oICYzxa6BJPxT+Imdjy3Q/FWdsXqX6aallhohPAZlMZgZL4eXECnV8rAfzyjOJggkMDZQt3Flc0Y4iDMfzrEhSOWMkNFBFwjK0F/dnhsX+fPX6GGRpUZgZcCt/hWvypqc05/SnrdKM/vV/jV/yZe0NVzY7S8Ur5g=="
+ PrivatevpnOpenvpnStaticKeyV1 = "a49082f082ca89d6a6bb4ecc7c047c6d428a1d3c8254a95206d38a61d7fbe65984214cd7d56eacc5a60803bffd677fa7294d4bfe555036339312de2dfb1335bd9d5fd94b04bba3a15fc5192aeb02fb6d8dd2ca831fad7509be5eefa8d1eaa689dc586c831a23b589c512662652ecf1bb3a4a673816aba434a04f6857b8c2f8bb265bfe48a7b8112539729d2f7d9734a720e1035188118c73fef1824d0237d5579ca382d703b4bb252acaedc753b12199f00154d3769efbcf85ef5ad6ee755cbeaa944cb98e7654286df54c793a8443f5363078e3da548ba0beed079df633283cefb256f6a4bcfc4ab2c4affc24955c1864d5458e84a7c210d0d186269e55dcf6"
+)
+
+func PrivatevpnCountryChoices() (choices []string) {
+ servers := PrivatevpnServers()
+ choices = make([]string, len(servers))
+ for i := range servers {
+ choices[i] = servers[i].Country
+ }
+ return makeChoicesUnique(choices)
+}
+
+func PrivatevpnCityChoices() (choices []string) {
+ servers := PrivatevpnServers()
+ choices = make([]string, len(servers))
+ for i := range servers {
+ choices[i] = servers[i].City
+ }
+ return makeChoicesUnique(choices)
+}
+
+func PrivatevpnHostnameChoices() (choices []string) {
+ servers := PrivatevpnServers()
+ choices = make([]string, len(servers))
+ for i := range servers {
+ choices[i] = servers[i].Hostname
+ }
+ return makeChoicesUnique(choices)
+}
+
+//nolint:lll
+// PrivatevpnServers returns a slice of all the server information for Privatevpn.
+func PrivatevpnServers() []models.PrivatevpnServer {
+ return []models.PrivatevpnServer{
+ {Country: "Argentina", City: "Buenos Aires", Hostname: "ar-bue.pvdata.host", IPs: []net.IP{{181, 119, 160, 59}}},
+ {Country: "Australia", City: "Melbourne", Hostname: "au-mel.pvdata.host", IPs: []net.IP{{103, 231, 88, 203}}},
+ {Country: "Australia", City: "Sydney", Hostname: "au-syd.pvdata.host", IPs: []net.IP{{143, 244, 63, 96}}},
+ {Country: "Austria", City: "Wien", Hostname: "at-wie.pvdata.host", IPs: []net.IP{{185, 9, 19, 91}}},
+ {Country: "Belgium", City: "Brussels", Hostname: "be-bru.pvdata.host", IPs: []net.IP{{185, 104, 186, 211}}},
+ {Country: "Brazil", City: "Sao Paulo", Hostname: "br-sao.pvdata.host", IPs: []net.IP{{45, 162, 230, 59}}},
+ {Country: "Bulgaria", City: "Sofia", Hostname: "bg-sof.pvdata.host", IPs: []net.IP{{185, 94, 192, 163}}},
+ {Country: "Canada", City: "Montreal", Hostname: "ca-mon.pvdata.host", IPs: []net.IP{{37, 120, 237, 163}, {87, 101, 92, 131}}},
+ {Country: "Canada", City: "Toronto", Hostname: "ca-tor.pvdata.host", IPs: []net.IP{{45, 148, 7, 3}, {45, 148, 7, 6}, {45, 148, 7, 8}}},
+ {Country: "Canada", City: "Vancouver", Hostname: "ca-van.pvdata.host", IPs: []net.IP{{74, 3, 160, 19}}},
+ {Country: "Chile", City: "Santiago", Hostname: "cl-san.pvdata.host", IPs: []net.IP{{216, 241, 14, 227}}},
+ {Country: "Costa Rica", City: "San Jose", Hostname: "cr-san.pvdata.host", IPs: []net.IP{{190, 10, 8, 218}}},
+ {Country: "Croatia", City: "Zagreb", Hostname: "hr-zag.pvdata.host", IPs: []net.IP{{85, 10, 56, 127}}},
+ {Country: "Cyprus", City: "Nicosia", Hostname: "cy-nic.pvdata.host", IPs: []net.IP{{185, 173, 226, 47}}},
+ {Country: "Czech Republic", City: "Prague", Hostname: "cz-pra.pvdata.host", IPs: []net.IP{{185, 156, 174, 179}}},
+ {Country: "Denmark", City: "Copenhagen", Hostname: "dk-cop.pvdata.host", IPs: []net.IP{{62, 115, 255, 188}, {62, 115, 255, 189}}},
+ {Country: "France", City: "Paris", Hostname: "fr-par.pvdata.host", IPs: []net.IP{{80, 239, 199, 102}, {80, 239, 199, 103}, {80, 239, 199, 104}, {80, 239, 199, 105}}},
+ {Country: "Germany", City: "Frankfurt", Hostname: "de-fra.pvdata.host", IPs: []net.IP{{193, 180, 119, 130}, {193, 180, 119, 131}}},
+ {Country: "Germany", City: "Nuremberg", Hostname: "de-nur.pvdata.host", IPs: []net.IP{{185, 89, 36, 3}}},
+ {Country: "Greece", City: "Athens", Hostname: "gr-ath.pvdata.host", IPs: []net.IP{{154, 57, 3, 33}}},
+ {Country: "Hong Kong", City: "Hong Kong", Hostname: "hk-hon.pvdata.host", IPs: []net.IP{{84, 17, 37, 58}}},
+ {Country: "Hungary", City: "Budapest", Hostname: "hu-bud.pvdata.host", IPs: []net.IP{{185, 104, 187, 67}}},
+ {Country: "Iceland", City: "Reykjavik", Hostname: "is-rey.pvdata.host", IPs: []net.IP{{82, 221, 113, 210}}},
+ {Country: "Indonesia", City: "Jakarta", Hostname: "id-jak.pvdata.host", IPs: []net.IP{{23, 248, 170, 136}}},
+ {Country: "Ireland", City: "Dublin", Hostname: "ie-dub.pvdata.host", IPs: []net.IP{{217, 138, 222, 67}}},
+ {Country: "Isle of Man", City: "Ballasalla", Hostname: "im-bal.pvdata.host", IPs: []net.IP{{81, 27, 96, 89}}},
+ {Country: "Italy", City: "Milan", Hostname: "it-mil.pvdata.host", IPs: []net.IP{{217, 212, 240, 90}, {217, 212, 240, 91}, {217, 212, 240, 92}, {217, 212, 240, 93}}},
+ {Country: "Japan", City: "Tokyo", Hostname: "jp-tok.pvdata.host", IPs: []net.IP{{89, 187, 160, 154}}},
+ {Country: "Korea", City: "Seoul", Hostname: "kr-seo.pvdata.host", IPs: []net.IP{{92, 223, 73, 37}}},
+ {Country: "Latvia", City: "Riga", Hostname: "lv-rig.pvdata.host", IPs: []net.IP{{80, 233, 134, 165}}},
+ {Country: "Lithuania", City: "Siauliai", Hostname: "lt-sia.pvdata.host", IPs: []net.IP{{5, 199, 171, 93}}},
+ {Country: "Luxembourg", City: "Steinsel", Hostname: "lu-ste.pvdata.host", IPs: []net.IP{{94, 242, 250, 71}}},
+ {Country: "Malaysia", City: "Kuala Lumpur", Hostname: "my-kua.pvdata.host", IPs: []net.IP{{128, 1, 160, 184}}},
+ {Country: "Malta", City: "Qormi", Hostname: "mt-qor.pvdata.host", IPs: []net.IP{{130, 185, 255, 25}}},
+ {Country: "Mexico", City: "Mexico City", Hostname: "mx-mex.pvdata.host", IPs: []net.IP{{190, 60, 16, 28}}},
+ {Country: "Moldova", City: "Chisinau", Hostname: "md-chi.pvdata.host", IPs: []net.IP{{178, 17, 172, 99}}},
+ {Country: "Netherlands", City: "Amsterdam", Hostname: "nl-ams.pvdata.host", IPs: []net.IP{{193, 180, 119, 194}, {193, 180, 119, 195}, {193, 180, 119, 196}, {193, 180, 119, 197}}},
+ {Country: "New Zealand", City: "Auckland", Hostname: "nz-auc.pvdata.host", IPs: []net.IP{{45, 252, 191, 34}}},
+ {Country: "Norway", City: "Oslo", Hostname: "no-osl.pvdata.host", IPs: []net.IP{{91, 205, 186, 26}}},
+ {Country: "Panama", City: "Panama City", Hostname: "pa-pan.pvdata.host", IPs: []net.IP{{200, 110, 155, 235}}},
+ {Country: "Peru", City: "Lima", Hostname: "pe-lim.pvdata.host", IPs: []net.IP{{170, 0, 81, 107}}},
+ {Country: "Philippines", City: "Manila", Hostname: "ph-man.pvdata.host", IPs: []net.IP{{128, 1, 209, 12}}},
+ {Country: "Portugal", City: "Lisbon", Hostname: "pt-lis.pvdata.host", IPs: []net.IP{{130, 185, 85, 107}}},
+ {Country: "Romania", City: "Bukarest", Hostname: "ro-buk.pvdata.host", IPs: []net.IP{{89, 40, 181, 203}}},
+ {Country: "Russian Federation", City: "Krasnoyarsk", Hostname: "ru-kra.pvdata.host", IPs: []net.IP{{92, 223, 87, 11}}},
+ {Country: "Russian Federation", City: "Moscow", Hostname: "ru-mos.pvdata.host", IPs: []net.IP{{92, 223, 103, 138}}},
+ {Country: "Russian Federation", City: "St Petersburg", Hostname: "ru-pet.pvdata.host", IPs: []net.IP{{95, 213, 148, 99}}},
+ {Country: "Serbia", City: "Belgrade", Hostname: "rs-bel.pvdata.host", IPs: []net.IP{{141, 98, 103, 166}}},
+ {Country: "Slovakia", City: "Bratislava", Hostname: "sg-sin.pvdata.host", IPs: []net.IP{{143, 244, 33, 81}}},
+ {Country: "Spain", City: "Madrid", Hostname: "es-mad.pvdata.host", IPs: []net.IP{{217, 212, 244, 92}, {217, 212, 244, 93}}},
+ {Country: "Sweden", City: "Gothenburg", Hostname: "se-got.pvdata.host", IPs: []net.IP{{193, 187, 91, 19}}},
+ {Country: "Sweden", City: "Kista", Hostname: "se-kis.pvdata.host", IPs: []net.IP{{193, 187, 88, 216}, {193, 187, 88, 217}, {193, 187, 88, 218}, {193, 187, 88, 219}, {193, 187, 88, 220}, {193, 187, 88, 221}, {193, 187, 88, 222}}},
+ {Country: "Sweden", City: "Stockholm", Hostname: "se-sto.pvdata.host", IPs: []net.IP{{193, 180, 119, 2}, {193, 180, 119, 6}, {193, 180, 119, 7}}},
+ {Country: "Switzerland", City: "Zurich", Hostname: "ch-zur.pvdata.host", IPs: []net.IP{{217, 212, 245, 92}, {217, 212, 245, 93}}},
+ {Country: "Taiwan", City: "Taipei", Hostname: "tw-tai.pvdata.host", IPs: []net.IP{{2, 58, 241, 51}}},
+ {Country: "Thailand", City: "Bangkok", Hostname: "th-ban.pvdata.host", IPs: []net.IP{{103, 27, 203, 234}}},
+ {Country: "Turkey", City: "Istanbul", Hostname: "tr-ist.pvdata.host", IPs: []net.IP{{92, 38, 180, 28}}},
+ {Country: "Ukraine", City: "Kiev", Hostname: "ua-kie.pvdata.host", IPs: []net.IP{{192, 121, 68, 131}}},
+ {Country: "Ukraine", City: "Nikolaev", Hostname: "ua-nik.pvdata.host", IPs: []net.IP{{194, 54, 83, 21}}},
+ {Country: "United Arab Emirates", City: "Dubai", Hostname: "ae-dub.pvdata.host", IPs: []net.IP{{45, 9, 249, 59}}},
+ {Country: "United Kingdom", City: "London", Hostname: "uk-lon.pvdata.host", IPs: []net.IP{{193, 180, 119, 66}, {193, 180, 119, 67}, {193, 180, 119, 68}, {193, 180, 119, 69}, {193, 180, 119, 70}}},
+ {Country: "United Kingdom", City: "London", Hostname: "uk-lon2.pvdata.host", IPs: []net.IP{{185, 41, 242, 67}}},
+ {Country: "United Kingdom", City: "London", Hostname: "uk-lon7.pvdata.host", IPs: []net.IP{{185, 125, 204, 179}}},
+ {Country: "United Kingdom", City: "Manchester", Hostname: "uk-man.pvdata.host", IPs: []net.IP{{185, 206, 227, 181}}},
+ {Country: "United States", City: "Buffalo", Hostname: "us-buf.pvdata.host", IPs: []net.IP{{172, 245, 13, 115}, {192, 210, 199, 35}}},
+ {Country: "United States", City: "Chicago", Hostname: "us-chi.pvdata.host", IPs: []net.IP{{185, 93, 1, 114}}},
+ {Country: "United States", City: "Dallas", Hostname: "us-dal.pvdata.host", IPs: []net.IP{{89, 187, 164, 97}}},
+ {Country: "United States", City: "Las Vegas", Hostname: "us-las.pvdata.host", IPs: []net.IP{{82, 102, 30, 19}}},
+ {Country: "United States", City: "Los Angeles", Hostname: "us-los.pvdata.host", IPs: []net.IP{{89, 187, 185, 78}, {185, 152, 67, 132}}},
+ {Country: "United States", City: "Miami", Hostname: "us-mia.pvdata.host", IPs: []net.IP{{195, 181, 163, 139}}},
+ {Country: "United States", City: "New York", Hostname: "us-nyc.pvdata.host", IPs: []net.IP{{45, 130, 86, 3}, {45, 130, 86, 5}, {45, 130, 86, 8}, {45, 130, 86, 10}, {45, 130, 86, 12}}},
+ {Country: "United States", City: "Phoenix", Hostname: "us-pho.pvdata.host", IPs: []net.IP{{82, 102, 30, 131}}},
+ {Country: "Vietnam", City: "Ho Chi Minh City", Hostname: "vn-hoc.pvdata.host", IPs: []net.IP{{210, 2, 64, 5}}},
+ }
+}
diff --git a/internal/constants/servers.go b/internal/constants/servers.go
index 12b37711..e3ee7038 100644
--- a/internal/constants/servers.go
+++ b/internal/constants/servers.go
@@ -31,6 +31,11 @@ func GetAllServers() (allServers models.AllServers) {
Timestamp: 1612031135,
Servers: PrivadoServers(),
},
+ Privatevpn: models.PrivatevpnServers{
+ Version: 1,
+ Timestamp: 1613861528,
+ Servers: PrivatevpnServers(),
+ },
Pia: models.PiaServers{
Version: 4,
Timestamp: 1613480675,
diff --git a/internal/constants/servers_test.go b/internal/constants/servers_test.go
index 3fe38932..c4e60bfe 100644
--- a/internal/constants/servers_test.go
+++ b/internal/constants/servers_test.go
@@ -64,6 +64,11 @@ func Test_versions(t *testing.T) {
version: allServers.Pia.Version,
digest: "3e6066ec",
},
+ "Privatevpn": {
+ model: models.PrivatevpnServer{},
+ version: allServers.Privatevpn.Version,
+ digest: "cba13d78",
+ },
"Purevpn": {
model: models.PurevpnServer{},
version: allServers.Purevpn.Version,
@@ -155,6 +160,11 @@ func Test_timestamps(t *testing.T) {
timestamp: allServers.Pia.Timestamp,
digest: "e0f95a01",
},
+ "Privatevpn": {
+ servers: allServers.Privatevpn.Servers,
+ timestamp: allServers.Privatevpn.Timestamp,
+ digest: "8ce3fba1",
+ },
"Purevpn": {
servers: allServers.Purevpn.Servers,
timestamp: allServers.Purevpn.Timestamp,
diff --git a/internal/constants/vpn.go b/internal/constants/vpn.go
index 3b9109a9..4e61e0f7 100644
--- a/internal/constants/vpn.go
+++ b/internal/constants/vpn.go
@@ -13,6 +13,8 @@ const (
Privado = "privado"
// PrivateInternetAccess is a VPN provider.
PrivateInternetAccess = "private internet access"
+ // Privatevpn is a VPN provider.
+ Privatevpn = "privatevpn"
// PureVPN is a VPN provider.
Purevpn = "purevpn"
// Surfshark is a VPN provider.
diff --git a/internal/models/server.go b/internal/models/server.go
index bd2b2c80..96d1c853 100644
--- a/internal/models/server.go
+++ b/internal/models/server.go
@@ -137,6 +137,18 @@ func (s *WindscribeServer) String() string {
s.Region, s.City, s.Hostname, goStringifyIP(s.IP))
}
+type PrivatevpnServer struct {
+ Country string `json:"country"`
+ City string `json:"city"`
+ Hostname string `json:"hostname"`
+ IPs []net.IP `json:"ip"`
+}
+
+func (s *PrivatevpnServer) String() string {
+ return fmt.Sprintf("{Country: %q, City: %q, Hostname: %q, IPs: %s}",
+ s.Country, s.City, s.Hostname, goStringifyIPs(s.IPs))
+}
+
func goStringifyIP(ip net.IP) string {
s := fmt.Sprintf("%#v", ip)
s = strings.TrimSuffix(strings.TrimPrefix(s, "net.IP{"), "}")
diff --git a/internal/models/servers.go b/internal/models/servers.go
index 61d2f3d1..8eb59334 100644
--- a/internal/models/servers.go
+++ b/internal/models/servers.go
@@ -8,6 +8,7 @@ type AllServers struct {
Nordvpn NordvpnServers `json:"nordvpn"`
Privado PrivadoServers `json:"privado"`
Pia PiaServers `json:"pia"`
+ Privatevpn PrivatevpnServers `json:"privatevpn"`
Purevpn PurevpnServers `json:"purevpn"`
Surfshark SurfsharkServers `json:"surfshark"`
Torguard TorguardServers `json:"torguard"`
@@ -22,6 +23,7 @@ func (a *AllServers) Count() int {
len(a.Nordvpn.Servers) +
len(a.Privado.Servers) +
len(a.Pia.Servers) +
+ len(a.Privatevpn.Servers) +
len(a.Purevpn.Servers) +
len(a.Surfshark.Servers) +
len(a.Torguard.Servers) +
@@ -59,6 +61,11 @@ type PiaServers struct {
Timestamp int64 `json:"timestamp"`
Servers []PIAServer `json:"servers"`
}
+type PrivatevpnServers struct {
+ Version uint16 `json:"version"`
+ Timestamp int64 `json:"timestamp"`
+ Servers []PrivatevpnServer `json:"servers"`
+}
type PurevpnServers struct {
Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"`
diff --git a/internal/provider/constants.go b/internal/provider/constants.go
index 29d350d0..123b76cd 100644
--- a/internal/provider/constants.go
+++ b/internal/provider/constants.go
@@ -2,6 +2,7 @@ package provider
const (
aes256cbc = "aes-256-cbc"
+ aes128gcm = "aes-128-gcm"
aes256gcm = "aes-256-gcm"
sha256 = "sha256"
)
diff --git a/internal/provider/privatevpn.go b/internal/provider/privatevpn.go
new file mode 100644
index 00000000..dade91e6
--- /dev/null
+++ b/internal/provider/privatevpn.go
@@ -0,0 +1,165 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "net"
+ "net/http"
+ "strconv"
+
+ "github.com/qdm12/gluetun/internal/configuration"
+ "github.com/qdm12/gluetun/internal/constants"
+ "github.com/qdm12/gluetun/internal/firewall"
+ "github.com/qdm12/gluetun/internal/models"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/golibs/os"
+)
+
+type privatevpn struct {
+ servers []models.PrivatevpnServer
+ randSource rand.Source
+}
+
+func newPrivatevpn(servers []models.PrivatevpnServer, timeNow timeNowFunc) *privatevpn {
+ return &privatevpn{
+ servers: servers,
+ randSource: rand.NewSource(timeNow().UnixNano()),
+ }
+}
+
+func (p *privatevpn) filterServers(countries, cities, hostnames []string) (servers []models.PrivatevpnServer) {
+ for _, server := range p.servers {
+ switch {
+ case
+ filterByPossibilities(server.Country, countries),
+ filterByPossibilities(server.City, cities),
+ filterByPossibilities(server.Hostname, hostnames):
+ default:
+ servers = append(servers, server)
+ }
+ }
+ return servers
+}
+
+func (p *privatevpn) notFoundErr(selection configuration.ServerSelection) error {
+ message := "no server found for protocol " + selection.Protocol
+
+ if len(selection.Countries) > 0 {
+ message += " + countries " + commaJoin(selection.Countries)
+ }
+
+ if len(selection.Cities) > 0 {
+ message += " + cities " + commaJoin(selection.Cities)
+ }
+
+ if len(selection.Hostnames) > 0 {
+ message += " + hostnames " + commaJoin(selection.Hostnames)
+ }
+
+ return fmt.Errorf(message)
+}
+
+func (p *privatevpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
+ connection models.OpenVPNConnection, err error) {
+ var port uint16
+ if selection.Protocol == constants.TCP {
+ port = 443
+ } else {
+ port = 1194
+ }
+
+ if selection.TargetIP != nil {
+ return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: selection.Protocol}, nil
+ }
+
+ servers := p.filterServers(selection.Countries, selection.Cities, selection.Hostnames)
+ if len(servers) == 0 {
+ return connection, p.notFoundErr(selection)
+ }
+
+ var connections []models.OpenVPNConnection
+ for _, server := range servers {
+ for _, ip := range server.IPs {
+ connection := models.OpenVPNConnection{
+ IP: ip,
+ Port: port,
+ Protocol: selection.Protocol,
+ }
+ connections = append(connections, connection)
+ }
+ }
+
+ return pickRandomConnection(connections, p.randSource), nil
+}
+
+func (p *privatevpn) BuildConf(connection models.OpenVPNConnection,
+ username string, settings configuration.OpenVPN) (lines []string) {
+ if len(settings.Cipher) == 0 {
+ settings.Cipher = aes128gcm
+ }
+ if len(settings.Auth) == 0 {
+ settings.Auth = sha256
+ }
+
+ lines = []string{
+ "client",
+ "dev tun",
+ "nobind",
+ "persist-key",
+ "remote-cert-tls server",
+ "tls-exit",
+
+ // Privatevpn specific
+ "comp-lzo",
+ "tun-ipv6",
+
+ // Added constant values
+ "auth-nocache",
+ "mute-replay-warnings",
+ "pull-filter ignore \"auth-token\"", // prevent auth failed loops
+ "pull-filter ignore \"block-outside-dns\"",
+ "auth-retry nointeract",
+ "suppress-timestamps",
+
+ // Modified variables
+ fmt.Sprintf("verb %d", settings.Verbosity),
+ fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
+ fmt.Sprintf("proto %s", connection.Protocol),
+ fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
+ fmt.Sprintf("cipher %s", settings.Cipher),
+ fmt.Sprintf("auth %s", settings.Auth),
+ }
+ if connection.Protocol == constants.UDP {
+ lines = append(lines, "key-direction 1")
+ }
+ if !settings.Root {
+ lines = append(lines, "user "+username)
+ }
+ if settings.MSSFix > 0 {
+ line := "mssfix " + strconv.Itoa(int(settings.MSSFix))
+ lines = append(lines, line)
+ }
+ lines = append(lines, []string{
+ "",
+ "-----BEGIN CERTIFICATE-----",
+ constants.PrivatevpnCertificate,
+ "-----END CERTIFICATE-----",
+ "",
+ }...)
+ lines = append(lines, []string{
+ "",
+ "-----BEGIN OpenVPN Static key V1-----",
+ constants.PrivatevpnOpenvpnStaticKeyV1,
+ "-----END OpenVPN Static key V1-----",
+ "",
+ "",
+ }...)
+ return lines
+}
+
+func (p *privatevpn) PortForward(ctx context.Context, client *http.Client,
+ openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
+ syncState func(port uint16) (pfFilepath string)) {
+ panic("port forwarding is not supported for privatevpn")
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 960eef04..1e39b8c3 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -37,6 +37,8 @@ func New(provider string, allServers models.AllServers, timeNow timeNowFunc) Pro
return newPrivado(allServers.Privado.Servers, timeNow)
case constants.PrivateInternetAccess:
return newPrivateInternetAccess(allServers.Pia.Servers, timeNow)
+ case constants.Privatevpn:
+ return newPrivatevpn(allServers.Privatevpn.Servers, timeNow)
case constants.Purevpn:
return newPurevpn(allServers.Purevpn.Servers, timeNow)
case constants.Surfshark:
diff --git a/internal/storage/merge.go b/internal/storage/merge.go
index 6468eeea..e1f8fb98 100644
--- a/internal/storage/merge.go
+++ b/internal/storage/merge.go
@@ -23,6 +23,7 @@ func (s *storage) mergeServers(hardcoded, persisted models.AllServers) models.Al
Nordvpn: s.mergeNordVPN(hardcoded.Nordvpn, persisted.Nordvpn),
Privado: s.mergePrivado(hardcoded.Privado, persisted.Privado),
Pia: s.mergePIA(hardcoded.Pia, persisted.Pia),
+ Privatevpn: s.mergePrivatevpn(hardcoded.Privatevpn, persisted.Privatevpn),
Purevpn: s.mergePureVPN(hardcoded.Purevpn, persisted.Purevpn),
Surfshark: s.mergeSurfshark(hardcoded.Surfshark, persisted.Surfshark),
Torguard: s.mergeTorguard(hardcoded.Torguard, persisted.Torguard),
@@ -106,6 +107,22 @@ func (s *storage) mergePIA(hardcoded, persisted models.PiaServers) models.PiaSer
return persisted
}
+func (s *storage) mergePrivatevpn(hardcoded, persisted models.PrivatevpnServers) models.PrivatevpnServers {
+ if persisted.Timestamp <= hardcoded.Timestamp {
+ return hardcoded
+ }
+ versionDiff := hardcoded.Version - persisted.Version
+ if versionDiff > 0 {
+ s.logger.Info(
+ "Privatevpn servers from file discarded because they are %d versions behind",
+ versionDiff)
+ return hardcoded
+ }
+ s.logger.Info("Using Privatevpn servers from file (%s more recent)",
+ getUnixTimeDifference(persisted.Timestamp, hardcoded.Timestamp))
+ return persisted
+}
+
func (s *storage) mergePureVPN(hardcoded, persisted models.PurevpnServers) models.PurevpnServers {
if persisted.Timestamp <= hardcoded.Timestamp {
return hardcoded
diff --git a/internal/storage/sync.go b/internal/storage/sync.go
index bb479931..e9f11d6e 100644
--- a/internal/storage/sync.go
+++ b/internal/storage/sync.go
@@ -23,6 +23,7 @@ func countServers(allServers models.AllServers) int {
len(allServers.Nordvpn.Servers) +
len(allServers.Privado.Servers) +
len(allServers.Pia.Servers) +
+ len(allServers.Privatevpn.Servers) +
len(allServers.Purevpn.Servers) +
len(allServers.Surfshark.Servers) +
len(allServers.Torguard.Servers) +
diff --git a/internal/updater/privatevpn.go b/internal/updater/privatevpn.go
new file mode 100644
index 00000000..2cb54643
--- /dev/null
+++ b/internal/updater/privatevpn.go
@@ -0,0 +1,133 @@
+package updater
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/qdm12/gluetun/internal/constants"
+ "github.com/qdm12/gluetun/internal/models"
+)
+
+func (u *updater) updatePrivatevpn(ctx context.Context) (err error) {
+ servers, warnings, err := findPrivatevpnServersFromZip(ctx, u.client, u.lookupIP)
+ if u.options.CLI {
+ for _, warning := range warnings {
+ u.logger.Warn("Privatevpn: %s", warning)
+ }
+ }
+ if err != nil {
+ return fmt.Errorf("cannot update Privatevpn servers: %w", err)
+ }
+ if u.options.Stdout {
+ u.println(stringifyPrivatevpnServers(servers))
+ }
+ u.servers.Privatevpn.Timestamp = u.timeNow().Unix()
+ u.servers.Privatevpn.Servers = servers
+ return nil
+}
+
+func findPrivatevpnServersFromZip(ctx context.Context, client *http.Client, lookupIP lookupIPFunc) (
+ servers []models.PrivatevpnServer, warnings []string, err error) {
+ // Note: all servers do both TCP and UDP
+ const zipURL = "https://privatevpn.com/client/PrivateVPN-TUN.zip"
+
+ contents, err := fetchAndExtractFiles(ctx, client, zipURL)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ trailingNumber := regexp.MustCompile(` [0-9]+$`)
+ countryCodes := constants.CountryCodes()
+
+ uniqueServers := map[string]models.PrivatevpnServer{} // key is the hostname
+
+ for fileName, content := range contents {
+ const prefix = "PrivateVPN-"
+ const suffix = "-TUN-443.ovpn"
+
+ if !strings.HasSuffix(fileName, suffix) {
+ continue // only process TCP servers as they're the same
+ }
+
+ var server models.PrivatevpnServer
+
+ s := strings.TrimPrefix(fileName, prefix)
+ s = strings.TrimSuffix(s, suffix)
+ s = trailingNumber.ReplaceAllString(s, "")
+
+ parts := strings.Split(s, "-")
+ var countryCode string
+ countryCode, server.City = parts[0], parts[1]
+ countryCode = strings.ToLower(countryCode)
+ var countryCodeOK bool
+ server.Country, countryCodeOK = countryCodes[countryCode]
+ if !countryCodeOK {
+ warnings = append(warnings, "unknown country code: "+countryCode)
+ server.Country = countryCode
+ }
+
+ var warning string
+ server.Hostname, warning, err = extractHostFromOVPN(content)
+ if len(warning) > 0 {
+ warnings = append(warnings, warning)
+ }
+ if err != nil {
+ return nil, warnings, err
+ }
+ if len(warning) > 0 {
+ continue
+ }
+
+ uniqueServers[server.Hostname] = server
+ }
+
+ hostnames := make([]string, len(uniqueServers))
+ i := 0
+ for hostname := range uniqueServers {
+ hostnames[i] = hostname
+ i++
+ }
+
+ const failOnError = false
+ hostToIPs, newWarnings, _ := parallelResolve(ctx, lookupIP, hostnames, 5, time.Second, failOnError)
+ if len(newWarnings) > 0 {
+ warnings = append(warnings, newWarnings...)
+ }
+
+ for hostname, server := range uniqueServers {
+ ips := hostToIPs[hostname]
+ if len(ips) == 0 {
+ continue
+ }
+ server.IPs = ips
+ servers = append(servers, server)
+ }
+
+ sort.Slice(servers, func(i, j int) bool {
+ if servers[i].Country == servers[j].Country {
+ if servers[i].City == servers[j].City {
+ return servers[i].Hostname < servers[j].Hostname
+ }
+ return servers[i].City < servers[j].City
+ }
+ return servers[i].Country < servers[j].Country
+ })
+
+ return servers, warnings, nil
+}
+
+func stringifyPrivatevpnServers(servers []models.PrivatevpnServer) (s string) {
+ s = "func PrivatevpnServers() []models.PrivatevpnServer {\n"
+ s += " return []models.PrivatevpnServer{\n"
+ for _, server := range servers {
+ s += " " + server.String() + ",\n"
+ }
+ s += " }\n"
+ s += "}"
+ return s
+}
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index 49d68fe8..dad21a1f 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -111,6 +111,16 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe
}
}
+ if u.options.Privatevpn {
+ u.logger.Info("updating Privatevpn servers...")
+ if err := u.updatePrivatevpn(ctx); err != nil {
+ if ctxErr := ctx.Err(); ctxErr != nil {
+ return allServers, ctxErr
+ }
+ u.logger.Error(err)
+ }
+ }
+
if u.options.Purevpn {
u.logger.Info("updating PureVPN servers...")
// TODO support servers offering only TCP or only UDP