Feature: PrivateVPN support (#393)

This commit is contained in:
Quentin McGaw
2021-03-05 22:58:57 -05:00
committed by GitHub
parent be72f4a046
commit 9509b855f1
24 changed files with 597 additions and 8 deletions

3
.github/labels.yml vendored
View File

@@ -33,6 +33,9 @@
- name: ":cloud: Privado"
color: "cfe8d4"
description: ""
- name: ":cloud: PrivateVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: PureVPN"
color: "cfe8d4"
description: ""

View File

@@ -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

View File

@@ -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")

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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":

View File

@@ -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,

View File

@@ -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"`

View File

@@ -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

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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}}},
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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.

View File

@@ -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{"), "}")

View File

@@ -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"`

View File

@@ -2,6 +2,7 @@ package provider
const (
aes256cbc = "aes-256-cbc"
aes128gcm = "aes-128-gcm"
aes256gcm = "aes-256-gcm"
sha256 = "sha256"
)

View File

@@ -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{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.PrivatevpnCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-crypt>",
"-----BEGIN OpenVPN Static key V1-----",
constants.PrivatevpnOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-crypt>",
"",
}...)
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")
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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) +

View File

@@ -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
}

View File

@@ -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